diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 7cd04ce3..24711c4a 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -6,11 +6,11 @@ runs: steps: # Setup the SDKs - name: Set up Python - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 with: python-version: '3.14' - name: Setup JDK - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: distribution: zulu java-version: '21' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cafd4889..3bd2e54d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,7 +77,7 @@ jobs: echo "SHA256 checksums:" find app/build/outputs/apk -name '*.apk' -print0 | xargs -0 sha256sum - name: Upload mapping files - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: mapping path: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e423cccf..9e518406 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,7 +20,7 @@ jobs: with: fetch-depth: 0 # Need the tags to build - name: Setup Python - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 with: python-version: '3.14' - name: Run pre-commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b108ce5..4822ce7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,14 +59,14 @@ jobs: echo "SHA256 checksums:" find app/build/outputs \( -name '*.apk' -or -name '*.aab' \) -print0 | xargs -0 sha256sum - name: Upload AAB - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: AAB path: | app/build/outputs/bundle/**/*.aab compression-level: 0 - name: Upload mapping files - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: mapping path: | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 30bff204..6178f8ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,3 @@ -import com.android.build.api.artifact.SingleArtifact import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ProductFlavor import com.android.build.api.variant.FilterConfiguration @@ -6,7 +5,6 @@ import com.google.protobuf.gradle.id import com.mikepenz.aboutlibraries.plugin.DuplicateMode import com.mikepenz.aboutlibraries.plugin.DuplicateRule import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import java.io.File import java.util.Base64 import java.util.Properties @@ -196,7 +194,6 @@ configure { sourceSets { getByName("main") { kotlin.directories += "$buildDir/generated/seerr_api/src/main/kotlin" - kotlin.directories += "src/wolfskin/java" } } @@ -227,47 +224,6 @@ androidComponents { "Wholphin-${variant.flavorName}-${variant.buildType}-${output.versionName.get()}-${output.versionCode.get()}$abi.apk" output.outputFileName = outputFileName } - if (variant.name == "defaultDebug") { - val packagedApks = variant.artifacts.get(SingleArtifact.APK) - val artifactLoader = variant.artifacts.getBuiltArtifactsLoader() - val aapt2 = sdkComponents.aapt2.flatMap { it.executable } - val checkWolfskinBranding = tasks.register("checkWolfskinBranding") { - inputs.dir(packagedApks) - inputs.file(aapt2) - doLast { - val artifacts = checkNotNull(artifactLoader.load(packagedApks.get())) { - "Packaged default/debug APK metadata was not found; branding is unverified" - }.elements - check(artifacts.isNotEmpty()) { "No default/debug APKs were found; branding is unverified" } - val header = Regex("""^\s*resource 0x[\da-fA-F]+ (?:[^\s:]+:)?([^\s/]+)/([^\s]+)""") - val visibleTypes = setOf("string", "plurals", "array") - // Upstream legal attribution and its project URL must survive the branding cutover. - val legalNames = setOf("wolfskin_upstream_credit", "wolfskin_upstream_url") - val violations = sortedSetOf() - artifacts.forEach { artifact -> - val dump = providers.exec { - commandLine(aapt2.get().asFile, "dump", "resources", artifact.outputFile) - }.standardOutput.asText.get() - var currentName: String? = null - var inspected = 0 - dump.lineSequence().forEach { line -> - val resource = header.find(line) - if (resource != null) { - currentName = resource.groupValues[2].takeIf { resource.groupValues[1] in visibleTypes } - if (currentName != null) inspected++ - } else if (currentName != null && currentName !in legalNames && line.contains("Wholphin", ignoreCase = true)) { - violations += "${File(artifact.outputFile).name}:$currentName" - } - } - check(inspected > 0) { "No packaged string resources were decoded; branding is unverified" } - logger.lifecycle("Inspected $inspected string/plural/array resources in ${File(artifact.outputFile).name}") - } - check(violations.isEmpty()) { "User-visible upstream branding remains: ${violations.joinToString()}" } - logger.lifecycle("Wolfskin branding check passed (${artifacts.size} packaged debug APKs; legal attribution preserved)") - } - } - tasks.matching { it.name == "assembleDefaultDebug" }.configureEach { dependsOn(checkWolfskinBranding) } - } } } @@ -332,8 +288,6 @@ tasks.named("preBuild") { } dependencies { - implementation(project(":wolfskin-design")) - implementation(project(":conductarr-client")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(platform(libs.androidx.compose.bom)) @@ -361,7 +315,6 @@ dependencies { implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui.compose) implementation(libs.ass.media) - implementation(libs.exoplayerhdrutils) implementation(libs.coil.core) implementation(libs.coil.compose) @@ -452,18 +405,8 @@ dependencies { testImplementation(libs.robolectric) testImplementation(libs.hilt.android.testing) testImplementation(libs.androidx.compose.ui.test.junit4) - testImplementation("io.github.takahirom.roborazzi:roborazzi:1.77.0") - testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.77.0") androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.mockk.android) androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.androidx.compose.ui.test.manifest) } - -tasks.withType().configureEach { - dependsOn(":wolfskin-design:testDebugUnitTest") - systemProperty("robolectric.dependency.dir", project(":wolfskin-design").layout.buildDirectory.dir("robolectric-sdk").get().asFile.absolutePath) - systemProperty("robolectric.offline", "true") - systemProperty("roborazzi.test.record", "true") - systemProperty("wolfskin.captureDirectory", rootProject.layout.buildDirectory.dir("wolfskin-captures").get().asFile.absolutePath) -} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b2a0c548..cddfb043 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -22,6 +22,3 @@ -keep class androidx.media3.ui.SubtitlePainter { private final float outlineWidth; } - -# Keep external/native functions --keep class com.suyashbelekar.exoplayerhdrutils.libdovi.LibDovi { *; } diff --git a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/40.json b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/40.json deleted file mode 100644 index 3e874e4e..00000000 --- a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/40.json +++ /dev/null @@ -1,834 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 40, - "identityHash": "b297c087485321c3ee8282ad1ff675a0", - "entities": [ - { - "tableName": "servers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT" - }, - { - "fieldPath": "lastUsed", - "columnName": "lastUsed", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, `preferredAudioLanguage` TEXT NOT NULL DEFAULT '', `preferredSubtitleLanguage` TEXT NOT NULL DEFAULT '', `subtitleMode` TEXT NOT NULL DEFAULT 'USE_USER_PROFILE', FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "rowId", - "columnName": "rowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "serverId", - "columnName": "serverId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accessToken", - "columnName": "accessToken", - "affinity": "TEXT" - }, - { - "fieldPath": "pin", - "columnName": "pin", - "affinity": "TEXT" - }, - { - "fieldPath": "requireLogin", - "columnName": "requireLogin", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "lastUsed", - "columnName": "lastUsed", - "affinity": "TEXT" - }, - { - "fieldPath": "uiLanguage", - "columnName": "uiLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "appPreferences.preferredAudioLanguage", - "columnName": "preferredAudioLanguage", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "appPreferences.preferredSubtitleLanguage", - "columnName": "preferredSubtitleLanguage", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "appPreferences.subtitleMode", - "columnName": "subtitleMode", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'USE_USER_PROFILE'" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "rowId" - ] - }, - "indices": [ - { - "name": "index_users_id_serverId", - "unique": true, - "columnNames": [ - "id", - "serverId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)" - }, - { - "name": "index_users_id", - "unique": false, - "columnNames": [ - "id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)" - }, - { - "name": "index_users_serverId", - "unique": false, - "columnNames": [ - "serverId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)" - } - ], - "foreignKeys": [ - { - "table": "servers", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "serverId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "ItemPlayback", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "rowId", - "columnName": "rowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sourceId", - "columnName": "sourceId", - "affinity": "TEXT" - }, - { - "fieldPath": "audioIndex", - "columnName": "audioIndex", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subtitleIndex", - "columnName": "subtitleIndex", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "rowId" - ] - }, - "indices": [ - { - "name": "index_ItemPlayback_userId_itemId", - "unique": true, - "columnNames": [ - "userId", - "itemId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" - } - ], - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "NavDrawerPinnedItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "order", - "columnName": "order", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "-1" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "LibraryDisplayInfo", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sort", - "columnName": "sort", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "direction", - "columnName": "direction", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "filter", - "columnName": "filter", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'{}'" - }, - { - "fieldPath": "viewOptions", - "columnName": "viewOptions", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId" - ] - }, - "indices": [ - { - "name": "index_LibraryDisplayInfo_userId_itemId", - "unique": true, - "columnNames": [ - "userId", - "itemId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" - } - ], - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "playback_effects", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))", - "fields": [ - { - "fieldPath": "jellyfinUserRowId", - "columnName": "jellyfinUserRowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "videoFilter.rotation", - "columnName": "rotation", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.brightness", - "columnName": "brightness", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.contrast", - "columnName": "contrast", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.saturation", - "columnName": "saturation", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.hue", - "columnName": "hue", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.red", - "columnName": "red", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.green", - "columnName": "green", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.blue", - "columnName": "blue", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.blur", - "columnName": "blur", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "jellyfinUserRowId", - "itemId", - "type" - ] - } - }, - { - "tableName": "PlaybackLanguageChoice", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "seriesId", - "columnName": "seriesId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT" - }, - { - "fieldPath": "audioLanguage", - "columnName": "audioLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "subtitleLanguage", - "columnName": "subtitleLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "subtitlesDisabled", - "columnName": "subtitlesDisabled", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "seriesId" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "ItemTrackModification", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "trackIndex", - "columnName": "trackIndex", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "delayMs", - "columnName": "delayMs", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId", - "trackIndex" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "seerr_servers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_seerr_servers_url", - "unique": true, - "columnNames": [ - "url" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)" - } - ] - }, - { - "tableName": "seerr_users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "jellyfinUserRowId", - "columnName": "jellyfinUserRowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "serverId", - "columnName": "serverId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "authMethod", - "columnName": "authMethod", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT" - }, - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT" - }, - { - "fieldPath": "credential", - "columnName": "credential", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "jellyfinUserRowId", - "serverId" - ] - }, - "foreignKeys": [ - { - "table": "seerr_servers", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "serverId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "jellyfinUserRowId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "RememberedTab", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "index", - "columnName": "index", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId" - ] - }, - "indices": [ - { - "name": "index_RememberedTab_userId_itemId", - "unique": true, - "columnNames": [ - "userId", - "itemId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" - } - ], - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "SeriesTrackChoice", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `parentId` TEXT NOT NULL, `type` TEXT NOT NULL, `parentType` TEXT NOT NULL, `itemId` TEXT, `language` TEXT, `activation` TEXT NOT NULL, `trackFlags` INTEGER NOT NULL, `codec` TEXT, `relativeTrackIndex` INTEGER, `title` TEXT, `audioChannels` INTEGER, PRIMARY KEY(`userId`, `parentId`, `type`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "parentId", - "columnName": "parentId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "parentType", - "columnName": "parentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT" - }, - { - "fieldPath": "language", - "columnName": "language", - "affinity": "TEXT" - }, - { - "fieldPath": "activation", - "columnName": "activation", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "trackFlags", - "columnName": "trackFlags", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "codec", - "columnName": "codec", - "affinity": "TEXT" - }, - { - "fieldPath": "relativeTrackIndex", - "columnName": "relativeTrackIndex", - "affinity": "INTEGER" - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT" - }, - { - "fieldPath": "audioChannels", - "columnName": "audioChannels", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "parentId", - "type" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b297c087485321c3ee8282ad1ff675a0')" - ] - } -} diff --git a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/41.json b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/41.json deleted file mode 100644 index 4a41a052..00000000 --- a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/41.json +++ /dev/null @@ -1,841 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 41, - "identityHash": "1f2fe7912f9e3ab83b77029e07488e84", - "entities": [ - { - "tableName": "servers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT" - }, - { - "fieldPath": "lastUsed", - "columnName": "lastUsed", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, `preferredAudioLanguage` TEXT NOT NULL DEFAULT '', `preferredSubtitleLanguage` TEXT NOT NULL DEFAULT '', `subtitleMode` TEXT NOT NULL DEFAULT 'USE_USER_PROFILE', `c_homeSettingsSource` TEXT NOT NULL DEFAULT 'UNSET', FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "rowId", - "columnName": "rowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "serverId", - "columnName": "serverId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accessToken", - "columnName": "accessToken", - "affinity": "TEXT" - }, - { - "fieldPath": "pin", - "columnName": "pin", - "affinity": "TEXT" - }, - { - "fieldPath": "requireLogin", - "columnName": "requireLogin", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "lastUsed", - "columnName": "lastUsed", - "affinity": "TEXT" - }, - { - "fieldPath": "uiLanguage", - "columnName": "uiLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "appPreferences.preferredAudioLanguage", - "columnName": "preferredAudioLanguage", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "appPreferences.preferredSubtitleLanguage", - "columnName": "preferredSubtitleLanguage", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "appPreferences.subtitleMode", - "columnName": "subtitleMode", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'USE_USER_PROFILE'" - }, - { - "fieldPath": "config.homeSettingsSource", - "columnName": "c_homeSettingsSource", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'UNSET'" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "rowId" - ] - }, - "indices": [ - { - "name": "index_users_id_serverId", - "unique": true, - "columnNames": [ - "id", - "serverId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)" - }, - { - "name": "index_users_id", - "unique": false, - "columnNames": [ - "id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)" - }, - { - "name": "index_users_serverId", - "unique": false, - "columnNames": [ - "serverId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)" - } - ], - "foreignKeys": [ - { - "table": "servers", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "serverId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "ItemPlayback", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "rowId", - "columnName": "rowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sourceId", - "columnName": "sourceId", - "affinity": "TEXT" - }, - { - "fieldPath": "audioIndex", - "columnName": "audioIndex", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subtitleIndex", - "columnName": "subtitleIndex", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "rowId" - ] - }, - "indices": [ - { - "name": "index_ItemPlayback_userId_itemId", - "unique": true, - "columnNames": [ - "userId", - "itemId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" - } - ], - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "NavDrawerPinnedItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "order", - "columnName": "order", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "-1" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "LibraryDisplayInfo", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sort", - "columnName": "sort", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "direction", - "columnName": "direction", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "filter", - "columnName": "filter", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'{}'" - }, - { - "fieldPath": "viewOptions", - "columnName": "viewOptions", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId" - ] - }, - "indices": [ - { - "name": "index_LibraryDisplayInfo_userId_itemId", - "unique": true, - "columnNames": [ - "userId", - "itemId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" - } - ], - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "playback_effects", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))", - "fields": [ - { - "fieldPath": "jellyfinUserRowId", - "columnName": "jellyfinUserRowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "videoFilter.rotation", - "columnName": "rotation", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.brightness", - "columnName": "brightness", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.contrast", - "columnName": "contrast", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.saturation", - "columnName": "saturation", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.hue", - "columnName": "hue", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.red", - "columnName": "red", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.green", - "columnName": "green", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.blue", - "columnName": "blue", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "videoFilter.blur", - "columnName": "blur", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "jellyfinUserRowId", - "itemId", - "type" - ] - } - }, - { - "tableName": "PlaybackLanguageChoice", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "seriesId", - "columnName": "seriesId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT" - }, - { - "fieldPath": "audioLanguage", - "columnName": "audioLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "subtitleLanguage", - "columnName": "subtitleLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "subtitlesDisabled", - "columnName": "subtitlesDisabled", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "seriesId" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "ItemTrackModification", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "trackIndex", - "columnName": "trackIndex", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "delayMs", - "columnName": "delayMs", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId", - "trackIndex" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "seerr_servers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_seerr_servers_url", - "unique": true, - "columnNames": [ - "url" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)" - } - ] - }, - { - "tableName": "seerr_users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "jellyfinUserRowId", - "columnName": "jellyfinUserRowId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "serverId", - "columnName": "serverId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "authMethod", - "columnName": "authMethod", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT" - }, - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT" - }, - { - "fieldPath": "credential", - "columnName": "credential", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "jellyfinUserRowId", - "serverId" - ] - }, - "foreignKeys": [ - { - "table": "seerr_servers", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "serverId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "jellyfinUserRowId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "RememberedTab", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "index", - "columnName": "index", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "itemId" - ] - }, - "indices": [ - { - "name": "index_RememberedTab_userId_itemId", - "unique": true, - "columnNames": [ - "userId", - "itemId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" - } - ], - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - }, - { - "tableName": "SeriesTrackChoice", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `parentId` TEXT NOT NULL, `type` TEXT NOT NULL, `parentType` TEXT NOT NULL, `itemId` TEXT, `language` TEXT, `activation` TEXT NOT NULL, `trackFlags` INTEGER NOT NULL, `codec` TEXT, `relativeTrackIndex` INTEGER, `title` TEXT, `audioChannels` INTEGER, PRIMARY KEY(`userId`, `parentId`, `type`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "parentId", - "columnName": "parentId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "parentType", - "columnName": "parentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "itemId", - "columnName": "itemId", - "affinity": "TEXT" - }, - { - "fieldPath": "language", - "columnName": "language", - "affinity": "TEXT" - }, - { - "fieldPath": "activation", - "columnName": "activation", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "trackFlags", - "columnName": "trackFlags", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "codec", - "columnName": "codec", - "affinity": "TEXT" - }, - { - "fieldPath": "relativeTrackIndex", - "columnName": "relativeTrackIndex", - "affinity": "INTEGER" - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT" - }, - { - "fieldPath": "audioChannels", - "columnName": "audioChannels", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId", - "parentId", - "type" - ] - }, - "foreignKeys": [ - { - "table": "users", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "userId" - ], - "referencedColumns": [ - "rowId" - ] - } - ] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1f2fe7912f9e3ab83b77029e07488e84')" - ] - } -} diff --git a/app/src/appstore/AndroidManifest.xml b/app/src/appstore/AndroidManifest.xml index e024ca83..8a762a28 100644 --- a/app/src/appstore/AndroidManifest.xml +++ b/app/src/appstore/AndroidManifest.xml @@ -3,8 +3,5 @@ xmlns:tools="http://schemas.android.com/tools"> - diff --git a/app/src/debug/assets/library/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg deleted file mode 100644 index b9b72894..00000000 Binary files a/app/src/debug/assets/library/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-12-years-a-slave-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-12-years-a-slave-2013-poster.jpg deleted file mode 100644 index ff1f55e6..00000000 Binary files a/app/src/debug/assets/library/assets/movie-12-years-a-slave-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-a-bronx-tale-1993-poster.jpg b/app/src/debug/assets/library/assets/movie-a-bronx-tale-1993-poster.jpg deleted file mode 100644 index f7444271..00000000 Binary files a/app/src/debug/assets/library/assets/movie-a-bronx-tale-1993-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-a-silent-voice-the-movie-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-a-silent-voice-the-movie-2016-poster.jpg deleted file mode 100644 index 580b22f4..00000000 Binary files a/app/src/debug/assets/library/assets/movie-a-silent-voice-the-movie-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-absolution-2024-poster.jpg b/app/src/debug/assets/library/assets/movie-absolution-2024-poster.jpg deleted file mode 100644 index 3ce49112..00000000 Binary files a/app/src/debug/assets/library/assets/movie-absolution-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-across-the-hall-2009-poster.jpg b/app/src/debug/assets/library/assets/movie-across-the-hall-2009-poster.jpg deleted file mode 100644 index 52362b4e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-across-the-hall-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-addicted-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-addicted-2014-poster.jpg deleted file mode 100644 index e9a03c14..00000000 Binary files a/app/src/debug/assets/library/assets/movie-addicted-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-adulterers-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-adulterers-2016-poster.jpg deleted file mode 100644 index 8b65745b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-adulterers-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-all-the-money-in-the-world-2017-poster.jpg b/app/src/debug/assets/library/assets/movie-all-the-money-in-the-world-2017-poster.jpg deleted file mode 100644 index 8beb2f7f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-all-the-money-in-the-world-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-alone-with-her-2006-poster.jpg b/app/src/debug/assets/library/assets/movie-alone-with-her-2006-poster.jpg deleted file mode 100644 index 7ce6fa96..00000000 Binary files a/app/src/debug/assets/library/assets/movie-alone-with-her-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-american-gangster-2007-poster.jpg b/app/src/debug/assets/library/assets/movie-american-gangster-2007-poster.jpg deleted file mode 100644 index 0f41d299..00000000 Binary files a/app/src/debug/assets/library/assets/movie-american-gangster-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-annie-hall-1977-poster.jpg b/app/src/debug/assets/library/assets/movie-annie-hall-1977-poster.jpg deleted file mode 100644 index 9023a691..00000000 Binary files a/app/src/debug/assets/library/assets/movie-annie-hall-1977-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg b/app/src/debug/assets/library/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg deleted file mode 100644 index 030addf0..00000000 Binary files a/app/src/debug/assets/library/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-apocalypse-now-1979-poster.jpg b/app/src/debug/assets/library/assets/movie-apocalypse-now-1979-poster.jpg deleted file mode 100644 index 09cd6aa8..00000000 Binary files a/app/src/debug/assets/library/assets/movie-apocalypse-now-1979-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-appleseed-alpha-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-appleseed-alpha-2014-poster.jpg deleted file mode 100644 index 62bd0afa..00000000 Binary files a/app/src/debug/assets/library/assets/movie-appleseed-alpha-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg b/app/src/debug/assets/library/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg deleted file mode 100644 index 4550bdaa..00000000 Binary files a/app/src/debug/assets/library/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-arctic-dogs-2019-poster.jpg b/app/src/debug/assets/library/assets/movie-arctic-dogs-2019-poster.jpg deleted file mode 100644 index 07552443..00000000 Binary files a/app/src/debug/assets/library/assets/movie-arctic-dogs-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-arrival-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-arrival-2016-poster.jpg deleted file mode 100644 index fe224a15..00000000 Binary files a/app/src/debug/assets/library/assets/movie-arrival-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg deleted file mode 100644 index f57f1052..00000000 Binary files a/app/src/debug/assets/library/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-avengers-age-of-ultron-2015-poster.jpg b/app/src/debug/assets/library/assets/movie-avengers-age-of-ultron-2015-poster.jpg deleted file mode 100644 index 300840ff..00000000 Binary files a/app/src/debug/assets/library/assets/movie-avengers-age-of-ultron-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg deleted file mode 100644 index b096fff2..00000000 Binary files a/app/src/debug/assets/library/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-avengers-infinity-war-2018-poster.jpg b/app/src/debug/assets/library/assets/movie-avengers-infinity-war-2018-poster.jpg deleted file mode 100644 index 57df9298..00000000 Binary files a/app/src/debug/assets/library/assets/movie-avengers-infinity-war-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-awake-2007-poster.jpg b/app/src/debug/assets/library/assets/movie-awake-2007-poster.jpg deleted file mode 100644 index 32595897..00000000 Binary files a/app/src/debug/assets/library/assets/movie-awake-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-batman-ninja-2018-poster.jpg b/app/src/debug/assets/library/assets/movie-batman-ninja-2018-poster.jpg deleted file mode 100644 index 7f59127b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-batman-ninja-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg b/app/src/debug/assets/library/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg deleted file mode 100644 index ce5623fe..00000000 Binary files a/app/src/debug/assets/library/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg deleted file mode 100644 index f8115170..00000000 Binary files a/app/src/debug/assets/library/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-black-adam-2022-poster.jpg b/app/src/debug/assets/library/assets/movie-black-adam-2022-poster.jpg deleted file mode 100644 index dde008b5..00000000 Binary files a/app/src/debug/assets/library/assets/movie-black-adam-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-black-bear-2020-poster.jpg b/app/src/debug/assets/library/assets/movie-black-bear-2020-poster.jpg deleted file mode 100644 index 61c8860f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-black-bear-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-black-panther-2018-poster.jpg b/app/src/debug/assets/library/assets/movie-black-panther-2018-poster.jpg deleted file mode 100644 index 6fdaa3d2..00000000 Binary files a/app/src/debug/assets/library/assets/movie-black-panther-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-black-panther-wakanda-forever-2022-poster.jpg b/app/src/debug/assets/library/assets/movie-black-panther-wakanda-forever-2022-poster.jpg deleted file mode 100644 index d0219a75..00000000 Binary files a/app/src/debug/assets/library/assets/movie-black-panther-wakanda-forever-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-blame-2017-poster.jpg b/app/src/debug/assets/library/assets/movie-blame-2017-poster.jpg deleted file mode 100644 index 8d50255f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-blame-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg b/app/src/debug/assets/library/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg deleted file mode 100644 index d3619fbd..00000000 Binary files a/app/src/debug/assets/library/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-bohemian-rhapsody-2018-poster.jpg b/app/src/debug/assets/library/assets/movie-bohemian-rhapsody-2018-poster.jpg deleted file mode 100644 index 20d2f9ee..00000000 Binary files a/app/src/debug/assets/library/assets/movie-bohemian-rhapsody-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-braveheart-1995-poster.jpg b/app/src/debug/assets/library/assets/movie-braveheart-1995-poster.jpg deleted file mode 100644 index 32fd8d4d..00000000 Binary files a/app/src/debug/assets/library/assets/movie-braveheart-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-butterfly-on-a-wheel-2007-poster.jpg b/app/src/debug/assets/library/assets/movie-butterfly-on-a-wheel-2007-poster.jpg deleted file mode 100644 index b54cc5c7..00000000 Binary files a/app/src/debug/assets/library/assets/movie-butterfly-on-a-wheel-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-by-any-means-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-by-any-means-2026-poster.jpg deleted file mode 100644 index 92fd601f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-by-any-means-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-captain-america-civil-war-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-captain-america-civil-war-2016-poster.jpg deleted file mode 100644 index 426142ef..00000000 Binary files a/app/src/debug/assets/library/assets/movie-captain-america-civil-war-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-captain-america-the-first-avenger-2011-poster.jpg b/app/src/debug/assets/library/assets/movie-captain-america-the-first-avenger-2011-poster.jpg deleted file mode 100644 index bba96574..00000000 Binary files a/app/src/debug/assets/library/assets/movie-captain-america-the-first-avenger-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-captain-america-the-winter-soldier-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-captain-america-the-winter-soldier-2014-poster.jpg deleted file mode 100644 index c2dc5612..00000000 Binary files a/app/src/debug/assets/library/assets/movie-captain-america-the-winter-soldier-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-careful-what-you-wish-for-2015-poster.jpg b/app/src/debug/assets/library/assets/movie-careful-what-you-wish-for-2015-poster.jpg deleted file mode 100644 index b58ce600..00000000 Binary files a/app/src/debug/assets/library/assets/movie-careful-what-you-wish-for-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-castle-in-the-sky-1986-poster.jpg b/app/src/debug/assets/library/assets/movie-castle-in-the-sky-1986-poster.jpg deleted file mode 100644 index a6a99fee..00000000 Binary files a/app/src/debug/assets/library/assets/movie-castle-in-the-sky-1986-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-chicago-2002-poster.jpg b/app/src/debug/assets/library/assets/movie-chicago-2002-poster.jpg deleted file mode 100644 index 1def9414..00000000 Binary files a/app/src/debug/assets/library/assets/movie-chicago-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-citizen-kane-1941-poster.jpg b/app/src/debug/assets/library/assets/movie-citizen-kane-1941-poster.jpg deleted file mode 100644 index f26414a7..00000000 Binary files a/app/src/debug/assets/library/assets/movie-citizen-kane-1941-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-coherence-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-coherence-2014-poster.jpg deleted file mode 100644 index f3601b9b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-coherence-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-conclave-2024-poster.jpg b/app/src/debug/assets/library/assets/movie-conclave-2024-poster.jpg deleted file mode 100644 index c6aecefa..00000000 Binary files a/app/src/debug/assets/library/assets/movie-conclave-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-crime-of-passion-1956-poster.jpg b/app/src/debug/assets/library/assets/movie-crime-of-passion-1956-poster.jpg deleted file mode 100644 index 4acfa871..00000000 Binary files a/app/src/debug/assets/library/assets/movie-crime-of-passion-1956-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-deep-water-2022-poster.jpg b/app/src/debug/assets/library/assets/movie-deep-water-2022-poster.jpg deleted file mode 100644 index 018d4f4b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-deep-water-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-derailed-2005-poster.jpg b/app/src/debug/assets/library/assets/movie-derailed-2005-poster.jpg deleted file mode 100644 index d93f3124..00000000 Binary files a/app/src/debug/assets/library/assets/movie-derailed-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-die-my-love-2025-poster.jpg b/app/src/debug/assets/library/assets/movie-die-my-love-2025-poster.jpg deleted file mode 100644 index f6eca818..00000000 Binary files a/app/src/debug/assets/library/assets/movie-die-my-love-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-dream-house-2011-poster.jpg b/app/src/debug/assets/library/assets/movie-dream-house-2011-poster.jpg deleted file mode 100644 index d3444637..00000000 Binary files a/app/src/debug/assets/library/assets/movie-dream-house-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-dream-lover-1994-poster.jpg b/app/src/debug/assets/library/assets/movie-dream-lover-1994-poster.jpg deleted file mode 100644 index 3b563cf9..00000000 Binary files a/app/src/debug/assets/library/assets/movie-dream-lover-1994-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-drop-2025-poster.jpg b/app/src/debug/assets/library/assets/movie-drop-2025-poster.jpg deleted file mode 100644 index 4cf7d66b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-drop-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-eastern-promises-2007-poster.jpg b/app/src/debug/assets/library/assets/movie-eastern-promises-2007-poster.jpg deleted file mode 100644 index 01a26f3b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-eastern-promises-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-easy-a-2010-poster.jpg b/app/src/debug/assets/library/assets/movie-easy-a-2010-poster.jpg deleted file mode 100644 index 45e13a32..00000000 Binary files a/app/src/debug/assets/library/assets/movie-easy-a-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg b/app/src/debug/assets/library/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg deleted file mode 100644 index 7a62df45..00000000 Binary files a/app/src/debug/assets/library/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-emancipation-2022-poster.jpg b/app/src/debug/assets/library/assets/movie-emancipation-2022-poster.jpg deleted file mode 100644 index c1bee971..00000000 Binary files a/app/src/debug/assets/library/assets/movie-emancipation-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-faces-1968-poster.jpg b/app/src/debug/assets/library/assets/movie-faces-1968-poster.jpg deleted file mode 100644 index 43f996bd..00000000 Binary files a/app/src/debug/assets/library/assets/movie-faces-1968-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-fatale-2020-poster.jpg b/app/src/debug/assets/library/assets/movie-fatale-2020-poster.jpg deleted file mode 100644 index dda9d622..00000000 Binary files a/app/src/debug/assets/library/assets/movie-fatale-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-fighting-with-my-family-2019-poster.jpg b/app/src/debug/assets/library/assets/movie-fighting-with-my-family-2019-poster.jpg deleted file mode 100644 index 169c466d..00000000 Binary files a/app/src/debug/assets/library/assets/movie-fighting-with-my-family-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg b/app/src/debug/assets/library/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg deleted file mode 100644 index 93ba0d71..00000000 Binary files a/app/src/debug/assets/library/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-free-state-of-jones-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-free-state-of-jones-2016-poster.jpg deleted file mode 100644 index e69af7c0..00000000 Binary files a/app/src/debug/assets/library/assets/movie-free-state-of-jones-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-from-up-on-poppy-hill-2011-poster.jpg b/app/src/debug/assets/library/assets/movie-from-up-on-poppy-hill-2011-poster.jpg deleted file mode 100644 index 264076e2..00000000 Binary files a/app/src/debug/assets/library/assets/movie-from-up-on-poppy-hill-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-ghost-in-the-shell-1995-poster.jpg b/app/src/debug/assets/library/assets/movie-ghost-in-the-shell-1995-poster.jpg deleted file mode 100644 index 58d221ba..00000000 Binary files a/app/src/debug/assets/library/assets/movie-ghost-in-the-shell-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg b/app/src/debug/assets/library/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg deleted file mode 100644 index ae262147..00000000 Binary files a/app/src/debug/assets/library/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-gosford-park-2001-poster.jpg b/app/src/debug/assets/library/assets/movie-gosford-park-2001-poster.jpg deleted file mode 100644 index 6a6aa651..00000000 Binary files a/app/src/debug/assets/library/assets/movie-gosford-park-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-grave-of-the-fireflies-1988-poster.jpg b/app/src/debug/assets/library/assets/movie-grave-of-the-fireflies-1988-poster.jpg deleted file mode 100644 index 1b71996a..00000000 Binary files a/app/src/debug/assets/library/assets/movie-grave-of-the-fireflies-1988-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-howl-s-moving-castle-2004-poster.jpg b/app/src/debug/assets/library/assets/movie-howl-s-moving-castle-2004-poster.jpg deleted file mode 100644 index 042a6c80..00000000 Binary files a/app/src/debug/assets/library/assets/movie-howl-s-moving-castle-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-in-the-heights-2021-poster.jpg b/app/src/debug/assets/library/assets/movie-in-the-heights-2021-poster.jpg deleted file mode 100644 index e05bfdb0..00000000 Binary files a/app/src/debug/assets/library/assets/movie-in-the-heights-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-jennifer-s-body-2009-poster.jpg b/app/src/debug/assets/library/assets/movie-jennifer-s-body-2009-poster.jpg deleted file mode 100644 index 41cc1eb5..00000000 Binary files a/app/src/debug/assets/library/assets/movie-jennifer-s-body-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-kiki-s-delivery-service-1989-poster.jpg b/app/src/debug/assets/library/assets/movie-kiki-s-delivery-service-1989-poster.jpg deleted file mode 100644 index 80a59b1e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-kiki-s-delivery-service-1989-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg b/app/src/debug/assets/library/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg deleted file mode 100644 index 4c78fee9..00000000 Binary files a/app/src/debug/assets/library/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-locke-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-locke-2014-poster.jpg deleted file mode 100644 index 94ebe97d..00000000 Binary files a/app/src/debug/assets/library/assets/movie-locke-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-look-who-s-back-2015-poster.jpg b/app/src/debug/assets/library/assets/movie-look-who-s-back-2015-poster.jpg deleted file mode 100644 index 423043cf..00000000 Binary files a/app/src/debug/assets/library/assets/movie-look-who-s-back-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-memories-1995-poster.jpg b/app/src/debug/assets/library/assets/movie-memories-1995-poster.jpg deleted file mode 100644 index f3725b42..00000000 Binary files a/app/src/debug/assets/library/assets/movie-memories-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-mercy-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-mercy-2026-poster.jpg deleted file mode 100644 index cfc0d812..00000000 Binary files a/app/src/debug/assets/library/assets/movie-mercy-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg b/app/src/debug/assets/library/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg deleted file mode 100644 index c6fa6a26..00000000 Binary files a/app/src/debug/assets/library/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-normal-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-normal-2026-poster.jpg deleted file mode 100644 index f4c89db7..00000000 Binary files a/app/src/debug/assets/library/assets/movie-normal-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-outlaw-king-2018-poster.jpg b/app/src/debug/assets/library/assets/movie-outlaw-king-2018-poster.jpg deleted file mode 100644 index d6760468..00000000 Binary files a/app/src/debug/assets/library/assets/movie-outlaw-king-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-panic-room-2002-poster.jpg b/app/src/debug/assets/library/assets/movie-panic-room-2002-poster.jpg deleted file mode 100644 index 0b2a0d30..00000000 Binary files a/app/src/debug/assets/library/assets/movie-panic-room-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg deleted file mode 100644 index a4814472..00000000 Binary files a/app/src/debug/assets/library/assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-ponyo-2008-poster.jpg b/app/src/debug/assets/library/assets/movie-ponyo-2008-poster.jpg deleted file mode 100644 index f548e831..00000000 Binary files a/app/src/debug/assets/library/assets/movie-ponyo-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-porco-rosso-1992-poster.jpg b/app/src/debug/assets/library/assets/movie-porco-rosso-1992-poster.jpg deleted file mode 100644 index 4a6a2bd1..00000000 Binary files a/app/src/debug/assets/library/assets/movie-porco-rosso-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-princess-mononoke-1997-poster.jpg b/app/src/debug/assets/library/assets/movie-princess-mononoke-1997-poster.jpg deleted file mode 100644 index 21dd9359..00000000 Binary files a/app/src/debug/assets/library/assets/movie-princess-mononoke-1997-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-prisoners-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-prisoners-2013-poster.jpg deleted file mode 100644 index 411b149b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-prisoners-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-racing-stripes-2005-poster.jpg b/app/src/debug/assets/library/assets/movie-racing-stripes-2005-poster.jpg deleted file mode 100644 index cc2ef2ce..00000000 Binary files a/app/src/debug/assets/library/assets/movie-racing-stripes-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-reincarnated-2012-poster.jpg b/app/src/debug/assets/library/assets/movie-reincarnated-2012-poster.jpg deleted file mode 100644 index e9ab449c..00000000 Binary files a/app/src/debug/assets/library/assets/movie-reincarnated-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-requiem-for-a-dream-2000-poster.jpg b/app/src/debug/assets/library/assets/movie-requiem-for-a-dream-2000-poster.jpg deleted file mode 100644 index 6f89abc3..00000000 Binary files a/app/src/debug/assets/library/assets/movie-requiem-for-a-dream-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg b/app/src/debug/assets/library/assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg deleted file mode 100644 index c79e70a1..00000000 Binary files a/app/src/debug/assets/library/assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-secondhand-lions-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-secondhand-lions-2003-poster.jpg deleted file mode 100644 index 33408fa0..00000000 Binary files a/app/src/debug/assets/library/assets/movie-secondhand-lions-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-self-reliance-2024-poster.jpg b/app/src/debug/assets/library/assets/movie-self-reliance-2024-poster.jpg deleted file mode 100644 index 0f6017ed..00000000 Binary files a/app/src/debug/assets/library/assets/movie-self-reliance-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-serpico-1973-poster.jpg b/app/src/debug/assets/library/assets/movie-serpico-1973-poster.jpg deleted file mode 100644 index 8ddd6e52..00000000 Binary files a/app/src/debug/assets/library/assets/movie-serpico-1973-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-shaft-2000-poster.jpg b/app/src/debug/assets/library/assets/movie-shaft-2000-poster.jpg deleted file mode 100644 index 6b2b79ce..00000000 Binary files a/app/src/debug/assets/library/assets/movie-shaft-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg deleted file mode 100644 index f200a28d..00000000 Binary files a/app/src/debug/assets/library/assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg deleted file mode 100644 index 428bb38b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-sleeping-with-other-people-2015-poster.jpg b/app/src/debug/assets/library/assets/movie-sleeping-with-other-people-2015-poster.jpg deleted file mode 100644 index a5ffff1f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-sleeping-with-other-people-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-snowpiercer-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-snowpiercer-2013-poster.jpg deleted file mode 100644 index be1873fd..00000000 Binary files a/app/src/debug/assets/library/assets/movie-snowpiercer-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-spider-man-brand-new-day-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-spider-man-brand-new-day-2026-poster.jpg deleted file mode 100644 index 51fa155f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-spider-man-brand-new-day-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-spider-man-far-from-home-2019-poster.jpg b/app/src/debug/assets/library/assets/movie-spider-man-far-from-home-2019-poster.jpg deleted file mode 100644 index 28521997..00000000 Binary files a/app/src/debug/assets/library/assets/movie-spider-man-far-from-home-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-spider-man-homecoming-2017-poster.jpg b/app/src/debug/assets/library/assets/movie-spider-man-homecoming-2017-poster.jpg deleted file mode 100644 index 97fc9c05..00000000 Binary files a/app/src/debug/assets/library/assets/movie-spider-man-homecoming-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-spider-man-no-way-home-2021-poster.jpg b/app/src/debug/assets/library/assets/movie-spider-man-no-way-home-2021-poster.jpg deleted file mode 100644 index bc92a4cd..00000000 Binary files a/app/src/debug/assets/library/assets/movie-spider-man-no-way-home-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-spirited-away-2001-poster.jpg b/app/src/debug/assets/library/assets/movie-spirited-away-2001-poster.jpg deleted file mode 100644 index 5a79da62..00000000 Binary files a/app/src/debug/assets/library/assets/movie-spirited-away-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg deleted file mode 100644 index e85ddb90..00000000 Binary files a/app/src/debug/assets/library/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-taxi-driver-1976-poster.jpg b/app/src/debug/assets/library/assets/movie-taxi-driver-1976-poster.jpg deleted file mode 100644 index c410b981..00000000 Binary files a/app/src/debug/assets/library/assets/movie-taxi-driver-1976-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-alto-knights-2025-poster.jpg b/app/src/debug/assets/library/assets/movie-the-alto-knights-2025-poster.jpg deleted file mode 100644 index bb009cbf..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-alto-knights-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-amazing-spider-man-2-2014-poster.jpg b/app/src/debug/assets/library/assets/movie-the-amazing-spider-man-2-2014-poster.jpg deleted file mode 100644 index 4de2711f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-amazing-spider-man-2-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-amazing-spider-man-2012-poster.jpg b/app/src/debug/assets/library/assets/movie-the-amazing-spider-man-2012-poster.jpg deleted file mode 100644 index c1aec876..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-amazing-spider-man-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-animatrix-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-the-animatrix-2003-poster.jpg deleted file mode 100644 index 65553d47..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-animatrix-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-avengers-2012-poster.jpg b/app/src/debug/assets/library/assets/movie-the-avengers-2012-poster.jpg deleted file mode 100644 index be87067c..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-avengers-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-boy-and-the-heron-2023-poster.jpg b/app/src/debug/assets/library/assets/movie-the-boy-and-the-heron-2023-poster.jpg deleted file mode 100644 index 094df64e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-boy-and-the-heron-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-breakfast-club-1985-poster.jpg b/app/src/debug/assets/library/assets/movie-the-breakfast-club-1985-poster.jpg deleted file mode 100644 index d1823120..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-breakfast-club-1985-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-dry-2021-poster.jpg b/app/src/debug/assets/library/assets/movie-the-dry-2021-poster.jpg deleted file mode 100644 index a0379f2f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-dry-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg b/app/src/debug/assets/library/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg deleted file mode 100644 index ded6cd2a..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-godfather-1972-poster.jpg b/app/src/debug/assets/library/assets/movie-the-godfather-1972-poster.jpg deleted file mode 100644 index 94111d9b..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-godfather-1972-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-godfather-part-ii-1974-poster.jpg b/app/src/debug/assets/library/assets/movie-the-godfather-part-ii-1974-poster.jpg deleted file mode 100644 index 1328c99e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-godfather-part-ii-1974-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-godfather-part-iii-1990-poster.jpg b/app/src/debug/assets/library/assets/movie-the-godfather-part-iii-1990-poster.jpg deleted file mode 100644 index abbe5e99..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-godfather-part-iii-1990-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-great-gatsby-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-the-great-gatsby-2013-poster.jpg deleted file mode 100644 index e949fb3e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-great-gatsby-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-hours-2002-poster.jpg b/app/src/debug/assets/library/assets/movie-the-hours-2002-poster.jpg deleted file mode 100644 index f87e919f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-hours-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg b/app/src/debug/assets/library/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg deleted file mode 100644 index 83464d4c..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-last-of-the-mohicans-1992-poster.jpg b/app/src/debug/assets/library/assets/movie-the-last-of-the-mohicans-1992-poster.jpg deleted file mode 100644 index 950e9ba1..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-last-of-the-mohicans-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-last-samurai-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-the-last-samurai-2003-poster.jpg deleted file mode 100644 index 0d58440c..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-last-samurai-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-lighthouse-2019-poster.jpg b/app/src/debug/assets/library/assets/movie-the-lighthouse-2019-poster.jpg deleted file mode 100644 index 15f26533..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-lighthouse-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-matrix-1999-poster.jpg b/app/src/debug/assets/library/assets/movie-the-matrix-1999-poster.jpg deleted file mode 100644 index c292dce5..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-matrix-1999-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-matrix-reloaded-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-the-matrix-reloaded-2003-poster.jpg deleted file mode 100644 index ab11f50f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-matrix-reloaded-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-matrix-resurrections-2021-poster.jpg b/app/src/debug/assets/library/assets/movie-the-matrix-resurrections-2021-poster.jpg deleted file mode 100644 index 4477ef1e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-matrix-resurrections-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-matrix-revolutions-2003-poster.jpg b/app/src/debug/assets/library/assets/movie-the-matrix-revolutions-2003-poster.jpg deleted file mode 100644 index 7236cc4e..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-matrix-revolutions-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-patriot-2000-poster.jpg b/app/src/debug/assets/library/assets/movie-the-patriot-2000-poster.jpg deleted file mode 100644 index c8582133..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-patriot-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg b/app/src/debug/assets/library/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg deleted file mode 100644 index fe50c2f8..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-substance-2024-poster.jpg b/app/src/debug/assets/library/assets/movie-the-substance-2024-poster.jpg deleted file mode 100644 index 72accfed..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-substance-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg deleted file mode 100644 index 882621cb..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-uprising-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-the-uprising-2026-poster.jpg deleted file mode 100644 index 21976395..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-uprising-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-wild-robot-2024-poster.jpg b/app/src/debug/assets/library/assets/movie-the-wild-robot-2024-poster.jpg deleted file mode 100644 index 0894754d..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-wild-robot-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-wind-rises-2013-poster.jpg b/app/src/debug/assets/library/assets/movie-the-wind-rises-2013-poster.jpg deleted file mode 100644 index b7688c77..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-wind-rises-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg b/app/src/debug/assets/library/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg deleted file mode 100644 index 6db33db1..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg b/app/src/debug/assets/library/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg deleted file mode 100644 index 8a54bc7f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-thelma-louise-1991-poster.jpg b/app/src/debug/assets/library/assets/movie-thelma-louise-1991-poster.jpg deleted file mode 100644 index 5a4ff0ab..00000000 Binary files a/app/src/debug/assets/library/assets/movie-thelma-louise-1991-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-triangle-of-sadness-2022-poster.jpg b/app/src/debug/assets/library/assets/movie-triangle-of-sadness-2022-poster.jpg deleted file mode 100644 index aee092c6..00000000 Binary files a/app/src/debug/assets/library/assets/movie-triangle-of-sadness-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-tsotsi-2005-poster.jpg b/app/src/debug/assets/library/assets/movie-tsotsi-2005-poster.jpg deleted file mode 100644 index 362ddd82..00000000 Binary files a/app/src/debug/assets/library/assets/movie-tsotsi-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg b/app/src/debug/assets/library/assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg deleted file mode 100644 index e32685f3..00000000 Binary files a/app/src/debug/assets/library/assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-vice-2018-poster.jpg b/app/src/debug/assets/library/assets/movie-vice-2018-poster.jpg deleted file mode 100644 index 76855126..00000000 Binary files a/app/src/debug/assets/library/assets/movie-vice-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-vicky-cristina-barcelona-2008-poster.jpg b/app/src/debug/assets/library/assets/movie-vicky-cristina-barcelona-2008-poster.jpg deleted file mode 100644 index 1351cad1..00000000 Binary files a/app/src/debug/assets/library/assets/movie-vicky-cristina-barcelona-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-warriors-of-the-wind-1984-poster.jpg b/app/src/debug/assets/library/assets/movie-warriors-of-the-wind-1984-poster.jpg deleted file mode 100644 index 918e424f..00000000 Binary files a/app/src/debug/assets/library/assets/movie-warriors-of-the-wind-1984-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-we-bury-the-dead-2026-poster.jpg b/app/src/debug/assets/library/assets/movie-we-bury-the-dead-2026-poster.jpg deleted file mode 100644 index a0d65904..00000000 Binary files a/app/src/debug/assets/library/assets/movie-we-bury-the-dead-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg b/app/src/debug/assets/library/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg deleted file mode 100644 index 3a123e80..00000000 Binary files a/app/src/debug/assets/library/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-12-monkeys-2015-poster.jpg b/app/src/debug/assets/library/assets/tv-12-monkeys-2015-poster.jpg deleted file mode 100644 index 96e3596e..00000000 Binary files a/app/src/debug/assets/library/assets/tv-12-monkeys-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-56-days-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-56-days-2026-poster.jpg deleted file mode 100644 index 64ca587b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-56-days-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg b/app/src/debug/assets/library/assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg deleted file mode 100644 index 3362da59..00000000 Binary files a/app/src/debug/assets/library/assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-90-day-fiance-the-other-way-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-90-day-fiance-the-other-way-2019-poster.jpg deleted file mode 100644 index b4d256eb..00000000 Binary files a/app/src/debug/assets/library/assets/tv-90-day-fiance-the-other-way-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg deleted file mode 100644 index 79f1351c..00000000 Binary files a/app/src/debug/assets/library/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-absolutely-fabulous-1992-poster.jpg b/app/src/debug/assets/library/assets/tv-absolutely-fabulous-1992-poster.jpg deleted file mode 100644 index afd371a9..00000000 Binary files a/app/src/debug/assets/library/assets/tv-absolutely-fabulous-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-accel-world-2012-poster.jpg b/app/src/debug/assets/library/assets/tv-accel-world-2012-poster.jpg deleted file mode 100644 index 207b1dd1..00000000 Binary files a/app/src/debug/assets/library/assets/tv-accel-world-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-accused-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-accused-2023-poster.jpg deleted file mode 100644 index 74af010a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-accused-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-adults-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-adults-2025-poster.jpg deleted file mode 100644 index 53a7ba33..00000000 Binary files a/app/src/debug/assets/library/assets/tv-adults-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-after-life-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-after-life-2019-poster.jpg deleted file mode 100644 index 4fae4e33..00000000 Binary files a/app/src/debug/assets/library/assets/tv-after-life-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-agatha-christie-s-marple-2004-poster.jpg b/app/src/debug/assets/library/assets/tv-agatha-christie-s-marple-2004-poster.jpg deleted file mode 100644 index ea7dda99..00000000 Binary files a/app/src/debug/assets/library/assets/tv-agatha-christie-s-marple-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-ahsoka-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-ahsoka-2023-poster.jpg deleted file mode 100644 index a29d6cb4..00000000 Binary files a/app/src/debug/assets/library/assets/tv-ahsoka-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-all-american-homecoming-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-all-american-homecoming-2022-poster.jpg deleted file mode 100644 index 09ccfc33..00000000 Binary files a/app/src/debug/assets/library/assets/tv-all-american-homecoming-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-all-creatures-great-and-small-1978-poster.jpg b/app/src/debug/assets/library/assets/tv-all-creatures-great-and-small-1978-poster.jpg deleted file mode 100644 index 5b1666e6..00000000 Binary files a/app/src/debug/assets/library/assets/tv-all-creatures-great-and-small-1978-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-all-creatures-great-small-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-all-creatures-great-small-2020-poster.jpg deleted file mode 100644 index b506f990..00000000 Binary files a/app/src/debug/assets/library/assets/tv-all-creatures-great-small-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-american-crime-story-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-american-crime-story-2016-poster.jpg deleted file mode 100644 index 4086bf17..00000000 Binary files a/app/src/debug/assets/library/assets/tv-american-crime-story-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-american-dad-2005-poster.jpg b/app/src/debug/assets/library/assets/tv-american-dad-2005-poster.jpg deleted file mode 100644 index 1f9e239a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-american-dad-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-american-gods-2017-poster.jpg b/app/src/debug/assets/library/assets/tv-american-gods-2017-poster.jpg deleted file mode 100644 index c02c46da..00000000 Binary files a/app/src/debug/assets/library/assets/tv-american-gods-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-american-housewife-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-american-housewife-2016-poster.jpg deleted file mode 100644 index 5f92bf05..00000000 Binary files a/app/src/debug/assets/library/assets/tv-american-housewife-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-american-primeval-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-american-primeval-2025-poster.jpg deleted file mode 100644 index 7f10e6aa..00000000 Binary files a/app/src/debug/assets/library/assets/tv-american-primeval-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-and-just-like-that-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-and-just-like-that-2021-poster.jpg deleted file mode 100644 index 9904c74b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-and-just-like-that-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-andor-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-andor-2022-poster.jpg deleted file mode 100644 index df5dc498..00000000 Binary files a/app/src/debug/assets/library/assets/tv-andor-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-andromeda-2000-poster.jpg b/app/src/debug/assets/library/assets/tv-andromeda-2000-poster.jpg deleted file mode 100644 index 67b766b0..00000000 Binary files a/app/src/debug/assets/library/assets/tv-andromeda-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-anne-with-an-e-2017-poster.jpg b/app/src/debug/assets/library/assets/tv-anne-with-an-e-2017-poster.jpg deleted file mode 100644 index 55d443b4..00000000 Binary files a/app/src/debug/assets/library/assets/tv-anne-with-an-e-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-aqua-teen-hunger-force-2000-poster.jpg b/app/src/debug/assets/library/assets/tv-aqua-teen-hunger-force-2000-poster.jpg deleted file mode 100644 index e0c97321..00000000 Binary files a/app/src/debug/assets/library/assets/tv-aqua-teen-hunger-force-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg deleted file mode 100644 index 6a8f9a24..00000000 Binary files a/app/src/debug/assets/library/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-arrested-development-2003-poster.jpg b/app/src/debug/assets/library/assets/tv-arrested-development-2003-poster.jpg deleted file mode 100644 index 02b0dd43..00000000 Binary files a/app/src/debug/assets/library/assets/tv-arrested-development-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-arrow-2012-poster.jpg b/app/src/debug/assets/library/assets/tv-arrow-2012-poster.jpg deleted file mode 100644 index adee3bb7..00000000 Binary files a/app/src/debug/assets/library/assets/tv-arrow-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-atlantis-2013-poster.jpg b/app/src/debug/assets/library/assets/tv-atlantis-2013-poster.jpg deleted file mode 100644 index 5cd5f098..00000000 Binary files a/app/src/debug/assets/library/assets/tv-atlantis-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-avatar-seven-havens-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-avatar-seven-havens-2026-poster.jpg deleted file mode 100644 index 61f459c6..00000000 Binary files a/app/src/debug/assets/library/assets/tv-avatar-seven-havens-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-avatar-the-last-airbender-2005-poster.jpg b/app/src/debug/assets/library/assets/tv-avatar-the-last-airbender-2005-poster.jpg deleted file mode 100644 index 6f99263a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-avatar-the-last-airbender-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-avatar-the-last-airbender-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-avatar-the-last-airbender-2024-poster.jpg deleted file mode 100644 index 9d46c2ff..00000000 Binary files a/app/src/debug/assets/library/assets/tv-avatar-the-last-airbender-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-awkward-2011-poster.jpg b/app/src/debug/assets/library/assets/tv-awkward-2011-poster.jpg deleted file mode 100644 index 8da6e8e2..00000000 Binary files a/app/src/debug/assets/library/assets/tv-awkward-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-b-b-vol-liefde-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-b-b-vol-liefde-2021-poster.jpg deleted file mode 100644 index a12ac33e..00000000 Binary files a/app/src/debug/assets/library/assets/tv-b-b-vol-liefde-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-back-to-life-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-back-to-life-2019-poster.jpg deleted file mode 100644 index b00730a8..00000000 Binary files a/app/src/debug/assets/library/assets/tv-back-to-life-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-ballers-2015-poster.jpg b/app/src/debug/assets/library/assets/tv-ballers-2015-poster.jpg deleted file mode 100644 index f1c18f69..00000000 Binary files a/app/src/debug/assets/library/assets/tv-ballers-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-baretta-1975-poster.jpg b/app/src/debug/assets/library/assets/tv-baretta-1975-poster.jpg deleted file mode 100644 index ceef0c9d..00000000 Binary files a/app/src/debug/assets/library/assets/tv-baretta-1975-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-based-on-a-true-story-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-based-on-a-true-story-2023-poster.jpg deleted file mode 100644 index 386d3d90..00000000 Binary files a/app/src/debug/assets/library/assets/tv-based-on-a-true-story-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg b/app/src/debug/assets/library/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg deleted file mode 100644 index a864f50c..00000000 Binary files a/app/src/debug/assets/library/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-batwoman-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-batwoman-2019-poster.jpg deleted file mode 100644 index 7ca86592..00000000 Binary files a/app/src/debug/assets/library/assets/tv-batwoman-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-beat-bobby-flay-2013-poster.jpg b/app/src/debug/assets/library/assets/tv-beat-bobby-flay-2013-poster.jpg deleted file mode 100644 index fe11122d..00000000 Binary files a/app/src/debug/assets/library/assets/tv-beat-bobby-flay-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-benson-1979-poster.jpg b/app/src/debug/assets/library/assets/tv-benson-1979-poster.jpg deleted file mode 100644 index fe264c76..00000000 Binary files a/app/src/debug/assets/library/assets/tv-benson-1979-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-big-boys-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-big-boys-2022-poster.jpg deleted file mode 100644 index 93e1e86a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-big-boys-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-big-city-greens-2018-poster.jpg b/app/src/debug/assets/library/assets/tv-big-city-greens-2018-poster.jpg deleted file mode 100644 index ef024946..00000000 Binary files a/app/src/debug/assets/library/assets/tv-big-city-greens-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-big-mouth-2017-poster.jpg b/app/src/debug/assets/library/assets/tv-big-mouth-2017-poster.jpg deleted file mode 100644 index 4f535fec..00000000 Binary files a/app/src/debug/assets/library/assets/tv-big-mouth-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-black-ish-2014-poster.jpg b/app/src/debug/assets/library/assets/tv-black-ish-2014-poster.jpg deleted file mode 100644 index 97c885ad..00000000 Binary files a/app/src/debug/assets/library/assets/tv-black-ish-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-black-lightning-2018-poster.jpg b/app/src/debug/assets/library/assets/tv-black-lightning-2018-poster.jpg deleted file mode 100644 index 733f5cbc..00000000 Binary files a/app/src/debug/assets/library/assets/tv-black-lightning-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-black-rabbit-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-black-rabbit-2025-poster.jpg deleted file mode 100644 index 3ff9aeed..00000000 Binary files a/app/src/debug/assets/library/assets/tv-black-rabbit-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-bleach-2004-poster.jpg b/app/src/debug/assets/library/assets/tv-bleach-2004-poster.jpg deleted file mode 100644 index b233e033..00000000 Binary files a/app/src/debug/assets/library/assets/tv-bleach-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-blue-planet-ii-2017-poster.jpg b/app/src/debug/assets/library/assets/tv-blue-planet-ii-2017-poster.jpg deleted file mode 100644 index 5e193ab2..00000000 Binary files a/app/src/debug/assets/library/assets/tv-blue-planet-ii-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-brassic-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-brassic-2019-poster.jpg deleted file mode 100644 index 73b77df2..00000000 Binary files a/app/src/debug/assets/library/assets/tv-brassic-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-breaking-bad-2008-poster.jpg b/app/src/debug/assets/library/assets/tv-breaking-bad-2008-poster.jpg deleted file mode 100644 index c4f7217c..00000000 Binary files a/app/src/debug/assets/library/assets/tv-breaking-bad-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-breeders-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-breeders-2020-poster.jpg deleted file mode 100644 index 49c61208..00000000 Binary files a/app/src/debug/assets/library/assets/tv-breeders-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-californication-2007-poster.jpg b/app/src/debug/assets/library/assets/tv-californication-2007-poster.jpg deleted file mode 100644 index 38428e76..00000000 Binary files a/app/src/debug/assets/library/assets/tv-californication-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-call-the-midwife-2012-poster.jpg b/app/src/debug/assets/library/assets/tv-call-the-midwife-2012-poster.jpg deleted file mode 100644 index 466a1a7b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-call-the-midwife-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-chainsaw-man-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-chainsaw-man-2022-poster.jpg deleted file mode 100644 index a3779772..00000000 Binary files a/app/src/debug/assets/library/assets/tv-chainsaw-man-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-chernobyl-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-chernobyl-2019-poster.jpg deleted file mode 100644 index 7664f4b0..00000000 Binary files a/app/src/debug/assets/library/assets/tv-chernobyl-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-community-2009-poster.jpg b/app/src/debug/assets/library/assets/tv-community-2009-poster.jpg deleted file mode 100644 index e75072ae..00000000 Binary files a/app/src/debug/assets/library/assets/tv-community-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-cyberpunk-edgerunners-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-cyberpunk-edgerunners-2022-poster.jpg deleted file mode 100644 index 8d338532..00000000 Binary files a/app/src/debug/assets/library/assets/tv-cyberpunk-edgerunners-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-dan-da-dan-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-dan-da-dan-2024-poster.jpg deleted file mode 100644 index f42cec16..00000000 Binary files a/app/src/debug/assets/library/assets/tv-dan-da-dan-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-dead-end-job-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-dead-end-job-2026-poster.jpg deleted file mode 100644 index f5546d88..00000000 Binary files a/app/src/debug/assets/library/assets/tv-dead-end-job-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-derry-girls-2018-poster.jpg b/app/src/debug/assets/library/assets/tv-derry-girls-2018-poster.jpg deleted file mode 100644 index 56a8cd79..00000000 Binary files a/app/src/debug/assets/library/assets/tv-derry-girls-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-desperate-housewives-2004-poster.jpg b/app/src/debug/assets/library/assets/tv-desperate-housewives-2004-poster.jpg deleted file mode 100644 index 0c4c38bc..00000000 Binary files a/app/src/debug/assets/library/assets/tv-desperate-housewives-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-dopesick-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-dopesick-2021-poster.jpg deleted file mode 100644 index ac0b5900..00000000 Binary files a/app/src/debug/assets/library/assets/tv-dopesick-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-dr-stone-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-dr-stone-2019-poster.jpg deleted file mode 100644 index 0783ed8c..00000000 Binary files a/app/src/debug/assets/library/assets/tv-dr-stone-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-dune-prophecy-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-dune-prophecy-2024-poster.jpg deleted file mode 100644 index eef2a4f3..00000000 Binary files a/app/src/debug/assets/library/assets/tv-dune-prophecy-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-east-of-eden-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-east-of-eden-2026-poster.jpg deleted file mode 100644 index 6f06f2c9..00000000 Binary files a/app/src/debug/assets/library/assets/tv-east-of-eden-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-etoile-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-etoile-2025-poster.jpg deleted file mode 100644 index f078bc89..00000000 Binary files a/app/src/debug/assets/library/assets/tv-etoile-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-fallout-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-fallout-2024-poster.jpg deleted file mode 100644 index 5e87a07d..00000000 Binary files a/app/src/debug/assets/library/assets/tv-fallout-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-fawlty-towers-1975-poster.jpg b/app/src/debug/assets/library/assets/tv-fawlty-towers-1975-poster.jpg deleted file mode 100644 index 53b406da..00000000 Binary files a/app/src/debug/assets/library/assets/tv-fawlty-towers-1975-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-firefly-2002-poster.jpg b/app/src/debug/assets/library/assets/tv-firefly-2002-poster.jpg deleted file mode 100644 index 9ad599b3..00000000 Binary files a/app/src/debug/assets/library/assets/tv-firefly-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-fleabag-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-fleabag-2016-poster.jpg deleted file mode 100644 index bd3ccea5..00000000 Binary files a/app/src/debug/assets/library/assets/tv-fleabag-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg deleted file mode 100644 index cb94104e..00000000 Binary files a/app/src/debug/assets/library/assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg b/app/src/debug/assets/library/assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg deleted file mode 100644 index 935e4944..00000000 Binary files a/app/src/debug/assets/library/assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-gilmore-girls-2000-poster.jpg b/app/src/debug/assets/library/assets/tv-gilmore-girls-2000-poster.jpg deleted file mode 100644 index 23b668cb..00000000 Binary files a/app/src/debug/assets/library/assets/tv-gilmore-girls-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-ginny-georgia-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-ginny-georgia-2021-poster.jpg deleted file mode 100644 index 1fca7bf6..00000000 Binary files a/app/src/debug/assets/library/assets/tv-ginny-georgia-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-gravity-falls-2012-poster.jpg b/app/src/debug/assets/library/assets/tv-gravity-falls-2012-poster.jpg deleted file mode 100644 index 41c051e0..00000000 Binary files a/app/src/debug/assets/library/assets/tv-gravity-falls-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-hellcats-2010-poster.jpg b/app/src/debug/assets/library/assets/tv-hellcats-2010-poster.jpg deleted file mode 100644 index 2427108b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-hellcats-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-high-potential-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-high-potential-2024-poster.jpg deleted file mode 100644 index 00a07ca7..00000000 Binary files a/app/src/debug/assets/library/assets/tv-high-potential-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-how-to-change-your-mind-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-how-to-change-your-mind-2022-poster.jpg deleted file mode 100644 index e80c7999..00000000 Binary files a/app/src/debug/assets/library/assets/tv-how-to-change-your-mind-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg deleted file mode 100644 index eb5fc487..00000000 Binary files a/app/src/debug/assets/library/assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-interrogation-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-interrogation-2020-poster.jpg deleted file mode 100644 index f3a21b66..00000000 Binary files a/app/src/debug/assets/library/assets/tv-interrogation-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-invincible-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-invincible-2021-poster.jpg deleted file mode 100644 index c278e37e..00000000 Binary files a/app/src/debug/assets/library/assets/tv-invincible-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg b/app/src/debug/assets/library/assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg deleted file mode 100644 index 652923ee..00000000 Binary files a/app/src/debug/assets/library/assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-jujutsu-kaisen-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-jujutsu-kaisen-2020-poster.jpg deleted file mode 100644 index 45f5ff22..00000000 Binary files a/app/src/debug/assets/library/assets/tv-jujutsu-kaisen-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-kaiju-no-8-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-kaiju-no-8-2024-poster.jpg deleted file mode 100644 index 062496fa..00000000 Binary files a/app/src/debug/assets/library/assets/tv-kaiju-no-8-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-landman-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-landman-2024-poster.jpg deleted file mode 100644 index 3d348798..00000000 Binary files a/app/src/debug/assets/library/assets/tv-landman-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-last-samurai-standing-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-last-samurai-standing-2025-poster.jpg deleted file mode 100644 index a9789909..00000000 Binary files a/app/src/debug/assets/library/assets/tv-last-samurai-standing-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-lord-of-the-flies-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-lord-of-the-flies-2026-poster.jpg deleted file mode 100644 index b1229ed8..00000000 Binary files a/app/src/debug/assets/library/assets/tv-lord-of-the-flies-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-love-death-robots-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-love-death-robots-2019-poster.jpg deleted file mode 100644 index 08449aa9..00000000 Binary files a/app/src/debug/assets/library/assets/tv-love-death-robots-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-love-is-blind-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-love-is-blind-2020-poster.jpg deleted file mode 100644 index 62ccf357..00000000 Binary files a/app/src/debug/assets/library/assets/tv-love-is-blind-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-maniac-2018-poster.jpg b/app/src/debug/assets/library/assets/tv-maniac-2018-poster.jpg deleted file mode 100644 index a1985217..00000000 Binary files a/app/src/debug/assets/library/assets/tv-maniac-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg deleted file mode 100644 index abe1b2e2..00000000 Binary files a/app/src/debug/assets/library/assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-minx-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-minx-2022-poster.jpg deleted file mode 100644 index a8797fe5..00000000 Binary files a/app/src/debug/assets/library/assets/tv-minx-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-mussolini-son-of-the-century-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-mussolini-son-of-the-century-2024-poster.jpg deleted file mode 100644 index 925d5c49..00000000 Binary files a/app/src/debug/assets/library/assets/tv-mussolini-son-of-the-century-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-my-hero-academia-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-my-hero-academia-2016-poster.jpg deleted file mode 100644 index cf571b93..00000000 Binary files a/app/src/debug/assets/library/assets/tv-my-hero-academia-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-neon-genesis-evangelion-1995-poster.jpg b/app/src/debug/assets/library/assets/tv-neon-genesis-evangelion-1995-poster.jpg deleted file mode 100644 index f86cd698..00000000 Binary files a/app/src/debug/assets/library/assets/tv-neon-genesis-evangelion-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-norsemen-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-norsemen-2016-poster.jpg deleted file mode 100644 index e7206b88..00000000 Binary files a/app/src/debug/assets/library/assets/tv-norsemen-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-offspring-2010-poster.jpg b/app/src/debug/assets/library/assets/tv-offspring-2010-poster.jpg deleted file mode 100644 index ae305043..00000000 Binary files a/app/src/debug/assets/library/assets/tv-offspring-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-one-piece-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-one-piece-2023-poster.jpg deleted file mode 100644 index 286e6260..00000000 Binary files a/app/src/debug/assets/library/assets/tv-one-piece-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-only-murders-in-the-building-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-only-murders-in-the-building-2021-poster.jpg deleted file mode 100644 index a6b147a4..00000000 Binary files a/app/src/debug/assets/library/assets/tv-only-murders-in-the-building-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-outlander-blood-of-my-blood-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-outlander-blood-of-my-blood-2025-poster.jpg deleted file mode 100644 index 930943b0..00000000 Binary files a/app/src/debug/assets/library/assets/tv-outlander-blood-of-my-blood-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-over-the-garden-wall-2014-poster.jpg b/app/src/debug/assets/library/assets/tv-over-the-garden-wall-2014-poster.jpg deleted file mode 100644 index 34b1ed84..00000000 Binary files a/app/src/debug/assets/library/assets/tv-over-the-garden-wall-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-peaky-blinders-2013-poster.jpg b/app/src/debug/assets/library/assets/tv-peaky-blinders-2013-poster.jpg deleted file mode 100644 index 25ceea88..00000000 Binary files a/app/src/debug/assets/library/assets/tv-peaky-blinders-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-pluribus-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-pluribus-2025-poster.jpg deleted file mode 100644 index 1ea4711b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-pluribus-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-record-of-ragnarok-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-record-of-ragnarok-2021-poster.jpg deleted file mode 100644 index 97d481d6..00000000 Binary files a/app/src/debug/assets/library/assets/tv-record-of-ragnarok-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-rick-and-morty-2013-poster.jpg b/app/src/debug/assets/library/assets/tv-rick-and-morty-2013-poster.jpg deleted file mode 100644 index b95c4890..00000000 Binary files a/app/src/debug/assets/library/assets/tv-rick-and-morty-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-ripley-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-ripley-2024-poster.jpg deleted file mode 100644 index de0b32dd..00000000 Binary files a/app/src/debug/assets/library/assets/tv-ripley-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-rivals-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-rivals-2024-poster.jpg deleted file mode 100644 index 296ac67a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-rivals-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-rooster-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-rooster-2026-poster.jpg deleted file mode 100644 index b698f8a6..00000000 Binary files a/app/src/debug/assets/library/assets/tv-rooster-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-sakamoto-days-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-sakamoto-days-2025-poster.jpg deleted file mode 100644 index ed9358b3..00000000 Binary files a/app/src/debug/assets/library/assets/tv-sakamoto-days-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-say-nothing-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-say-nothing-2024-poster.jpg deleted file mode 100644 index cf50fc09..00000000 Binary files a/app/src/debug/assets/library/assets/tv-say-nothing-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-secret-level-2024-poster.jpg b/app/src/debug/assets/library/assets/tv-secret-level-2024-poster.jpg deleted file mode 100644 index 1b6e67f6..00000000 Binary files a/app/src/debug/assets/library/assets/tv-secret-level-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-sentenced-to-be-a-hero-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-sentenced-to-be-a-hero-2025-poster.jpg deleted file mode 100644 index 2fb1c815..00000000 Binary files a/app/src/debug/assets/library/assets/tv-sentenced-to-be-a-hero-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-severance-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-severance-2022-poster.jpg deleted file mode 100644 index 5a29d6f9..00000000 Binary files a/app/src/debug/assets/library/assets/tv-severance-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-sex-and-the-city-1998-poster.jpg b/app/src/debug/assets/library/assets/tv-sex-and-the-city-1998-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/library/assets/tv-sex-and-the-city-1998-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-silo-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-silo-2023-poster.jpg deleted file mode 100644 index b493bc7b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-silo-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-six-feet-under-2001-poster.jpg b/app/src/debug/assets/library/assets/tv-six-feet-under-2001-poster.jpg deleted file mode 100644 index 559d6bd1..00000000 Binary files a/app/src/debug/assets/library/assets/tv-six-feet-under-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-skymed-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-skymed-2022-poster.jpg deleted file mode 100644 index 2540dad7..00000000 Binary files a/app/src/debug/assets/library/assets/tv-skymed-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-so-you-think-you-can-dance-2008-poster.jpg b/app/src/debug/assets/library/assets/tv-so-you-think-you-can-dance-2008-poster.jpg deleted file mode 100644 index a2e91496..00000000 Binary files a/app/src/debug/assets/library/assets/tv-so-you-think-you-can-dance-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-south-park-1997-poster.jpg b/app/src/debug/assets/library/assets/tv-south-park-1997-poster.jpg deleted file mode 100644 index d8568d54..00000000 Binary files a/app/src/debug/assets/library/assets/tv-south-park-1997-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-spy-x-family-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-spy-x-family-2022-poster.jpg deleted file mode 100644 index fc326930..00000000 Binary files a/app/src/debug/assets/library/assets/tv-spy-x-family-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-100-2014-poster.jpg b/app/src/debug/assets/library/assets/tv-the-100-2014-poster.jpg deleted file mode 100644 index 4d254cb0..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-100-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-abbott-and-costello-show-1952-poster.jpg b/app/src/debug/assets/library/assets/tv-the-abbott-and-costello-show-1952-poster.jpg deleted file mode 100644 index 7d6768ea..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-abbott-and-costello-show-1952-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-afterparty-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-the-afterparty-2022-poster.jpg deleted file mode 100644 index 9beacde2..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-afterparty-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-baby-sitters-club-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-the-baby-sitters-club-2020-poster.jpg deleted file mode 100644 index 7e4e7be2..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-baby-sitters-club-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-bear-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-the-bear-2022-poster.jpg deleted file mode 100644 index bbf0ab28..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-bear-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-beast-in-me-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-the-beast-in-me-2025-poster.jpg deleted file mode 100644 index c78c5cf0..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-beast-in-me-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-bold-type-2017-poster.jpg b/app/src/debug/assets/library/assets/tv-the-bold-type-2017-poster.jpg deleted file mode 100644 index f663eb8b..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-bold-type-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-boys-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-the-boys-2019-poster.jpg deleted file mode 100644 index f67ff16f..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-boys-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-capture-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-the-capture-2019-poster.jpg deleted file mode 100644 index a4039011..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-capture-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-diplomat-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-the-diplomat-2023-poster.jpg deleted file mode 100644 index 2eb48b62..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-diplomat-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-durrells-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-the-durrells-2016-poster.jpg deleted file mode 100644 index d29448bb..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-durrells-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-final-problem-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-the-final-problem-2026-poster.jpg deleted file mode 100644 index f0332826..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-final-problem-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-four-seasons-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-the-four-seasons-2025-poster.jpg deleted file mode 100644 index fd252088..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-four-seasons-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-get-down-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-the-get-down-2016-poster.jpg deleted file mode 100644 index 7bb38abd..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-get-down-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-hunting-wives-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-the-hunting-wives-2025-poster.jpg deleted file mode 100644 index 034273eb..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-hunting-wives-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-last-of-us-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-the-last-of-us-2023-poster.jpg deleted file mode 100644 index 7da462da..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-last-of-us-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-legend-of-korra-2012-poster.jpg b/app/src/debug/assets/library/assets/tv-the-legend-of-korra-2012-poster.jpg deleted file mode 100644 index a7086d5e..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-legend-of-korra-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg deleted file mode 100644 index dc387fcd..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg b/app/src/debug/assets/library/assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg deleted file mode 100644 index 71407f36..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-night-manager-2016-poster.jpg b/app/src/debug/assets/library/assets/tv-the-night-manager-2016-poster.jpg deleted file mode 100644 index 7ab2dbf5..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-night-manager-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-pitt-2025-poster.jpg b/app/src/debug/assets/library/assets/tv-the-pitt-2025-poster.jpg deleted file mode 100644 index ad1ec8ff..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-pitt-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-tatami-galaxy-2010-poster.jpg b/app/src/debug/assets/library/assets/tv-the-tatami-galaxy-2010-poster.jpg deleted file mode 100644 index 5324dc75..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-tatami-galaxy-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-walk-in-2022-poster.jpg b/app/src/debug/assets/library/assets/tv-the-walk-in-2022-poster.jpg deleted file mode 100644 index 715d0c80..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-walk-in-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-white-lotus-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-the-white-lotus-2021-poster.jpg deleted file mode 100644 index 2f23d1fa..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-white-lotus-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-the-witcher-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-the-witcher-2019-poster.jpg deleted file mode 100644 index f5eb8f59..00000000 Binary files a/app/src/debug/assets/library/assets/tv-the-witcher-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-undercover-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-undercover-2019-poster.jpg deleted file mode 100644 index 8611eab4..00000000 Binary files a/app/src/debug/assets/library/assets/tv-undercover-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-upload-2020-poster.jpg b/app/src/debug/assets/library/assets/tv-upload-2020-poster.jpg deleted file mode 100644 index 547d3b7a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-upload-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-vinland-saga-2019-poster.jpg b/app/src/debug/assets/library/assets/tv-vinland-saga-2019-poster.jpg deleted file mode 100644 index 7d0ba17a..00000000 Binary files a/app/src/debug/assets/library/assets/tv-vinland-saga-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-widow-s-bay-2026-poster.jpg b/app/src/debug/assets/library/assets/tv-widow-s-bay-2026-poster.jpg deleted file mode 100644 index 8c02a0d7..00000000 Binary files a/app/src/debug/assets/library/assets/tv-widow-s-bay-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-yellowjackets-2021-poster.jpg b/app/src/debug/assets/library/assets/tv-yellowjackets-2021-poster.jpg deleted file mode 100644 index c7e16a57..00000000 Binary files a/app/src/debug/assets/library/assets/tv-yellowjackets-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/library/assets/tv-yu-yu-hakusho-1992-poster.jpg b/app/src/debug/assets/library/assets/tv-yu-yu-hakusho-1992-poster.jpg deleted file mode 100644 index f5843766..00000000 Binary files a/app/src/debug/assets/library/assets/tv-yu-yu-hakusho-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg deleted file mode 100644 index f5b2f94c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg deleted file mode 100644 index cf571b93..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-announcement-2022-s1e7-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-announcement-2022-s1e7-backdrop.jpg deleted file mode 100644 index a874a0c0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-announcement-2022-s1e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-announcement-2022-s1e7-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-announcement-2022-s1e7-poster.jpg deleted file mode 100644 index df5dc498..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-announcement-2022-s1e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-1-2025-s1e1-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-episode-1-2025-s1e1-backdrop.jpg deleted file mode 100644 index f2519b04..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-1-2025-s1e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-1-2025-s1e1-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-episode-1-2025-s1e1-poster.jpg deleted file mode 100644 index 77febd79..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-1-2025-s1e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-2-2016-s1e2-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-episode-2-2016-s1e2-backdrop.jpg deleted file mode 100644 index 3f8bdeec..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-2-2016-s1e2-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-2-2016-s1e2-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-episode-2-2016-s1e2-poster.jpg deleted file mode 100644 index d29448bb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-episode-2-2016-s1e2-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-erickerhead-2026-s9e6-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-erickerhead-2026-s9e6-backdrop.jpg deleted file mode 100644 index 1c4c0ffc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-erickerhead-2026-s9e6-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-erickerhead-2026-s9e6-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-erickerhead-2026-s9e6-poster.jpg deleted file mode 100644 index b95c4890..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-erickerhead-2026-s9e6-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg deleted file mode 100644 index ffff979b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-girls-interrupted-2007-s1e7-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-girls-interrupted-2007-s1e7-poster.jpg deleted file mode 100644 index 38428e76..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-girls-interrupted-2007-s1e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg deleted file mode 100644 index 57beacc1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-i-heart-ny-2002-s4e18-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-i-heart-ny-2002-s4e18-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-i-heart-ny-2002-s4e18-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-new-day-2021-s1e2-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-new-day-2021-s1e2-backdrop.jpg deleted file mode 100644 index 1f747323..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-new-day-2021-s1e2-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-new-day-2021-s1e2-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-new-day-2021-s1e2-poster.jpg deleted file mode 100644 index 2f23d1fa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-new-day-2021-s1e2-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg deleted file mode 100644 index 9d95ba77..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-no-honeymoon-2020-s1e8-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-no-honeymoon-2020-s1e8-poster.jpg deleted file mode 100644 index 49c61208..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-no-honeymoon-2020-s1e8-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-notapusy-2005-s3e4-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-notapusy-2005-s3e4-backdrop.jpg deleted file mode 100644 index ecb7e8cd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-notapusy-2005-s3e4-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-notapusy-2005-s3e4-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-notapusy-2005-s3e4-poster.jpg deleted file mode 100644 index 02b0dd43..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-notapusy-2005-s3e4-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-one-of-us-2024-s1e7-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-one-of-us-2024-s1e7-backdrop.jpg deleted file mode 100644 index 66bb4d00..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-one-of-us-2024-s1e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-one-of-us-2024-s1e7-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-one-of-us-2024-s1e7-poster.jpg deleted file mode 100644 index 00a07ca7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-one-of-us-2024-s1e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-remembrance-day-2026-s2e1-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-remembrance-day-2026-s2e1-backdrop.jpg deleted file mode 100644 index 23a242da..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-remembrance-day-2026-s2e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-remembrance-day-2026-s2e1-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-remembrance-day-2026-s2e1-poster.jpg deleted file mode 100644 index 930943b0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-remembrance-day-2026-s2e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg deleted file mode 100644 index 57beacc1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-sex-and-the-country-2001-s4e9-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-sex-and-the-country-2001-s4e9-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-sex-and-the-country-2001-s4e9-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg deleted file mode 100644 index cd0aec39..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg deleted file mode 100644 index d8568d54..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg deleted file mode 100644 index b42ba953..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg deleted file mode 100644 index 23b668cb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg deleted file mode 100644 index 84944d39..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg deleted file mode 100644 index 4086bf17..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg deleted file mode 100644 index bc22e11a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-the-uncomfortability-2010-s1e2-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-the-uncomfortability-2010-s1e2-poster.jpg deleted file mode 100644 index ae305043..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-the-uncomfortability-2010-s1e2-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-three-monsters-1992-s1e6-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-three-monsters-1992-s1e6-backdrop.jpg deleted file mode 100644 index ba65ca2d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-three-monsters-1992-s1e6-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-three-monsters-1992-s1e6-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-three-monsters-1992-s1e6-poster.jpg deleted file mode 100644 index f5843766..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-three-monsters-1992-s1e6-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-tomorrow-2024-s3e1-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-tomorrow-2024-s3e1-backdrop.jpg deleted file mode 100644 index 9c9cca57..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-tomorrow-2024-s3e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-tomorrow-2024-s3e1-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-tomorrow-2024-s3e1-poster.jpg deleted file mode 100644 index bbf0ab28..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-tomorrow-2024-s3e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg deleted file mode 100644 index 40ec3001..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg deleted file mode 100644 index 13f0f7b3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg deleted file mode 100644 index f0d6f752..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg deleted file mode 100644 index b9b72894..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-12-years-a-slave-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-12-years-a-slave-2013-backdrop.jpg deleted file mode 100644 index d93357e5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-12-years-a-slave-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-12-years-a-slave-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-12-years-a-slave-2013-poster.jpg deleted file mode 100644 index ff1f55e6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-12-years-a-slave-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-a-bronx-tale-1993-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-a-bronx-tale-1993-backdrop.jpg deleted file mode 100644 index 15c11ff7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-a-bronx-tale-1993-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-a-bronx-tale-1993-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-a-bronx-tale-1993-poster.jpg deleted file mode 100644 index f7444271..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-a-bronx-tale-1993-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg deleted file mode 100644 index b386b136..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-a-silent-voice-the-movie-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-a-silent-voice-the-movie-2016-poster.jpg deleted file mode 100644 index 580b22f4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-a-silent-voice-the-movie-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-absolution-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-absolution-2024-backdrop.jpg deleted file mode 100644 index eb28037c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-absolution-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-absolution-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-absolution-2024-poster.jpg deleted file mode 100644 index 3ce49112..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-absolution-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-across-the-hall-2009-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-across-the-hall-2009-backdrop.jpg deleted file mode 100644 index 6c1ee8ac..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-across-the-hall-2009-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-across-the-hall-2009-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-across-the-hall-2009-poster.jpg deleted file mode 100644 index 52362b4e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-across-the-hall-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-addicted-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-addicted-2014-backdrop.jpg deleted file mode 100644 index 5c016976..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-addicted-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-addicted-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-addicted-2014-poster.jpg deleted file mode 100644 index e9a03c14..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-addicted-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-adulterers-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-adulterers-2016-backdrop.jpg deleted file mode 100644 index 331391ed..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-adulterers-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-adulterers-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-adulterers-2016-poster.jpg deleted file mode 100644 index 8b65745b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-adulterers-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-all-the-money-in-the-world-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-all-the-money-in-the-world-2017-backdrop.jpg deleted file mode 100644 index ac4b828a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-all-the-money-in-the-world-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-all-the-money-in-the-world-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-all-the-money-in-the-world-2017-poster.jpg deleted file mode 100644 index 8beb2f7f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-all-the-money-in-the-world-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-alone-with-her-2006-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-alone-with-her-2006-backdrop.jpg deleted file mode 100644 index ddf8f1dc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-alone-with-her-2006-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-alone-with-her-2006-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-alone-with-her-2006-poster.jpg deleted file mode 100644 index 7ce6fa96..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-alone-with-her-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-american-gangster-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-american-gangster-2007-backdrop.jpg deleted file mode 100644 index cb5c5de7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-american-gangster-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-american-gangster-2007-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-american-gangster-2007-poster.jpg deleted file mode 100644 index 0f41d299..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-american-gangster-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-annie-hall-1977-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-annie-hall-1977-backdrop.jpg deleted file mode 100644 index 16d0b27d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-annie-hall-1977-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-annie-hall-1977-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-annie-hall-1977-poster.jpg deleted file mode 100644 index 9023a691..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-annie-hall-1977-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg deleted file mode 100644 index 2a99846d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg deleted file mode 100644 index 030addf0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-apocalypse-now-1979-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-apocalypse-now-1979-backdrop.jpg deleted file mode 100644 index 93292da3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-apocalypse-now-1979-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-apocalypse-now-1979-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-apocalypse-now-1979-poster.jpg deleted file mode 100644 index 09cd6aa8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-apocalypse-now-1979-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-appleseed-alpha-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-appleseed-alpha-2014-backdrop.jpg deleted file mode 100644 index 82579b86..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-appleseed-alpha-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-appleseed-alpha-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-appleseed-alpha-2014-poster.jpg deleted file mode 100644 index 62bd0afa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-appleseed-alpha-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg deleted file mode 100644 index 6693cb6d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg deleted file mode 100644 index 4550bdaa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-arctic-dogs-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-arctic-dogs-2019-backdrop.jpg deleted file mode 100644 index 1794c99d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-arctic-dogs-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-arctic-dogs-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-arctic-dogs-2019-poster.jpg deleted file mode 100644 index 07552443..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-arctic-dogs-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-arrival-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-arrival-2016-backdrop.jpg deleted file mode 100644 index 605724e7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-arrival-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-arrival-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-arrival-2016-poster.jpg deleted file mode 100644 index fe224a15..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-arrival-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg deleted file mode 100644 index d3b12fe0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg deleted file mode 100644 index f57f1052..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-age-of-ultron-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-age-of-ultron-2015-backdrop.jpg deleted file mode 100644 index f739d620..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-age-of-ultron-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-age-of-ultron-2015-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-age-of-ultron-2015-poster.jpg deleted file mode 100644 index 300840ff..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-age-of-ultron-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg deleted file mode 100644 index 8163931a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg deleted file mode 100644 index b096fff2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-infinity-war-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-infinity-war-2018-backdrop.jpg deleted file mode 100644 index 90dc4672..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-infinity-war-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-infinity-war-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-infinity-war-2018-poster.jpg deleted file mode 100644 index 57df9298..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-avengers-infinity-war-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-awake-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-awake-2007-backdrop.jpg deleted file mode 100644 index 6ddd7f59..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-awake-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-awake-2007-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-awake-2007-poster.jpg deleted file mode 100644 index 32595897..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-awake-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-ninja-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-batman-ninja-2018-backdrop.jpg deleted file mode 100644 index 9b538d9f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-ninja-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-ninja-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-batman-ninja-2018-poster.jpg deleted file mode 100644 index 7f59127b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-ninja-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg deleted file mode 100644 index 9d385801..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg deleted file mode 100644 index ce5623fe..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg deleted file mode 100644 index b4fa63b9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg deleted file mode 100644 index f8115170..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-adam-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-adam-2022-backdrop.jpg deleted file mode 100644 index 3232fb22..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-adam-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-adam-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-adam-2022-poster.jpg deleted file mode 100644 index dde008b5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-adam-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-bear-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-bear-2020-backdrop.jpg deleted file mode 100644 index 4b0b262f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-bear-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-bear-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-bear-2020-poster.jpg deleted file mode 100644 index 61c8860f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-bear-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-2018-backdrop.jpg deleted file mode 100644 index f0b8de8a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-2018-poster.jpg deleted file mode 100644 index 6fdaa3d2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg deleted file mode 100644 index ac7ad532..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-wakanda-forever-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-wakanda-forever-2022-poster.jpg deleted file mode 100644 index d0219a75..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-black-panther-wakanda-forever-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-blame-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-blame-2017-backdrop.jpg deleted file mode 100644 index f39cf5ce..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-blame-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-blame-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-blame-2017-poster.jpg deleted file mode 100644 index 8d50255f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-blame-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg deleted file mode 100644 index f426d0ce..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg deleted file mode 100644 index d3619fbd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-bohemian-rhapsody-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-bohemian-rhapsody-2018-backdrop.jpg deleted file mode 100644 index b6b5a7cd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-bohemian-rhapsody-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-bohemian-rhapsody-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-bohemian-rhapsody-2018-poster.jpg deleted file mode 100644 index 20d2f9ee..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-bohemian-rhapsody-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-braveheart-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-braveheart-1995-backdrop.jpg deleted file mode 100644 index c1c84c59..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-braveheart-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-braveheart-1995-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-braveheart-1995-poster.jpg deleted file mode 100644 index 32fd8d4d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-braveheart-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg deleted file mode 100644 index e49fe1d5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-butterfly-on-a-wheel-2007-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-butterfly-on-a-wheel-2007-poster.jpg deleted file mode 100644 index b54cc5c7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-butterfly-on-a-wheel-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-by-any-means-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-by-any-means-2026-backdrop.jpg deleted file mode 100644 index e536359f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-by-any-means-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-by-any-means-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-by-any-means-2026-poster.jpg deleted file mode 100644 index 92fd601f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-by-any-means-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-civil-war-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-civil-war-2016-backdrop.jpg deleted file mode 100644 index 5700c4e7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-civil-war-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-civil-war-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-civil-war-2016-poster.jpg deleted file mode 100644 index 426142ef..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-civil-war-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg deleted file mode 100644 index 411ceebe..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-first-avenger-2011-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-first-avenger-2011-poster.jpg deleted file mode 100644 index bba96574..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-first-avenger-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg deleted file mode 100644 index 5661fcff..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-winter-soldier-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-winter-soldier-2014-poster.jpg deleted file mode 100644 index c2dc5612..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-captain-america-the-winter-soldier-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-careful-what-you-wish-for-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-careful-what-you-wish-for-2015-backdrop.jpg deleted file mode 100644 index 988e20d5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-careful-what-you-wish-for-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-careful-what-you-wish-for-2015-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-careful-what-you-wish-for-2015-poster.jpg deleted file mode 100644 index b58ce600..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-careful-what-you-wish-for-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-castle-in-the-sky-1986-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-castle-in-the-sky-1986-backdrop.jpg deleted file mode 100644 index f785c549..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-castle-in-the-sky-1986-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-castle-in-the-sky-1986-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-castle-in-the-sky-1986-poster.jpg deleted file mode 100644 index a6a99fee..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-castle-in-the-sky-1986-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-chicago-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-chicago-2002-backdrop.jpg deleted file mode 100644 index 6b3d02a3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-chicago-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-chicago-2002-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-chicago-2002-poster.jpg deleted file mode 100644 index 1def9414..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-chicago-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-citizen-kane-1941-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-citizen-kane-1941-backdrop.jpg deleted file mode 100644 index d41a10ef..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-citizen-kane-1941-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-citizen-kane-1941-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-citizen-kane-1941-poster.jpg deleted file mode 100644 index f26414a7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-citizen-kane-1941-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-coherence-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-coherence-2014-backdrop.jpg deleted file mode 100644 index 37da64f9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-coherence-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-coherence-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-coherence-2014-poster.jpg deleted file mode 100644 index f3601b9b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-coherence-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-conclave-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-conclave-2024-backdrop.jpg deleted file mode 100644 index db0a7fcd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-conclave-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-conclave-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-conclave-2024-poster.jpg deleted file mode 100644 index c6aecefa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-conclave-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-crime-of-passion-1956-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-crime-of-passion-1956-backdrop.jpg deleted file mode 100644 index 6c70a452..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-crime-of-passion-1956-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-crime-of-passion-1956-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-crime-of-passion-1956-poster.jpg deleted file mode 100644 index 4acfa871..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-crime-of-passion-1956-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-deep-water-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-deep-water-2022-backdrop.jpg deleted file mode 100644 index bec98992..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-deep-water-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-deep-water-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-deep-water-2022-poster.jpg deleted file mode 100644 index 018d4f4b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-deep-water-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-derailed-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-derailed-2005-backdrop.jpg deleted file mode 100644 index e958449b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-derailed-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-derailed-2005-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-derailed-2005-poster.jpg deleted file mode 100644 index d93f3124..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-derailed-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-die-my-love-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-die-my-love-2025-backdrop.jpg deleted file mode 100644 index 754e713f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-die-my-love-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-die-my-love-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-die-my-love-2025-poster.jpg deleted file mode 100644 index f6eca818..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-die-my-love-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-house-2011-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-dream-house-2011-backdrop.jpg deleted file mode 100644 index 29ecc103..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-house-2011-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-house-2011-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-dream-house-2011-poster.jpg deleted file mode 100644 index d3444637..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-house-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-lover-1994-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-dream-lover-1994-backdrop.jpg deleted file mode 100644 index 7f5a31ac..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-lover-1994-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-lover-1994-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-dream-lover-1994-poster.jpg deleted file mode 100644 index 3b563cf9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-dream-lover-1994-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-drop-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-drop-2025-backdrop.jpg deleted file mode 100644 index ee642530..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-drop-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-drop-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-drop-2025-poster.jpg deleted file mode 100644 index 4cf7d66b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-drop-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-eastern-promises-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-eastern-promises-2007-backdrop.jpg deleted file mode 100644 index 9096a0fa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-eastern-promises-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-eastern-promises-2007-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-eastern-promises-2007-poster.jpg deleted file mode 100644 index 01a26f3b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-eastern-promises-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-easy-a-2010-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-easy-a-2010-backdrop.jpg deleted file mode 100644 index c27fa57b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-easy-a-2010-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-easy-a-2010-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-easy-a-2010-poster.jpg deleted file mode 100644 index 45e13a32..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-easy-a-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg deleted file mode 100644 index 8edc02a6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg deleted file mode 100644 index 7a62df45..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-emancipation-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-emancipation-2022-backdrop.jpg deleted file mode 100644 index ac0777c7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-emancipation-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-emancipation-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-emancipation-2022-poster.jpg deleted file mode 100644 index c1bee971..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-emancipation-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-faces-1968-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-faces-1968-backdrop.jpg deleted file mode 100644 index 60273dd9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-faces-1968-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-faces-1968-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-faces-1968-poster.jpg deleted file mode 100644 index 43f996bd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-faces-1968-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-fatale-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-fatale-2020-backdrop.jpg deleted file mode 100644 index b13c33c6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-fatale-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-fatale-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-fatale-2020-poster.jpg deleted file mode 100644 index dda9d622..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-fatale-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-fighting-with-my-family-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-fighting-with-my-family-2019-backdrop.jpg deleted file mode 100644 index 46835996..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-fighting-with-my-family-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-fighting-with-my-family-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-fighting-with-my-family-2019-poster.jpg deleted file mode 100644 index 169c466d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-fighting-with-my-family-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg deleted file mode 100644 index 3735ee63..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg deleted file mode 100644 index 93ba0d71..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-free-state-of-jones-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-free-state-of-jones-2016-backdrop.jpg deleted file mode 100644 index a7e5ddff..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-free-state-of-jones-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-free-state-of-jones-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-free-state-of-jones-2016-poster.jpg deleted file mode 100644 index e69af7c0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-free-state-of-jones-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-from-up-on-poppy-hill-2011-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-from-up-on-poppy-hill-2011-backdrop.jpg deleted file mode 100644 index 32a28bd7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-from-up-on-poppy-hill-2011-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-from-up-on-poppy-hill-2011-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-from-up-on-poppy-hill-2011-poster.jpg deleted file mode 100644 index 264076e2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-from-up-on-poppy-hill-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-1995-backdrop.jpg deleted file mode 100644 index 716b8a87..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-1995-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-1995-poster.jpg deleted file mode 100644 index 58d221ba..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg deleted file mode 100644 index 4a9931ed..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg deleted file mode 100644 index ae262147..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-gosford-park-2001-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-gosford-park-2001-backdrop.jpg deleted file mode 100644 index 42c4cea6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-gosford-park-2001-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-gosford-park-2001-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-gosford-park-2001-poster.jpg deleted file mode 100644 index 6a6aa651..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-gosford-park-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-grave-of-the-fireflies-1988-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-grave-of-the-fireflies-1988-backdrop.jpg deleted file mode 100644 index 0e1add06..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-grave-of-the-fireflies-1988-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-grave-of-the-fireflies-1988-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-grave-of-the-fireflies-1988-poster.jpg deleted file mode 100644 index 1b71996a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-grave-of-the-fireflies-1988-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-howl-s-moving-castle-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-howl-s-moving-castle-2004-backdrop.jpg deleted file mode 100644 index 0aeef2ba..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-howl-s-moving-castle-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-howl-s-moving-castle-2004-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-howl-s-moving-castle-2004-poster.jpg deleted file mode 100644 index 042a6c80..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-howl-s-moving-castle-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-in-the-heights-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-in-the-heights-2021-backdrop.jpg deleted file mode 100644 index 7cd4ee24..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-in-the-heights-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-in-the-heights-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-in-the-heights-2021-poster.jpg deleted file mode 100644 index e05bfdb0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-in-the-heights-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-jennifer-s-body-2009-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-jennifer-s-body-2009-backdrop.jpg deleted file mode 100644 index 032771a0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-jennifer-s-body-2009-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-jennifer-s-body-2009-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-jennifer-s-body-2009-poster.jpg deleted file mode 100644 index 41cc1eb5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-jennifer-s-body-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-kiki-s-delivery-service-1989-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-kiki-s-delivery-service-1989-backdrop.jpg deleted file mode 100644 index 2a108f0c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-kiki-s-delivery-service-1989-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-kiki-s-delivery-service-1989-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-kiki-s-delivery-service-1989-poster.jpg deleted file mode 100644 index 80a59b1e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-kiki-s-delivery-service-1989-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg deleted file mode 100644 index 38203b77..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg deleted file mode 100644 index 4c78fee9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-locke-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-locke-2014-backdrop.jpg deleted file mode 100644 index a24e64ab..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-locke-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-locke-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-locke-2014-poster.jpg deleted file mode 100644 index 94ebe97d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-locke-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-look-who-s-back-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-look-who-s-back-2015-backdrop.jpg deleted file mode 100644 index a064686f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-look-who-s-back-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-look-who-s-back-2015-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-look-who-s-back-2015-poster.jpg deleted file mode 100644 index 423043cf..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-look-who-s-back-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-memories-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-memories-1995-backdrop.jpg deleted file mode 100644 index 2fcaeaad..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-memories-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-memories-1995-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-memories-1995-poster.jpg deleted file mode 100644 index f3725b42..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-memories-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-mercy-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-mercy-2026-backdrop.jpg deleted file mode 100644 index 45591cc8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-mercy-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-mercy-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-mercy-2026-poster.jpg deleted file mode 100644 index cfc0d812..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-mercy-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg deleted file mode 100644 index da38be48..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg deleted file mode 100644 index c6fa6a26..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-normal-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-normal-2026-backdrop.jpg deleted file mode 100644 index 51711864..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-normal-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-normal-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-normal-2026-poster.jpg deleted file mode 100644 index f4c89db7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-normal-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-outlaw-king-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-outlaw-king-2018-backdrop.jpg deleted file mode 100644 index 54ac5a2a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-outlaw-king-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-outlaw-king-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-outlaw-king-2018-poster.jpg deleted file mode 100644 index d6760468..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-outlaw-king-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-panic-room-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-panic-room-2002-backdrop.jpg deleted file mode 100644 index ec877751..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-panic-room-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-panic-room-2002-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-panic-room-2002-poster.jpg deleted file mode 100644 index 0b2a0d30..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-panic-room-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-peaky-blinders-the-immortal-man-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-peaky-blinders-the-immortal-man-2026-backdrop.jpg deleted file mode 100644 index e9d7155a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-peaky-blinders-the-immortal-man-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg deleted file mode 100644 index a4814472..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ponyo-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ponyo-2008-backdrop.jpg deleted file mode 100644 index 8aa4ecbd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ponyo-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-ponyo-2008-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-ponyo-2008-poster.jpg deleted file mode 100644 index f548e831..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-ponyo-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-porco-rosso-1992-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-porco-rosso-1992-backdrop.jpg deleted file mode 100644 index 63d8112e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-porco-rosso-1992-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-porco-rosso-1992-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-porco-rosso-1992-poster.jpg deleted file mode 100644 index 4a6a2bd1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-porco-rosso-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-princess-mononoke-1997-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-princess-mononoke-1997-backdrop.jpg deleted file mode 100644 index 9486d2cf..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-princess-mononoke-1997-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-princess-mononoke-1997-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-princess-mononoke-1997-poster.jpg deleted file mode 100644 index 21dd9359..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-princess-mononoke-1997-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-prisoners-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-prisoners-2013-backdrop.jpg deleted file mode 100644 index 724008c5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-prisoners-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-prisoners-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-prisoners-2013-poster.jpg deleted file mode 100644 index 411b149b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-prisoners-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-racing-stripes-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-racing-stripes-2005-backdrop.jpg deleted file mode 100644 index 1d332cec..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-racing-stripes-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-racing-stripes-2005-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-racing-stripes-2005-poster.jpg deleted file mode 100644 index cc2ef2ce..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-racing-stripes-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-reincarnated-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-reincarnated-2012-backdrop.jpg deleted file mode 100644 index 326e8ebc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-reincarnated-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-reincarnated-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-reincarnated-2012-poster.jpg deleted file mode 100644 index e9ab449c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-reincarnated-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-requiem-for-a-dream-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-requiem-for-a-dream-2000-backdrop.jpg deleted file mode 100644 index 21842ef2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-requiem-for-a-dream-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-requiem-for-a-dream-2000-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-requiem-for-a-dream-2000-poster.jpg deleted file mode 100644 index 6f89abc3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-requiem-for-a-dream-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-rogue-one-a-star-wars-story-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-rogue-one-a-star-wars-story-2016-backdrop.jpg deleted file mode 100644 index ff0c14c0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-rogue-one-a-star-wars-story-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg deleted file mode 100644 index c79e70a1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-secondhand-lions-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-secondhand-lions-2003-backdrop.jpg deleted file mode 100644 index 7ea43937..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-secondhand-lions-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-secondhand-lions-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-secondhand-lions-2003-poster.jpg deleted file mode 100644 index 33408fa0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-secondhand-lions-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-self-reliance-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-self-reliance-2024-backdrop.jpg deleted file mode 100644 index 3cc0d123..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-self-reliance-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-self-reliance-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-self-reliance-2024-poster.jpg deleted file mode 100644 index 0f6017ed..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-self-reliance-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-serpico-1973-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-serpico-1973-backdrop.jpg deleted file mode 100644 index c9649485..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-serpico-1973-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-serpico-1973-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-serpico-1973-poster.jpg deleted file mode 100644 index 8ddd6e52..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-serpico-1973-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-shaft-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-shaft-2000-backdrop.jpg deleted file mode 100644 index 13f14f93..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-shaft-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-shaft-2000-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-shaft-2000-poster.jpg deleted file mode 100644 index 6b2b79ce..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-shaft-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-shipwrecked-nightmare-at-sea-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-shipwrecked-nightmare-at-sea-2026-backdrop.jpg deleted file mode 100644 index 6e98e242..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-shipwrecked-nightmare-at-sea-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg deleted file mode 100644 index f200a28d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg deleted file mode 100644 index d04685fe..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg deleted file mode 100644 index 428bb38b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-sleeping-with-other-people-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-sleeping-with-other-people-2015-backdrop.jpg deleted file mode 100644 index df5bb046..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-sleeping-with-other-people-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-sleeping-with-other-people-2015-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-sleeping-with-other-people-2015-poster.jpg deleted file mode 100644 index a5ffff1f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-sleeping-with-other-people-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-snowpiercer-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-snowpiercer-2013-backdrop.jpg deleted file mode 100644 index c1281a9c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-snowpiercer-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-snowpiercer-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-snowpiercer-2013-poster.jpg deleted file mode 100644 index be1873fd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-snowpiercer-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-brand-new-day-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-brand-new-day-2026-backdrop.jpg deleted file mode 100644 index 2fdaadb4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-brand-new-day-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-brand-new-day-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-brand-new-day-2026-poster.jpg deleted file mode 100644 index 51fa155f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-brand-new-day-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-far-from-home-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-far-from-home-2019-backdrop.jpg deleted file mode 100644 index d09f655a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-far-from-home-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-far-from-home-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-far-from-home-2019-poster.jpg deleted file mode 100644 index 28521997..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-far-from-home-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-homecoming-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-homecoming-2017-backdrop.jpg deleted file mode 100644 index 6088fadf..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-homecoming-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-homecoming-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-homecoming-2017-poster.jpg deleted file mode 100644 index 97fc9c05..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-homecoming-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-no-way-home-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-no-way-home-2021-backdrop.jpg deleted file mode 100644 index a76afec9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-no-way-home-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-no-way-home-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-no-way-home-2021-poster.jpg deleted file mode 100644 index bc92a4cd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spider-man-no-way-home-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spirited-away-2001-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spirited-away-2001-backdrop.jpg deleted file mode 100644 index af91c123..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spirited-away-2001-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-spirited-away-2001-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-spirited-away-2001-poster.jpg deleted file mode 100644 index 5a79da62..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-spirited-away-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg deleted file mode 100644 index f6675060..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg deleted file mode 100644 index e85ddb90..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-taxi-driver-1976-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-taxi-driver-1976-backdrop.jpg deleted file mode 100644 index 7946ea40..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-taxi-driver-1976-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-taxi-driver-1976-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-taxi-driver-1976-poster.jpg deleted file mode 100644 index c410b981..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-taxi-driver-1976-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-alto-knights-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-alto-knights-2025-backdrop.jpg deleted file mode 100644 index ea619c14..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-alto-knights-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-alto-knights-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-alto-knights-2025-poster.jpg deleted file mode 100644 index bb009cbf..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-alto-knights-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg deleted file mode 100644 index 7c5878b7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2-2014-poster.jpg deleted file mode 100644 index 4de2711f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2012-backdrop.jpg deleted file mode 100644 index 75ae9b64..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2012-poster.jpg deleted file mode 100644 index c1aec876..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-amazing-spider-man-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-animatrix-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-animatrix-2003-backdrop.jpg deleted file mode 100644 index 7d67ccb0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-animatrix-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-animatrix-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-animatrix-2003-poster.jpg deleted file mode 100644 index 65553d47..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-animatrix-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-avengers-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-avengers-2012-backdrop.jpg deleted file mode 100644 index 83636f25..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-avengers-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-avengers-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-avengers-2012-poster.jpg deleted file mode 100644 index be87067c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-avengers-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-boy-and-the-heron-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-boy-and-the-heron-2023-backdrop.jpg deleted file mode 100644 index a19dc77b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-boy-and-the-heron-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-boy-and-the-heron-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-boy-and-the-heron-2023-poster.jpg deleted file mode 100644 index 094df64e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-boy-and-the-heron-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-breakfast-club-1985-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-breakfast-club-1985-backdrop.jpg deleted file mode 100644 index 9f979cd8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-breakfast-club-1985-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-breakfast-club-1985-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-breakfast-club-1985-poster.jpg deleted file mode 100644 index d1823120..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-breakfast-club-1985-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-dry-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-dry-2021-backdrop.jpg deleted file mode 100644 index 2f016498..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-dry-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-dry-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-dry-2021-poster.jpg deleted file mode 100644 index a0379f2f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-dry-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg deleted file mode 100644 index 6e920467..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg deleted file mode 100644 index ded6cd2a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-1972-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-1972-backdrop.jpg deleted file mode 100644 index 8fbb8f2a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-1972-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-1972-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-1972-poster.jpg deleted file mode 100644 index 94111d9b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-1972-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-ii-1974-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-ii-1974-backdrop.jpg deleted file mode 100644 index 6dbff534..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-ii-1974-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-ii-1974-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-ii-1974-poster.jpg deleted file mode 100644 index 1328c99e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-ii-1974-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-iii-1990-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-iii-1990-backdrop.jpg deleted file mode 100644 index d2080487..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-iii-1990-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-iii-1990-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-iii-1990-poster.jpg deleted file mode 100644 index abbe5e99..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-godfather-part-iii-1990-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-great-gatsby-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-great-gatsby-2013-backdrop.jpg deleted file mode 100644 index 7a4f8a32..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-great-gatsby-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-great-gatsby-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-great-gatsby-2013-poster.jpg deleted file mode 100644 index e949fb3e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-great-gatsby-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-hours-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-hours-2002-backdrop.jpg deleted file mode 100644 index 507a78c4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-hours-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-hours-2002-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-hours-2002-poster.jpg deleted file mode 100644 index f87e919f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-hours-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg deleted file mode 100644 index dd755ee8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg deleted file mode 100644 index 83464d4c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-of-the-mohicans-1992-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-of-the-mohicans-1992-backdrop.jpg deleted file mode 100644 index 3256d38d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-of-the-mohicans-1992-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-of-the-mohicans-1992-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-of-the-mohicans-1992-poster.jpg deleted file mode 100644 index 950e9ba1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-of-the-mohicans-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-samurai-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-samurai-2003-backdrop.jpg deleted file mode 100644 index 509af2d6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-samurai-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-samurai-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-samurai-2003-poster.jpg deleted file mode 100644 index 0d58440c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-last-samurai-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-lighthouse-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-lighthouse-2019-backdrop.jpg deleted file mode 100644 index 862c064a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-lighthouse-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-lighthouse-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-lighthouse-2019-poster.jpg deleted file mode 100644 index 15f26533..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-lighthouse-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-1999-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-1999-backdrop.jpg deleted file mode 100644 index c3bf8f52..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-1999-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-1999-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-1999-poster.jpg deleted file mode 100644 index c292dce5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-1999-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-reloaded-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-reloaded-2003-backdrop.jpg deleted file mode 100644 index dcbf7b96..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-reloaded-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-reloaded-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-reloaded-2003-poster.jpg deleted file mode 100644 index ab11f50f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-reloaded-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-resurrections-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-resurrections-2021-backdrop.jpg deleted file mode 100644 index 99d85a40..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-resurrections-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-resurrections-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-resurrections-2021-poster.jpg deleted file mode 100644 index 4477ef1e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-resurrections-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-revolutions-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-revolutions-2003-backdrop.jpg deleted file mode 100644 index 2cc02692..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-revolutions-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-revolutions-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-revolutions-2003-poster.jpg deleted file mode 100644 index 7236cc4e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-matrix-revolutions-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-patriot-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-patriot-2000-backdrop.jpg deleted file mode 100644 index ce7ce498..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-patriot-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-patriot-2000-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-patriot-2000-poster.jpg deleted file mode 100644 index c8582133..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-patriot-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg deleted file mode 100644 index 698f4ad2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg deleted file mode 100644 index fe50c2f8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-substance-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-substance-2024-backdrop.jpg deleted file mode 100644 index 47a8b372..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-substance-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-substance-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-substance-2024-poster.jpg deleted file mode 100644 index 72accfed..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-substance-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg deleted file mode 100644 index b9157b91..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg deleted file mode 100644 index 882621cb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-uprising-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-uprising-2026-backdrop.jpg deleted file mode 100644 index 638eab3d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-uprising-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-uprising-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-uprising-2026-poster.jpg deleted file mode 100644 index 21976395..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-uprising-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wild-robot-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-wild-robot-2024-backdrop.jpg deleted file mode 100644 index bb48460b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wild-robot-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wild-robot-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-wild-robot-2024-poster.jpg deleted file mode 100644 index 0894754d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wild-robot-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wind-rises-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-wind-rises-2013-backdrop.jpg deleted file mode 100644 index 2af40e28..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wind-rises-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wind-rises-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-wind-rises-2013-poster.jpg deleted file mode 100644 index b7688c77..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-wind-rises-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg deleted file mode 100644 index cb5bfb28..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg deleted file mode 100644 index 6db33db1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg deleted file mode 100644 index aad8ea2c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg deleted file mode 100644 index 8a54bc7f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-thelma-louise-1991-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-thelma-louise-1991-backdrop.jpg deleted file mode 100644 index a35327a9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-thelma-louise-1991-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-thelma-louise-1991-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-thelma-louise-1991-poster.jpg deleted file mode 100644 index 5a4ff0ab..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-thelma-louise-1991-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-triangle-of-sadness-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-triangle-of-sadness-2022-backdrop.jpg deleted file mode 100644 index 7c9b445d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-triangle-of-sadness-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-triangle-of-sadness-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-triangle-of-sadness-2022-poster.jpg deleted file mode 100644 index aee092c6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-triangle-of-sadness-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-tsotsi-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-tsotsi-2005-backdrop.jpg deleted file mode 100644 index e119de53..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-tsotsi-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-tsotsi-2005-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-tsotsi-2005-poster.jpg deleted file mode 100644 index 362ddd82..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-tsotsi-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-unknown-number-the-high-school-catfish-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-unknown-number-the-high-school-catfish-2025-backdrop.jpg deleted file mode 100644 index f456c01d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-unknown-number-the-high-school-catfish-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg deleted file mode 100644 index e32685f3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-vice-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-vice-2018-backdrop.jpg deleted file mode 100644 index 2918db3a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-vice-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-vice-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-vice-2018-poster.jpg deleted file mode 100644 index 76855126..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-vice-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-vicky-cristina-barcelona-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-vicky-cristina-barcelona-2008-backdrop.jpg deleted file mode 100644 index 20eaa159..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-vicky-cristina-barcelona-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-vicky-cristina-barcelona-2008-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-vicky-cristina-barcelona-2008-poster.jpg deleted file mode 100644 index 1351cad1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-vicky-cristina-barcelona-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-warriors-of-the-wind-1984-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-warriors-of-the-wind-1984-backdrop.jpg deleted file mode 100644 index 1fb22b96..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-warriors-of-the-wind-1984-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-warriors-of-the-wind-1984-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-warriors-of-the-wind-1984-poster.jpg deleted file mode 100644 index 918e424f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-warriors-of-the-wind-1984-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-we-bury-the-dead-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-we-bury-the-dead-2026-backdrop.jpg deleted file mode 100644 index 4a24720d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-we-bury-the-dead-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-we-bury-the-dead-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-we-bury-the-dead-2026-poster.jpg deleted file mode 100644 index a0d65904..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-we-bury-the-dead-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-backdrop.jpg deleted file mode 100644 index 2f186030..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg deleted file mode 100644 index 3a123e80..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-12-monkeys-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-12-monkeys-2015-backdrop.jpg deleted file mode 100644 index 3d2825f5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-12-monkeys-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-12-monkeys-2015-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-12-monkeys-2015-poster.jpg deleted file mode 100644 index 96e3596e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-12-monkeys-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-56-days-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-56-days-2026-backdrop.jpg deleted file mode 100644 index dcda667a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-56-days-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-56-days-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-56-days-2026-poster.jpg deleted file mode 100644 index 64ca587b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-56-days-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg deleted file mode 100644 index 24f66f2d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg deleted file mode 100644 index 3362da59..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg deleted file mode 100644 index b18891fd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-the-other-way-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-the-other-way-2019-poster.jpg deleted file mode 100644 index b4d256eb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-90-day-fiance-the-other-way-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg deleted file mode 100644 index ce12fc71..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg deleted file mode 100644 index 79f1351c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-absolutely-fabulous-1992-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-absolutely-fabulous-1992-backdrop.jpg deleted file mode 100644 index 2df9722d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-absolutely-fabulous-1992-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-absolutely-fabulous-1992-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-absolutely-fabulous-1992-poster.jpg deleted file mode 100644 index afd371a9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-absolutely-fabulous-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-accel-world-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-accel-world-2012-backdrop.jpg deleted file mode 100644 index 1f5f6bdc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-accel-world-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-accel-world-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-accel-world-2012-poster.jpg deleted file mode 100644 index 207b1dd1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-accel-world-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-accused-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-accused-2023-backdrop.jpg deleted file mode 100644 index d41c6397..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-accused-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-accused-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-accused-2023-poster.jpg deleted file mode 100644 index 74af010a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-accused-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-adults-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-adults-2025-backdrop.jpg deleted file mode 100644 index 9cc0ffe2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-adults-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-adults-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-adults-2025-poster.jpg deleted file mode 100644 index 53a7ba33..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-adults-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-after-life-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-after-life-2019-backdrop.jpg deleted file mode 100644 index eaf9a4fa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-after-life-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-after-life-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-after-life-2019-poster.jpg deleted file mode 100644 index 4fae4e33..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-after-life-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-agatha-christie-s-marple-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-agatha-christie-s-marple-2004-backdrop.jpg deleted file mode 100644 index 0c52f2e6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-agatha-christie-s-marple-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-agatha-christie-s-marple-2004-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-agatha-christie-s-marple-2004-poster.jpg deleted file mode 100644 index ea7dda99..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-agatha-christie-s-marple-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ahsoka-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ahsoka-2023-backdrop.jpg deleted file mode 100644 index 7343fb32..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ahsoka-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ahsoka-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ahsoka-2023-poster.jpg deleted file mode 100644 index a29d6cb4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ahsoka-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-all-american-homecoming-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-all-american-homecoming-2022-backdrop.jpg deleted file mode 100644 index 09fbb9dc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-all-american-homecoming-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-all-american-homecoming-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-all-american-homecoming-2022-poster.jpg deleted file mode 100644 index 09ccfc33..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-all-american-homecoming-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-and-small-1978-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-and-small-1978-backdrop.jpg deleted file mode 100644 index 909ae1ba..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-and-small-1978-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-and-small-1978-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-and-small-1978-poster.jpg deleted file mode 100644 index 5b1666e6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-and-small-1978-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-small-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-small-2020-backdrop.jpg deleted file mode 100644 index d2bcc88d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-small-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-small-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-small-2020-poster.jpg deleted file mode 100644 index b506f990..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-all-creatures-great-small-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-crime-story-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-crime-story-2016-backdrop.jpg deleted file mode 100644 index 84944d39..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-crime-story-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-crime-story-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-crime-story-2016-poster.jpg deleted file mode 100644 index 4086bf17..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-crime-story-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-dad-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-dad-2005-backdrop.jpg deleted file mode 100644 index 75a1948b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-dad-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-dad-2005-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-dad-2005-poster.jpg deleted file mode 100644 index 1f9e239a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-dad-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-gods-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-gods-2017-backdrop.jpg deleted file mode 100644 index 4817c690..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-gods-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-gods-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-gods-2017-poster.jpg deleted file mode 100644 index c02c46da..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-gods-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-housewife-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-housewife-2016-backdrop.jpg deleted file mode 100644 index bf758ae0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-housewife-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-housewife-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-housewife-2016-poster.jpg deleted file mode 100644 index 5f92bf05..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-housewife-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-primeval-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-primeval-2025-backdrop.jpg deleted file mode 100644 index c3bfe5e1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-primeval-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-american-primeval-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-american-primeval-2025-poster.jpg deleted file mode 100644 index 7f10e6aa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-american-primeval-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-and-just-like-that-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-and-just-like-that-2021-backdrop.jpg deleted file mode 100644 index 59c981e5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-and-just-like-that-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-and-just-like-that-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-and-just-like-that-2021-poster.jpg deleted file mode 100644 index 9904c74b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-and-just-like-that-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-andor-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-andor-2022-backdrop.jpg deleted file mode 100644 index a874a0c0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-andor-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-andor-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-andor-2022-poster.jpg deleted file mode 100644 index df5dc498..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-andor-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-andromeda-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-andromeda-2000-backdrop.jpg deleted file mode 100644 index 199249d7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-andromeda-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-andromeda-2000-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-andromeda-2000-poster.jpg deleted file mode 100644 index 67b766b0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-andromeda-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-anne-with-an-e-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-anne-with-an-e-2017-backdrop.jpg deleted file mode 100644 index 15e325c9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-anne-with-an-e-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-anne-with-an-e-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-anne-with-an-e-2017-poster.jpg deleted file mode 100644 index 55d443b4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-anne-with-an-e-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg deleted file mode 100644 index d0e37959..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-aqua-teen-hunger-force-2000-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-aqua-teen-hunger-force-2000-poster.jpg deleted file mode 100644 index e0c97321..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-aqua-teen-hunger-force-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg deleted file mode 100644 index 9300e20f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg deleted file mode 100644 index 6a8f9a24..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-arrested-development-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-arrested-development-2003-backdrop.jpg deleted file mode 100644 index ecb7e8cd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-arrested-development-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-arrested-development-2003-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-arrested-development-2003-poster.jpg deleted file mode 100644 index 02b0dd43..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-arrested-development-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-arrow-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-arrow-2012-backdrop.jpg deleted file mode 100644 index b32f5aca..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-arrow-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-arrow-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-arrow-2012-poster.jpg deleted file mode 100644 index adee3bb7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-arrow-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-atlantis-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-atlantis-2013-backdrop.jpg deleted file mode 100644 index 39ecc0d6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-atlantis-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-atlantis-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-atlantis-2013-poster.jpg deleted file mode 100644 index 5cd5f098..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-atlantis-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-seven-havens-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-seven-havens-2026-backdrop.jpg deleted file mode 100644 index dacfd59e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-seven-havens-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-seven-havens-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-seven-havens-2026-poster.jpg deleted file mode 100644 index 61f459c6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-seven-havens-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2005-backdrop.jpg deleted file mode 100644 index f60120da..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2005-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2005-poster.jpg deleted file mode 100644 index 6f99263a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2024-backdrop.jpg deleted file mode 100644 index ad740218..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2024-poster.jpg deleted file mode 100644 index 9d46c2ff..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-avatar-the-last-airbender-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-awkward-2011-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-awkward-2011-backdrop.jpg deleted file mode 100644 index 1ca5e56c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-awkward-2011-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-awkward-2011-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-awkward-2011-poster.jpg deleted file mode 100644 index 8da6e8e2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-awkward-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-b-b-vol-liefde-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-b-b-vol-liefde-2021-backdrop.jpg deleted file mode 100644 index 57ece15e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-b-b-vol-liefde-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-b-b-vol-liefde-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-b-b-vol-liefde-2021-poster.jpg deleted file mode 100644 index a12ac33e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-b-b-vol-liefde-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-back-to-life-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-back-to-life-2019-backdrop.jpg deleted file mode 100644 index 7ffef681..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-back-to-life-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-back-to-life-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-back-to-life-2019-poster.jpg deleted file mode 100644 index b00730a8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-back-to-life-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ballers-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ballers-2015-backdrop.jpg deleted file mode 100644 index c4685e84..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ballers-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ballers-2015-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ballers-2015-poster.jpg deleted file mode 100644 index f1c18f69..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ballers-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-baretta-1975-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-baretta-1975-backdrop.jpg deleted file mode 100644 index 4d61dfbe..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-baretta-1975-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-baretta-1975-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-baretta-1975-poster.jpg deleted file mode 100644 index ceef0c9d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-baretta-1975-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-based-on-a-true-story-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-based-on-a-true-story-2023-backdrop.jpg deleted file mode 100644 index b59c0e58..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-based-on-a-true-story-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-based-on-a-true-story-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-based-on-a-true-story-2023-poster.jpg deleted file mode 100644 index 386d3d90..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-based-on-a-true-story-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg deleted file mode 100644 index 69e567cb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg deleted file mode 100644 index a864f50c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-batwoman-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-batwoman-2019-backdrop.jpg deleted file mode 100644 index d04a1a20..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-batwoman-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-batwoman-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-batwoman-2019-poster.jpg deleted file mode 100644 index 7ca86592..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-batwoman-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-beat-bobby-flay-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-beat-bobby-flay-2013-backdrop.jpg deleted file mode 100644 index e6c36ddd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-beat-bobby-flay-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-beat-bobby-flay-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-beat-bobby-flay-2013-poster.jpg deleted file mode 100644 index fe11122d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-beat-bobby-flay-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-benson-1979-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-benson-1979-backdrop.jpg deleted file mode 100644 index 32bd63a9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-benson-1979-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-benson-1979-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-benson-1979-poster.jpg deleted file mode 100644 index fe264c76..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-benson-1979-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-big-boys-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-big-boys-2022-backdrop.jpg deleted file mode 100644 index c7c91e2d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-big-boys-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-big-boys-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-big-boys-2022-poster.jpg deleted file mode 100644 index 93e1e86a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-big-boys-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-big-city-greens-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-big-city-greens-2018-backdrop.jpg deleted file mode 100644 index 381774e7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-big-city-greens-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-big-city-greens-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-big-city-greens-2018-poster.jpg deleted file mode 100644 index ef024946..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-big-city-greens-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-big-mouth-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-big-mouth-2017-backdrop.jpg deleted file mode 100644 index 760e0262..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-big-mouth-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-big-mouth-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-big-mouth-2017-poster.jpg deleted file mode 100644 index 4f535fec..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-big-mouth-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-black-ish-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-black-ish-2014-backdrop.jpg deleted file mode 100644 index 967fb621..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-black-ish-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-black-ish-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-black-ish-2014-poster.jpg deleted file mode 100644 index 97c885ad..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-black-ish-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-black-lightning-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-black-lightning-2018-backdrop.jpg deleted file mode 100644 index 3a0854c9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-black-lightning-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-black-lightning-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-black-lightning-2018-poster.jpg deleted file mode 100644 index 733f5cbc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-black-lightning-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-black-rabbit-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-black-rabbit-2025-backdrop.jpg deleted file mode 100644 index 48df9d46..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-black-rabbit-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-black-rabbit-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-black-rabbit-2025-poster.jpg deleted file mode 100644 index 3ff9aeed..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-black-rabbit-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-bleach-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-bleach-2004-backdrop.jpg deleted file mode 100644 index ef8eaaa3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-bleach-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-bleach-2004-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-bleach-2004-poster.jpg deleted file mode 100644 index b233e033..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-bleach-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-blue-planet-ii-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-blue-planet-ii-2017-backdrop.jpg deleted file mode 100644 index 8175e568..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-blue-planet-ii-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-blue-planet-ii-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-blue-planet-ii-2017-poster.jpg deleted file mode 100644 index 5e193ab2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-blue-planet-ii-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-brassic-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-brassic-2019-backdrop.jpg deleted file mode 100644 index 86d3d806..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-brassic-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-brassic-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-brassic-2019-poster.jpg deleted file mode 100644 index 73b77df2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-brassic-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-breaking-bad-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-breaking-bad-2008-backdrop.jpg deleted file mode 100644 index e4ac4ae3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-breaking-bad-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-breaking-bad-2008-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-breaking-bad-2008-poster.jpg deleted file mode 100644 index c4f7217c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-breaking-bad-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-breeders-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-breeders-2020-backdrop.jpg deleted file mode 100644 index 9d95ba77..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-breeders-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-breeders-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-breeders-2020-poster.jpg deleted file mode 100644 index 49c61208..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-breeders-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-californication-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-californication-2007-backdrop.jpg deleted file mode 100644 index ffff979b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-californication-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-californication-2007-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-californication-2007-poster.jpg deleted file mode 100644 index 38428e76..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-californication-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-call-the-midwife-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-call-the-midwife-2012-backdrop.jpg deleted file mode 100644 index 1ee28b8d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-call-the-midwife-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-call-the-midwife-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-call-the-midwife-2012-poster.jpg deleted file mode 100644 index 466a1a7b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-call-the-midwife-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-chainsaw-man-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-chainsaw-man-2022-backdrop.jpg deleted file mode 100644 index f1fbee3e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-chainsaw-man-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-chainsaw-man-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-chainsaw-man-2022-poster.jpg deleted file mode 100644 index a3779772..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-chainsaw-man-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-chernobyl-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-chernobyl-2019-backdrop.jpg deleted file mode 100644 index 818a6665..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-chernobyl-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-chernobyl-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-chernobyl-2019-poster.jpg deleted file mode 100644 index 7664f4b0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-chernobyl-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-community-2009-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-community-2009-backdrop.jpg deleted file mode 100644 index 8deb8d5c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-community-2009-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-community-2009-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-community-2009-poster.jpg deleted file mode 100644 index e75072ae..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-community-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg deleted file mode 100644 index 2920cf5a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-cyberpunk-edgerunners-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-cyberpunk-edgerunners-2022-poster.jpg deleted file mode 100644 index 8d338532..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-cyberpunk-edgerunners-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dan-da-dan-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dan-da-dan-2024-backdrop.jpg deleted file mode 100644 index 0cbc4b07..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dan-da-dan-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dan-da-dan-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dan-da-dan-2024-poster.jpg deleted file mode 100644 index f42cec16..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dan-da-dan-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dead-end-job-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dead-end-job-2026-backdrop.jpg deleted file mode 100644 index 3525c2fc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dead-end-job-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dead-end-job-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dead-end-job-2026-poster.jpg deleted file mode 100644 index f5546d88..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dead-end-job-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-derry-girls-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-derry-girls-2018-backdrop.jpg deleted file mode 100644 index 76fae98c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-derry-girls-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-derry-girls-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-derry-girls-2018-poster.jpg deleted file mode 100644 index 56a8cd79..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-derry-girls-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-desperate-housewives-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-desperate-housewives-2004-backdrop.jpg deleted file mode 100644 index 40ec3001..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-desperate-housewives-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-desperate-housewives-2004-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-desperate-housewives-2004-poster.jpg deleted file mode 100644 index 0c4c38bc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-desperate-housewives-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dopesick-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dopesick-2021-backdrop.jpg deleted file mode 100644 index 8d9f2727..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dopesick-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dopesick-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dopesick-2021-poster.jpg deleted file mode 100644 index ac0b5900..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dopesick-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dr-stone-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dr-stone-2019-backdrop.jpg deleted file mode 100644 index 5ac396b6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dr-stone-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dr-stone-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dr-stone-2019-poster.jpg deleted file mode 100644 index 0783ed8c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dr-stone-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dune-prophecy-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dune-prophecy-2024-backdrop.jpg deleted file mode 100644 index 5b940b9f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dune-prophecy-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-dune-prophecy-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-dune-prophecy-2024-poster.jpg deleted file mode 100644 index eef2a4f3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-dune-prophecy-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-east-of-eden-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-east-of-eden-2026-backdrop.jpg deleted file mode 100644 index 89dfafbf..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-east-of-eden-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-east-of-eden-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-east-of-eden-2026-poster.jpg deleted file mode 100644 index 6f06f2c9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-east-of-eden-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-etoile-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-etoile-2025-backdrop.jpg deleted file mode 100644 index a8e9576c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-etoile-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-etoile-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-etoile-2025-poster.jpg deleted file mode 100644 index f078bc89..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-etoile-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fallout-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fallout-2024-backdrop.jpg deleted file mode 100644 index c9d6a190..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fallout-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fallout-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fallout-2024-poster.jpg deleted file mode 100644 index 5e87a07d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fallout-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fawlty-towers-1975-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fawlty-towers-1975-backdrop.jpg deleted file mode 100644 index 2c9c258f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fawlty-towers-1975-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fawlty-towers-1975-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fawlty-towers-1975-poster.jpg deleted file mode 100644 index 53b406da..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fawlty-towers-1975-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-firefly-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-firefly-2002-backdrop.jpg deleted file mode 100644 index fa3b7604..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-firefly-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-firefly-2002-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-firefly-2002-poster.jpg deleted file mode 100644 index 9ad599b3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-firefly-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fleabag-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fleabag-2016-backdrop.jpg deleted file mode 100644 index e5a83462..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fleabag-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fleabag-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fleabag-2016-poster.jpg deleted file mode 100644 index bd3ccea5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fleabag-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-frieren-beyond-journey-s-end-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-frieren-beyond-journey-s-end-2023-backdrop.jpg deleted file mode 100644 index a42a2c94..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-frieren-beyond-journey-s-end-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg deleted file mode 100644 index cb94104e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fullmetal-alchemist-brotherhood-2009-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fullmetal-alchemist-brotherhood-2009-backdrop.jpg deleted file mode 100644 index 91ce4909..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fullmetal-alchemist-brotherhood-2009-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg deleted file mode 100644 index 935e4944..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-gilmore-girls-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-gilmore-girls-2000-backdrop.jpg deleted file mode 100644 index b42ba953..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-gilmore-girls-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-gilmore-girls-2000-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-gilmore-girls-2000-poster.jpg deleted file mode 100644 index 23b668cb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-gilmore-girls-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ginny-georgia-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ginny-georgia-2021-backdrop.jpg deleted file mode 100644 index 1271d577..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ginny-georgia-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ginny-georgia-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ginny-georgia-2021-poster.jpg deleted file mode 100644 index 1fca7bf6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ginny-georgia-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-gravity-falls-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-gravity-falls-2012-backdrop.jpg deleted file mode 100644 index b9ef40fa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-gravity-falls-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-gravity-falls-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-gravity-falls-2012-poster.jpg deleted file mode 100644 index 41c051e0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-gravity-falls-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-hellcats-2010-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-hellcats-2010-backdrop.jpg deleted file mode 100644 index 38e856b0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-hellcats-2010-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-hellcats-2010-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-hellcats-2010-poster.jpg deleted file mode 100644 index 2427108b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-hellcats-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-high-potential-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-high-potential-2024-backdrop.jpg deleted file mode 100644 index 66bb4d00..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-high-potential-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-high-potential-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-high-potential-2024-poster.jpg deleted file mode 100644 index 00a07ca7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-high-potential-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-change-your-mind-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-change-your-mind-2022-backdrop.jpg deleted file mode 100644 index 9e4171e3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-change-your-mind-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-change-your-mind-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-change-your-mind-2022-poster.jpg deleted file mode 100644 index e80c7999..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-change-your-mind-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-get-to-heaven-from-belfast-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-get-to-heaven-from-belfast-2026-backdrop.jpg deleted file mode 100644 index a605895b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-get-to-heaven-from-belfast-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg deleted file mode 100644 index eb5fc487..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-interrogation-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-interrogation-2020-backdrop.jpg deleted file mode 100644 index c347e016..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-interrogation-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-interrogation-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-interrogation-2020-poster.jpg deleted file mode 100644 index f3a21b66..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-interrogation-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-invincible-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-invincible-2021-backdrop.jpg deleted file mode 100644 index e8120387..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-invincible-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-invincible-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-invincible-2021-poster.jpg deleted file mode 100644 index c278e37e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-invincible-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-it-s-always-sunny-in-philadelphia-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-it-s-always-sunny-in-philadelphia-2005-backdrop.jpg deleted file mode 100644 index cbdc6525..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-it-s-always-sunny-in-philadelphia-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg deleted file mode 100644 index 652923ee..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-jujutsu-kaisen-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-jujutsu-kaisen-2020-backdrop.jpg deleted file mode 100644 index 79e7d6bd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-jujutsu-kaisen-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-jujutsu-kaisen-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-jujutsu-kaisen-2020-poster.jpg deleted file mode 100644 index 45f5ff22..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-jujutsu-kaisen-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-kaiju-no-8-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-kaiju-no-8-2024-backdrop.jpg deleted file mode 100644 index 480b2a8e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-kaiju-no-8-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-kaiju-no-8-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-kaiju-no-8-2024-poster.jpg deleted file mode 100644 index 062496fa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-kaiju-no-8-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-landman-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-landman-2024-backdrop.jpg deleted file mode 100644 index 009fbfc2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-landman-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-landman-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-landman-2024-poster.jpg deleted file mode 100644 index 3d348798..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-landman-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-last-samurai-standing-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-last-samurai-standing-2025-backdrop.jpg deleted file mode 100644 index c85a210b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-last-samurai-standing-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-last-samurai-standing-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-last-samurai-standing-2025-poster.jpg deleted file mode 100644 index a9789909..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-last-samurai-standing-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-lord-of-the-flies-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-lord-of-the-flies-2026-backdrop.jpg deleted file mode 100644 index 1249a8f3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-lord-of-the-flies-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-lord-of-the-flies-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-lord-of-the-flies-2026-poster.jpg deleted file mode 100644 index b1229ed8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-lord-of-the-flies-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-love-death-robots-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-love-death-robots-2019-backdrop.jpg deleted file mode 100644 index 3694bfc4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-love-death-robots-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-love-death-robots-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-love-death-robots-2019-poster.jpg deleted file mode 100644 index 08449aa9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-love-death-robots-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-love-is-blind-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-love-is-blind-2020-backdrop.jpg deleted file mode 100644 index 5cea5b52..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-love-is-blind-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-love-is-blind-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-love-is-blind-2020-poster.jpg deleted file mode 100644 index 62ccf357..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-love-is-blind-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-maniac-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-maniac-2018-backdrop.jpg deleted file mode 100644 index 9d46de1d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-maniac-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-maniac-2018-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-maniac-2018-poster.jpg deleted file mode 100644 index a1985217..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-maniac-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-maximum-pleasure-guaranteed-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-maximum-pleasure-guaranteed-2026-backdrop.jpg deleted file mode 100644 index 20748a70..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-maximum-pleasure-guaranteed-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg deleted file mode 100644 index abe1b2e2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-minx-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-minx-2022-backdrop.jpg deleted file mode 100644 index bd459fde..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-minx-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-minx-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-minx-2022-poster.jpg deleted file mode 100644 index a8797fe5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-minx-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-mussolini-son-of-the-century-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-mussolini-son-of-the-century-2024-backdrop.jpg deleted file mode 100644 index 21674163..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-mussolini-son-of-the-century-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-mussolini-son-of-the-century-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-mussolini-son-of-the-century-2024-poster.jpg deleted file mode 100644 index 925d5c49..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-mussolini-son-of-the-century-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-my-hero-academia-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-my-hero-academia-2016-backdrop.jpg deleted file mode 100644 index f5b2f94c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-my-hero-academia-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-my-hero-academia-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-my-hero-academia-2016-poster.jpg deleted file mode 100644 index cf571b93..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-my-hero-academia-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-neon-genesis-evangelion-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-neon-genesis-evangelion-1995-backdrop.jpg deleted file mode 100644 index d80e17c1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-neon-genesis-evangelion-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-neon-genesis-evangelion-1995-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-neon-genesis-evangelion-1995-poster.jpg deleted file mode 100644 index f86cd698..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-neon-genesis-evangelion-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-norsemen-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-norsemen-2016-backdrop.jpg deleted file mode 100644 index 58805e67..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-norsemen-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-norsemen-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-norsemen-2016-poster.jpg deleted file mode 100644 index e7206b88..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-norsemen-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-offspring-2010-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-offspring-2010-backdrop.jpg deleted file mode 100644 index bc22e11a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-offspring-2010-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-offspring-2010-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-offspring-2010-poster.jpg deleted file mode 100644 index ae305043..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-offspring-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-one-piece-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-one-piece-2023-backdrop.jpg deleted file mode 100644 index 3fa4a282..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-one-piece-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-one-piece-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-one-piece-2023-poster.jpg deleted file mode 100644 index 286e6260..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-one-piece-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-only-murders-in-the-building-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-only-murders-in-the-building-2021-backdrop.jpg deleted file mode 100644 index 0b768d54..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-only-murders-in-the-building-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-only-murders-in-the-building-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-only-murders-in-the-building-2021-poster.jpg deleted file mode 100644 index a6b147a4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-only-murders-in-the-building-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-outlander-blood-of-my-blood-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-outlander-blood-of-my-blood-2025-backdrop.jpg deleted file mode 100644 index 23a242da..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-outlander-blood-of-my-blood-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-outlander-blood-of-my-blood-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-outlander-blood-of-my-blood-2025-poster.jpg deleted file mode 100644 index 930943b0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-outlander-blood-of-my-blood-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-over-the-garden-wall-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-over-the-garden-wall-2014-backdrop.jpg deleted file mode 100644 index 43ac4428..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-over-the-garden-wall-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-over-the-garden-wall-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-over-the-garden-wall-2014-poster.jpg deleted file mode 100644 index 34b1ed84..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-over-the-garden-wall-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-peaky-blinders-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-peaky-blinders-2013-backdrop.jpg deleted file mode 100644 index da587ec3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-peaky-blinders-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-peaky-blinders-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-peaky-blinders-2013-poster.jpg deleted file mode 100644 index 25ceea88..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-peaky-blinders-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-pluribus-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-pluribus-2025-backdrop.jpg deleted file mode 100644 index e64c0268..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-pluribus-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-pluribus-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-pluribus-2025-poster.jpg deleted file mode 100644 index 1ea4711b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-pluribus-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-record-of-ragnarok-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-record-of-ragnarok-2021-backdrop.jpg deleted file mode 100644 index d68a4060..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-record-of-ragnarok-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-record-of-ragnarok-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-record-of-ragnarok-2021-poster.jpg deleted file mode 100644 index 97d481d6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-record-of-ragnarok-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-rick-and-morty-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-rick-and-morty-2013-backdrop.jpg deleted file mode 100644 index 1c4c0ffc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-rick-and-morty-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-rick-and-morty-2013-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-rick-and-morty-2013-poster.jpg deleted file mode 100644 index b95c4890..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-rick-and-morty-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ripley-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ripley-2024-backdrop.jpg deleted file mode 100644 index 2e7399c1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ripley-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-ripley-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-ripley-2024-poster.jpg deleted file mode 100644 index de0b32dd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-ripley-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-rivals-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-rivals-2024-backdrop.jpg deleted file mode 100644 index fe7025e4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-rivals-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-rivals-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-rivals-2024-poster.jpg deleted file mode 100644 index 296ac67a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-rivals-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-rooster-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-rooster-2026-backdrop.jpg deleted file mode 100644 index f0f7a19e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-rooster-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-rooster-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-rooster-2026-poster.jpg deleted file mode 100644 index b698f8a6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-rooster-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-sakamoto-days-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-sakamoto-days-2025-backdrop.jpg deleted file mode 100644 index 3186e89a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-sakamoto-days-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-sakamoto-days-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-sakamoto-days-2025-poster.jpg deleted file mode 100644 index ed9358b3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-sakamoto-days-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-say-nothing-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-say-nothing-2024-backdrop.jpg deleted file mode 100644 index 445862dc..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-say-nothing-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-say-nothing-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-say-nothing-2024-poster.jpg deleted file mode 100644 index cf50fc09..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-say-nothing-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-secret-level-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-secret-level-2024-backdrop.jpg deleted file mode 100644 index 54d2a13e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-secret-level-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-secret-level-2024-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-secret-level-2024-poster.jpg deleted file mode 100644 index 1b6e67f6..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-secret-level-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg deleted file mode 100644 index 8d081a15..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-sentenced-to-be-a-hero-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-sentenced-to-be-a-hero-2025-poster.jpg deleted file mode 100644 index 2fb1c815..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-sentenced-to-be-a-hero-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-severance-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-severance-2022-backdrop.jpg deleted file mode 100644 index a24c03bd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-severance-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-severance-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-severance-2022-poster.jpg deleted file mode 100644 index 5a29d6f9..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-severance-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-sex-and-the-city-1998-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-sex-and-the-city-1998-backdrop.jpg deleted file mode 100644 index 57beacc1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-sex-and-the-city-1998-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-sex-and-the-city-1998-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-sex-and-the-city-1998-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-sex-and-the-city-1998-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-silo-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-silo-2023-backdrop.jpg deleted file mode 100644 index bcc1c4b7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-silo-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-silo-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-silo-2023-poster.jpg deleted file mode 100644 index b493bc7b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-silo-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-six-feet-under-2001-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-six-feet-under-2001-backdrop.jpg deleted file mode 100644 index 4d2c02ad..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-six-feet-under-2001-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-six-feet-under-2001-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-six-feet-under-2001-poster.jpg deleted file mode 100644 index 559d6bd1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-six-feet-under-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-skymed-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-skymed-2022-backdrop.jpg deleted file mode 100644 index c7ec326b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-skymed-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-skymed-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-skymed-2022-poster.jpg deleted file mode 100644 index 2540dad7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-skymed-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg deleted file mode 100644 index feb777b2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-so-you-think-you-can-dance-2008-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-so-you-think-you-can-dance-2008-poster.jpg deleted file mode 100644 index a2e91496..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-so-you-think-you-can-dance-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-south-park-1997-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-south-park-1997-backdrop.jpg deleted file mode 100644 index cd0aec39..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-south-park-1997-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-south-park-1997-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-south-park-1997-poster.jpg deleted file mode 100644 index d8568d54..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-south-park-1997-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-spy-x-family-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-spy-x-family-2022-backdrop.jpg deleted file mode 100644 index 8764e6eb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-spy-x-family-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-spy-x-family-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-spy-x-family-2022-poster.jpg deleted file mode 100644 index fc326930..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-spy-x-family-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-100-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-100-2014-backdrop.jpg deleted file mode 100644 index 8d41cb22..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-100-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-100-2014-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-100-2014-poster.jpg deleted file mode 100644 index 4d254cb0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-100-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg deleted file mode 100644 index 53e1d593..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-abbott-and-costello-show-1952-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-abbott-and-costello-show-1952-poster.jpg deleted file mode 100644 index 7d6768ea..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-abbott-and-costello-show-1952-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-afterparty-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-afterparty-2022-backdrop.jpg deleted file mode 100644 index 72ca19db..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-afterparty-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-afterparty-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-afterparty-2022-poster.jpg deleted file mode 100644 index 9beacde2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-afterparty-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-baby-sitters-club-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-baby-sitters-club-2020-backdrop.jpg deleted file mode 100644 index d72d7c7e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-baby-sitters-club-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-baby-sitters-club-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-baby-sitters-club-2020-poster.jpg deleted file mode 100644 index 7e4e7be2..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-baby-sitters-club-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bear-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-bear-2022-backdrop.jpg deleted file mode 100644 index 9c9cca57..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bear-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bear-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-bear-2022-poster.jpg deleted file mode 100644 index bbf0ab28..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bear-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-beast-in-me-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-beast-in-me-2025-backdrop.jpg deleted file mode 100644 index e7727a79..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-beast-in-me-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-beast-in-me-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-beast-in-me-2025-poster.jpg deleted file mode 100644 index c78c5cf0..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-beast-in-me-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bold-type-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-bold-type-2017-backdrop.jpg deleted file mode 100644 index dd84deab..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bold-type-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bold-type-2017-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-bold-type-2017-poster.jpg deleted file mode 100644 index f663eb8b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-bold-type-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-boys-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-boys-2019-backdrop.jpg deleted file mode 100644 index a834d9fb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-boys-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-boys-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-boys-2019-poster.jpg deleted file mode 100644 index f67ff16f..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-boys-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-capture-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-capture-2019-backdrop.jpg deleted file mode 100644 index 42256fc1..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-capture-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-capture-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-capture-2019-poster.jpg deleted file mode 100644 index a4039011..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-capture-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-diplomat-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-diplomat-2023-backdrop.jpg deleted file mode 100644 index b25ab5f3..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-diplomat-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-diplomat-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-diplomat-2023-poster.jpg deleted file mode 100644 index 2eb48b62..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-diplomat-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-durrells-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-durrells-2016-backdrop.jpg deleted file mode 100644 index 3f8bdeec..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-durrells-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-durrells-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-durrells-2016-poster.jpg deleted file mode 100644 index d29448bb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-durrells-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-final-problem-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-final-problem-2026-backdrop.jpg deleted file mode 100644 index 61a310d8..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-final-problem-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-final-problem-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-final-problem-2026-poster.jpg deleted file mode 100644 index f0332826..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-final-problem-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-four-seasons-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-four-seasons-2025-backdrop.jpg deleted file mode 100644 index a358664c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-four-seasons-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-four-seasons-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-four-seasons-2025-poster.jpg deleted file mode 100644 index fd252088..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-four-seasons-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-get-down-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-get-down-2016-backdrop.jpg deleted file mode 100644 index 1de4a3ca..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-get-down-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-get-down-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-get-down-2016-poster.jpg deleted file mode 100644 index 7bb38abd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-get-down-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-hunting-wives-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-hunting-wives-2025-backdrop.jpg deleted file mode 100644 index d1758fef..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-hunting-wives-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-hunting-wives-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-hunting-wives-2025-poster.jpg deleted file mode 100644 index 034273eb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-hunting-wives-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-last-of-us-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-last-of-us-2023-backdrop.jpg deleted file mode 100644 index fa28e38d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-last-of-us-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-last-of-us-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-last-of-us-2023-poster.jpg deleted file mode 100644 index 7da462da..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-last-of-us-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-legend-of-korra-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-legend-of-korra-2012-backdrop.jpg deleted file mode 100644 index ab6086bd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-legend-of-korra-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-legend-of-korra-2012-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-legend-of-korra-2012-poster.jpg deleted file mode 100644 index a7086d5e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-legend-of-korra-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg deleted file mode 100644 index 0b88fd4b..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg deleted file mode 100644 index dc387fcd..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lost-flowers-of-alice-hart-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-lost-flowers-of-alice-hart-2023-backdrop.jpg deleted file mode 100644 index c5d17d97..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lost-flowers-of-alice-hart-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg deleted file mode 100644 index 71407f36..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-night-manager-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-night-manager-2016-backdrop.jpg deleted file mode 100644 index 327aa449..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-night-manager-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-night-manager-2016-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-night-manager-2016-poster.jpg deleted file mode 100644 index 7ab2dbf5..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-night-manager-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-pitt-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-pitt-2025-backdrop.jpg deleted file mode 100644 index b593444a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-pitt-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-pitt-2025-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-pitt-2025-poster.jpg deleted file mode 100644 index ad1ec8ff..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-pitt-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-tatami-galaxy-2010-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-tatami-galaxy-2010-backdrop.jpg deleted file mode 100644 index a267b4cb..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-tatami-galaxy-2010-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-tatami-galaxy-2010-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-tatami-galaxy-2010-poster.jpg deleted file mode 100644 index 5324dc75..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-tatami-galaxy-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-walk-in-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-walk-in-2022-backdrop.jpg deleted file mode 100644 index e37f8d5c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-walk-in-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-walk-in-2022-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-walk-in-2022-poster.jpg deleted file mode 100644 index 715d0c80..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-walk-in-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-white-lotus-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-white-lotus-2021-backdrop.jpg deleted file mode 100644 index 1f747323..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-white-lotus-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-white-lotus-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-white-lotus-2021-poster.jpg deleted file mode 100644 index 2f23d1fa..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-white-lotus-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-witcher-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-witcher-2019-backdrop.jpg deleted file mode 100644 index acca834e..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-witcher-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-the-witcher-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-the-witcher-2019-poster.jpg deleted file mode 100644 index f5eb8f59..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-the-witcher-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-undercover-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-undercover-2019-backdrop.jpg deleted file mode 100644 index ad972c17..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-undercover-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-undercover-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-undercover-2019-poster.jpg deleted file mode 100644 index 8611eab4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-undercover-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-upload-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-upload-2020-backdrop.jpg deleted file mode 100644 index f350a49c..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-upload-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-upload-2020-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-upload-2020-poster.jpg deleted file mode 100644 index 547d3b7a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-upload-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-vinland-saga-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-vinland-saga-2019-backdrop.jpg deleted file mode 100644 index 9ae58ec4..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-vinland-saga-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-vinland-saga-2019-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-vinland-saga-2019-poster.jpg deleted file mode 100644 index 7d0ba17a..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-vinland-saga-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-widow-s-bay-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-widow-s-bay-2026-backdrop.jpg deleted file mode 100644 index 03b1c335..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-widow-s-bay-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-widow-s-bay-2026-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-widow-s-bay-2026-poster.jpg deleted file mode 100644 index 8c02a0d7..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-widow-s-bay-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-yellowjackets-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-yellowjackets-2021-backdrop.jpg deleted file mode 100644 index 960c2d23..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-yellowjackets-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-yellowjackets-2021-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-yellowjackets-2021-poster.jpg deleted file mode 100644 index c7e16a57..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-yellowjackets-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-yu-yu-hakusho-1992-backdrop.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-yu-yu-hakusho-1992-backdrop.jpg deleted file mode 100644 index ba65ca2d..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-yu-yu-hakusho-1992-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/assets/tv-yu-yu-hakusho-1992-poster.jpg b/app/src/debug/assets/wolfskin-discover/assets/tv-yu-yu-hakusho-1992-poster.jpg deleted file mode 100644 index f5843766..00000000 Binary files a/app/src/debug/assets/wolfskin-discover/assets/tv-yu-yu-hakusho-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-discover/fixture.json b/app/src/debug/assets/wolfskin-discover/fixture.json deleted file mode 100644 index 450f0ece..00000000 --- a/app/src/debug/assets/wolfskin-discover/fixture.json +++ /dev/null @@ -1 +0,0 @@ -{"schemaVersion": 1, "generatedAt": "2026-11-04T20:42:12.030108+00:00", "profiles": [{"key": "justin", "name": "Justin", "initial": "J", "color": "#62c4ff", "destinations": {"Home": {"kind": "home"}, "Movies": {"kind": "library", "collection": "movies"}, "Shows": {"kind": "library", "collection": "shows"}, "Anime": {"kind": "library", "collection": "anime"}, "Discover": {"kind": "discover", "collection": "discover"}}, "movies": [{"id": "movie-the-girl-who-leapt-through-time-2006", "title": "The Girl Who Leapt Through Time", "year": 2006, "type": "movie", "runtimeSeconds": 5886, "seasonCount": null, "communityRating": 7.772, "criticRating": 87, "genres": ["Fantasy", "Animation", "Drama"], "overview": "When 17-year-old Makoto Konno gains the ability to 'leap' backwards through time, she immediately sets about improving her grades and preventing personal mishaps. However, she soon realises that changing the past\u2026", "poster": "assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg", "backdrop": "assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T17:39:11.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 14069, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Fresh", "rtAudience": 90, "rtAudienceLabel": "Upright", "imdb": 7.6, "imdbVotes": 76917, "tmdb": 7.775, "tmdbVotes": 2345}}, {"id": "movie-locke-2014", "title": "Locke", "year": 2014, "type": "movie", "runtimeSeconds": 5090, "seasonCount": null, "communityRating": 6.891, "criticRating": 91, "genres": ["Drama", "Thriller"], "overview": "Ivan Locke has worked hard to craft a good life for himself. Tonight, that life will collapse around him. On the eve of the biggest challenge of his career, Ivan receives a phone call that sets in motion a series of\u2026", "poster": "assets/movie-locke-2014-poster.jpg", "backdrop": "assets/movie-locke-2014-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T17:38:48.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 210479, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 72, "rtAudienceLabel": "Upright", "imdb": 7.1, "imdbVotes": 171265, "tmdb": 6.889, "tmdbVotes": 3088}}, {"id": "movie-look-who-s-back-2015", "title": "Look Who's Back", "year": 2015, "type": "movie", "runtimeSeconds": 6936, "seasonCount": null, "communityRating": 6.793, "criticRating": null, "genres": ["Comedy"], "overview": "When Adolf Hitler reawakens at the site of his former bunker in present-day Berlin, he is mistaken for a comedian and quickly becomes a media phenomenon.", "poster": "assets/movie-look-who-s-back-2015-poster.jpg", "backdrop": "assets/movie-look-who-s-back-2015-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:27:48.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 318917, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 54672, "tmdb": 6.796, "tmdbVotes": 1990}}, {"id": "movie-memories-1995", "title": "Memories", "year": 1995, "type": "movie", "runtimeSeconds": 6805, "seasonCount": null, "communityRating": 7.465, "criticRating": null, "genres": ["Fantasy", "Animation", "Science Fiction"], "overview": "Three back-to-back anime films by three different directors make up this sci-fi trilogy three years in the making.", "poster": "assets/movie-memories-1995-poster.jpg", "backdrop": "assets/movie-memories-1995-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-30T21:36:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 42994, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 82, "rtAudienceLabel": "Upright", "imdb": 7.5, "imdbVotes": 26638, "tmdb": 7.459, "tmdbVotes": 622}}, {"id": "movie-reincarnated-2012", "title": "Reincarnated", "year": 2012, "type": "movie", "runtimeSeconds": 5759, "seasonCount": null, "communityRating": 6.145, "criticRating": 28, "genres": ["Music", "Documentary"], "overview": "Evolution as an artist is often times what separates legends from the more mundane. After being heavily influenced by his experience in Jamaica \u2013 and his subsequent name change from Snoop Dogg to Snoop Lion \u2013 the LBC\u2026", "poster": "assets/movie-reincarnated-2012-poster.jpg", "backdrop": "assets/movie-reincarnated-2012-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:27:26.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 123431, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 28, "rtCriticsLabel": "Rotten", "rtAudience": 41, "rtAudienceLabel": "Spilled", "imdb": 6.2, "imdbVotes": 2563, "tmdb": 6.1, "tmdbVotes": 69}}, {"id": "movie-shipwrecked-nightmare-at-sea-2026", "title": "Shipwrecked: Nightmare at Sea", "year": 2026, "type": "movie", "runtimeSeconds": 5385, "seasonCount": null, "communityRating": 7.181, "criticRating": null, "genres": ["Documentary"], "overview": "Never-before-seen footage and survivor accounts trace the 2012 shipwreck of a luxury cruise and the disaster that ensued, in this immersive documentary.", "poster": "assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg", "backdrop": "assets/movie-shipwrecked-nightmare-at-sea-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T20:11:43.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1715492, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": 6.5, "imdbVotes": 5080, "tmdb": 7.172, "tmdbVotes": 87}}, {"id": "movie-steins-gate-the-movie-load-region-of-deja-vu-2013", "title": "Steins;Gate: The Movie - Load Region of D\u00e9j\u00e0 Vu", "year": 2013, "type": "movie", "runtimeSeconds": 5403, "seasonCount": null, "communityRating": 7.8, "criticRating": null, "genres": ["Animation", "Mystery", "Sci-Fi"], "overview": "After the events of the anime, Rintarou begins to feel the repercussions of extensive time travel, and eventually completely fades from reality. Kurisu, being the only companion to remember him, now must find a way\u2026", "poster": "assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg", "backdrop": "assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T14:55:54.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}], "shows": [{"id": "tv-andor-2022", "title": "Andor", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.293, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Action & Adventure", "Drama"], "overview": "In an era filled with danger, deception and intrigue, Cassian Andor will discover the difference he can make in the struggle against the tyrannical Galactic Empire. He embarks on a path that is destined to turn him\u2026", "poster": "assets/tv-andor-2022-poster.jpg", "backdrop": "assets/tv-andor-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0432996Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 83867, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.321, "tmdbVotes": 2312}}, {"id": "tv-the-boys-2019", "title": "The Boys", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 8.443, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Action & Adventure"], "overview": "A group of vigilantes known informally as \u201cThe Boys\u201d set out to take down corrupt superheroes with no more than blue-collar grit and a willingness to fight dirty.", "poster": "assets/tv-the-boys-2019-poster.jpg", "backdrop": "assets/tv-the-boys-2019-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0410131Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 76479, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.4, "tmdbVotes": 13628}}, {"id": "tv-californication-2007", "title": "Californication", "year": 2007, "type": "tv", "runtimeSeconds": 1680, "seasonCount": 7, "communityRating": 7.632, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "A self-loathing, alcoholic writer attempts to repair his damaged relationships with his daughter and her mother while combating sex addiction, a budding drug problem, and the seeming inability to avoid making bad\u2026", "poster": "assets/tv-californication-2007-poster.jpg", "backdrop": "assets/tv-californication-2007-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0428037Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1215, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 57, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.62, "tmdbVotes": 1406}}, {"id": "tv-firefly-2002", "title": "Firefly", "year": 2002, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.347, "criticRating": null, "genres": ["Drama", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In the year 2517, the universe is a vast frontier where lawlessness reigns and adventure awaits! Join Captain Malcolm Reynolds and his ragtag crew aboard the spaceship Serenity as they navigate the fringes of\u2026", "poster": "assets/tv-firefly-2002-poster.jpg", "backdrop": "assets/tv-firefly-2002-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T11:40:08.4202369Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1437, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 77, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.351, "tmdbVotes": 2550}}, {"id": "tv-fleabag-2016", "title": "Fleabag", "year": 2016, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.278, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "A portrait into the mind of a dry-witted, sexual, angry, porn-watching, grief-riddled woman, trying to make sense of the world. As she hurls herself headlong at modern living, Fleabag is thrown roughly up against the\u2026", "poster": "assets/tv-fleabag-2016-poster.jpg", "backdrop": "assets/tv-fleabag-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T17:42:25.4848214Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 67070, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.3, "tmdbVotes": 1993}}, {"id": "tv-gravity-falls-2012", "title": "Gravity Falls", "year": 2012, "type": "tv", "runtimeSeconds": 1320, "seasonCount": 2, "communityRating": 8.619, "criticRating": null, "genres": ["Action & Adventure", "Animation", "Comedy"], "overview": "Twin brother and sister Dipper and Mabel Pines are in for an unexpected adventure when they spend the summer helping their great uncle Stan run a tourist trap in the mysterious town of Gravity Falls, Oregon.", "poster": "assets/tv-gravity-falls-2012-poster.jpg", "backdrop": "assets/tv-gravity-falls-2012-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0435827Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 40075, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.624, "tmdbVotes": 3647}}, {"id": "tv-invincible-2021", "title": "INVINCIBLE", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 8.638, "criticRating": null, "genres": ["Animation", "Drama", "Sci-Fi & Fantasy"], "overview": "Mark Grayson is a normal teenager except for the fact that his father is the most powerful superhero on the planet. Shortly after his seventeenth birthday, Mark begins to develop powers of his own and enters into his\u2026", "poster": "assets/tv-invincible-2021-poster.jpg", "backdrop": "assets/tv-invincible-2021-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.040159Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 95557, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.6, "tmdbVotes": 6155}}, {"id": "tv-the-legend-of-korra-2012", "title": "The Legend of Korra", "year": 2012, "type": "tv", "runtimeSeconds": 1380, "seasonCount": 4, "communityRating": 8.2, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Avatar Korra, a headstrong, rebellious, feisty young woman who continually challenges and breaks with tradition, is on her quest to become a fully realized Avatar. In this story, the Avatar struggles to find balance\u2026", "poster": "assets/tv-the-legend-of-korra-2012-poster.jpg", "backdrop": "assets/tv-the-legend-of-korra-2012-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T00:19:23.2494945Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 33880, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.221, "tmdbVotes": 2482}}, {"id": "tv-love-death-robots-2019", "title": "Love, Death & Robots", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 8.205, "criticRating": null, "genres": ["Animation", "Sci-Fi & Fantasy"], "overview": "Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.", "poster": "assets/tv-love-death-robots-2019-poster.jpg", "backdrop": "assets/tv-love-death-robots-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T11:31:20.8818757Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 86831, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.203, "tmdbVotes": 4238}}, {"id": "tv-norsemen-2016", "title": "Norsemen", "year": 2016, "type": "tv", "runtimeSeconds": 1800, "seasonCount": 3, "communityRating": 7.177, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Norsemen is an epic and humorous drama series set in the Viking Age. The residents of an 8th-century Viking village experience political rivalry, social change and innovations that upend their culture and way of life.", "poster": "assets/tv-norsemen-2016-poster.jpg", "backdrop": "assets/tv-norsemen-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.034182Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 68126, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.2, "tmdbVotes": 233}}, {"id": "tv-rick-and-morty-2013", "title": "Rick and Morty", "year": 2013, "type": "tv", "runtimeSeconds": null, "seasonCount": 9, "communityRating": 8.679, "criticRating": null, "genres": ["Animation", "Comedy", "Sci-Fi & Fantasy"], "overview": "Follows a sociopathic genius scientist who drags his inherently timid grandson on adventures across the universe.", "poster": "assets/tv-rick-and-morty-2013-poster.jpg", "backdrop": "assets/tv-rick-and-morty-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0419581Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 60625, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.7, "tmdbVotes": 11548}}, {"id": "tv-secret-level-2024", "title": "Secret Level", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.411, "criticRating": null, "genres": ["Animation", "Sci-Fi & Fantasy", "Action & Adventure"], "overview": "Adult animated series of original short stories which are set within the worlds of beloved video games. Each episode serves as a gateway to a new adventure, unlocking exciting worlds from beloved gaming classics and\u2026", "poster": "assets/tv-secret-level-2024-poster.jpg", "backdrop": "assets/tv-secret-level-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0336417Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 261579, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 65, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.392, "tmdbVotes": 612}}, {"id": "tv-silo-2023", "title": "Silo", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 8.178, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Drama"], "overview": "In a ruined and toxic future, thousands live in a giant silo deep underground. After its sheriff breaks a cardinal rule and residents die mysteriously, engineer Juliette starts to uncover shocking secrets and the\u2026", "poster": "assets/tv-silo-2023-poster.jpg", "backdrop": "assets/tv-silo-2023-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0346526Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 125988, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.232, "tmdbVotes": 2881}}, {"id": "tv-south-park-1997", "title": "South Park", "year": 1997, "type": "tv", "runtimeSeconds": null, "seasonCount": 29, "communityRating": 8.336, "criticRating": null, "genres": ["Animation", "Comedy"], "overview": "Follow the misadventures of four irreverent grade-schoolers in the quiet, dysfunctional town of South Park, Colorado.", "poster": "assets/tv-south-park-1997-poster.jpg", "backdrop": "assets/tv-south-park-1997-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0456124Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 2190, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.332, "tmdbVotes": 5250}}], "anime": [{"id": "tv-bleach-2004", "title": "Bleach", "year": 2004, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 17, "communityRating": 8.363, "criticRating": null, "genres": ["Action & Adventure", "Animation", "Sci-Fi & Fantasy"], "overview": "For as long as he can remember, Ichigo Kurosaki has been able to see ghosts. But when he meets Rukia, a Soul Reaper who battles evil spirits known as Hollows, he finds his life is changed forever. Now, with a\u2026", "poster": "assets/tv-bleach-2004-poster.jpg", "backdrop": "assets/tv-bleach-2004-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0359827Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 30984, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.4, "tmdbVotes": 2331}}, {"id": "tv-chainsaw-man-2022", "title": "Chainsaw Man", "year": 2022, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 1, "communityRating": 8.515, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Denji has a simple dream\u2014to live a happy and peaceful life, spending time with a girl he likes. This is a far cry from reality, however, as Denji is forced by the yakuza into killing devils in order to pay off his\u2026", "poster": "assets/tv-chainsaw-man-2022-poster.jpg", "backdrop": "assets/tv-chainsaw-man-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2095501Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 114410, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.508, "tmdbVotes": 2364}}, {"id": "tv-cyberpunk-edgerunners-2022", "title": "Cyberpunk: Edgerunners", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.466, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In a dystopia riddled with corruption and cybernetic implants, a talented but reckless street kid strives to become a mercenary outlaw \u2014 an edgerunner.", "poster": "assets/tv-cyberpunk-edgerunners-2022-poster.jpg", "backdrop": "assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2093107Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 105248, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.507, "tmdbVotes": 2132}}, {"id": "tv-dan-da-dan-2024", "title": "Dan Da Dan", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.538, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Comedy"], "overview": "In a bet to prove whether ghosts or aliens exist, two high schoolers face terrifying paranormal threats, gain superpowers and maybe even fall in love?!", "poster": "assets/tv-dan-da-dan-2024-poster.jpg", "backdrop": "assets/tv-dan-da-dan-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2652252Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 240411, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.517, "tmdbVotes": 986}}, {"id": "tv-dr-stone-2019", "title": "Dr. STONE", "year": 2019, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 4, "communityRating": 8.541, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Comedy"], "overview": "One fateful day, all of humanity was petrified by a blinding flash of light. After several millennia, high schooler Taiju awakens and finds himself lost in a world of statues. However, he\u2019s not alone! His\u2026", "poster": "assets/tv-dr-stone-2019-poster.jpg", "backdrop": "assets/tv-dr-stone-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2091579Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 86031, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.53, "tmdbVotes": 1814}}, {"id": "tv-frieren-beyond-journey-s-end-2023", "title": "Frieren: Beyond Journey's End", "year": 2023, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 2, "communityRating": 8.803, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Drama"], "overview": "After the party of heroes defeated the Demon King, they restored peace to the land and returned to lives of solitude. Generations pass, and the elven mage Frieren comes face to face with humanity\u2019s mortality. She\u2026", "poster": "assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg", "backdrop": "assets/tv-frieren-beyond-journey-s-end-2023-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2094435Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 209867, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.826, "tmdbVotes": 1016}}, {"id": "tv-fullmetal-alchemist-brotherhood-2009", "title": "Fullmetal Alchemist: Brotherhood", "year": 2009, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 1, "communityRating": 8.7, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Disregard for alchemy\u2019s laws ripped half of Edward Elric\u2019s limbs from his body and left his brother Alphonse\u2019s soul clinging to a suit of armor. To restore what was lost, the brothers seek the Philosopher\u2019s Stone.\u2026", "poster": "assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg", "backdrop": "assets/tv-fullmetal-alchemist-brotherhood-2009-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T10:27:12.3573939Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 31911, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.7, "tmdbVotes": 2633}}, {"id": "tv-jujutsu-kaisen-2020", "title": "JUJUTSU KAISEN", "year": 2020, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 3, "communityRating": 8.583, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Yuji Itadori is a boy with tremendous physical strength, though he lives a completely ordinary high school life. One day, to save a classmate who has been attacked by curses, he eats the finger of Ryomen Sukuna,\u2026", "poster": "assets/tv-jujutsu-kaisen-2020-poster.jpg", "backdrop": "assets/tv-jujutsu-kaisen-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.207573Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 95479, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.574, "tmdbVotes": 4735}}, {"id": "tv-kaiju-no-8-2024", "title": "Kaiju No. 8", "year": 2024, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 2, "communityRating": 8.436, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In a world plagued by creatures known as Kaiju, Kafka Hibino aspired to enlist in The Defense Force. He makes a promise to enlist with his childhood friend, Mina Ashiro. Soon, life takes them in separate ways. While\u2026", "poster": "assets/tv-kaiju-no-8-2024-poster.jpg", "backdrop": "assets/tv-kaiju-no-8-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2646566Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 207468, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.4, "tmdbVotes": 836}}, {"id": "tv-my-hero-academia-2016", "title": "My Hero Academia", "year": 2016, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 8, "communityRating": 8.607, "criticRating": null, "genres": ["Action & Adventure", "Animation", "Sci-Fi & Fantasy"], "overview": "After he saves a bully from a Villain, a normal student is granted a superpower that allows him to attend a high school training academy for Heroes.", "poster": "assets/tv-my-hero-academia-2016-poster.jpg", "backdrop": "assets/tv-my-hero-academia-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2089315Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 65930, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.6, "tmdbVotes": 5426}}, {"id": "tv-neon-genesis-evangelion-1995", "title": "Neon Genesis Evangelion", "year": 1995, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 1, "communityRating": 8.568, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Animation", "Drama"], "overview": "At the turn of the century, the Angels returned to Earth, seeking to wipe out humanity in an apocalyptic fury. Devastated, mankind's last remnants moved underground to wait for the day when the Angels would come back\u2026", "poster": "assets/tv-neon-genesis-evangelion-1995-poster.jpg", "backdrop": "assets/tv-neon-genesis-evangelion-1995-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0340629Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 890, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.56, "tmdbVotes": 2205}}, {"id": "tv-record-of-ragnarok-2021", "title": "Record of Ragnarok", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 8.402, "criticRating": null, "genres": ["Action & Adventure", "Animation", "Sci-Fi & Fantasy"], "overview": "Before eradicating humankind from the world, the gods give them one last chance to prove themselves worthy of survival. Let the Ragnarok battles begin.", "poster": "assets/tv-record-of-ragnarok-2021-poster.jpg", "backdrop": "assets/tv-record-of-ragnarok-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2085766Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 114868, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.403, "tmdbVotes": 1929}}, {"id": "tv-sakamoto-days-2025", "title": "SAKAMOTO DAYS", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.071, "criticRating": null, "genres": ["Animation", "Comedy", "Action & Adventure"], "overview": "Once the greatest hitman of all, Taro Sakamoto retired in the name of love. But when his past catches up, he must fight to protect his beloved family.", "poster": "assets/tv-sakamoto-days-2025-poster.jpg", "backdrop": "assets/tv-sakamoto-days-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.266066Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 207332, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.057, "tmdbVotes": 375}}, {"id": "tv-sentenced-to-be-a-hero-2025", "title": "Sentenced to Be a Hero", "year": 2025, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 1, "communityRating": 8.338, "criticRating": null, "genres": ["Animation", "Sci-Fi & Fantasy", "Action & Adventure"], "overview": "In a world where heroism is a punishment, Xylo Forbartz, a condemned goddess killer, battles endless hordes of monstrous abominations as part of Penal Hero Unit 9004. Death is no escape, only a cycle of resurrection\u2026", "poster": "assets/tv-sentenced-to-be-a-hero-2025-poster.jpg", "backdrop": "assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2666791Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 249907, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.244, "tmdbVotes": 129}}, {"id": "tv-spy-x-family-2022", "title": "SPY x FAMILY", "year": 2022, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 3, "communityRating": 8.506, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Comedy"], "overview": "A spy, an assassin and a telepath come together to pose as a family, each for their own reasons, while hiding their true identities from each other.", "poster": "assets/tv-spy-x-family-2022-poster.jpg", "backdrop": "assets/tv-spy-x-family-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2670364Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 120089, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.5, "tmdbVotes": 2382}}, {"id": "tv-the-tatami-galaxy-2010", "title": "The Tatami Galaxy", "year": 2010, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 2, "communityRating": 8.271, "criticRating": null, "genres": ["Animation", "Comedy", "Drama"], "overview": "When a college dropout stops for a late night bite at a mysterious ramen stand, he crosses paths with a self-proclaimed deity of matrimony. This bizarre meeting sends the young man hurtling through a horrifying\u2026", "poster": "assets/tv-the-tatami-galaxy-2010-poster.jpg", "backdrop": "assets/tv-the-tatami-galaxy-2010-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-18T05:52:51.0023256Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 36243, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.3, "tmdbVotes": 152}}, {"id": "tv-vinland-saga-2019", "title": "Vinland Saga", "year": 2019, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 2, "communityRating": 8.527, "criticRating": null, "genres": ["Animation", "War & Politics", "Action & Adventure"], "overview": "For a thousand years, the Vikings have made quite a name and reputation for themselves as the strongest families with a thirst for violence. Thorfinn, the son of one of the Vikings' greatest warriors, spends his\u2026", "poster": "assets/tv-vinland-saga-2019-poster.jpg", "backdrop": "assets/tv-vinland-saga-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.207389Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 88803, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.509, "tmdbVotes": 1064}}, {"id": "tv-yu-yu-hakusho-1992", "title": "Yu Yu Hakusho", "year": 1992, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 4, "communityRating": 8.5, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "After dying to save a boy, delinquent tough guy Yusuke Urameshi is granted another chance at life by redeeming himself as a \"Spirit Detective.\"", "poster": "assets/tv-yu-yu-hakusho-1992-poster.jpg", "backdrop": "assets/tv-yu-yu-hakusho-1992-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-19T21:46:01.5733049Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 30669, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.475, "tmdbVotes": 519}}], "resume": [], "nextUp": [{"id": "episode-three-monsters-1992-s1e6", "title": "Three Monsters", "year": 1992, "type": "episode", "runtimeSeconds": 1421, "seasonCount": null, "communityRating": 7.4, "criticRating": null, "genres": ["Animation", "Action", "Adventure"], "overview": "Yusuke receives his first real case as Spirit Detective. Three demon thieves have muscled their way into King Yama's sacred vault, stealing the three Artifacts of Darkness. Yusuke must recover the artifacts and bring\u2026", "poster": "assets/episode-three-monsters-1992-s1e6-poster.jpg", "backdrop": "assets/episode-three-monsters-1992-s1e6-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-19T21:59:21.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Yu Yu Hakusho", "seasonNumber": 1, "episodeNumber": 6, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-all-hands-on-deck-class-1-a-2021-s5e1", "title": "All Hands on Deck! Class 1-A", "year": 2021, "type": "episode", "runtimeSeconds": 1433, "seasonCount": null, "communityRating": 6.938, "criticRating": null, "genres": ["Animation", "Action", "Adventure"], "overview": "There's an emergency drill at U.A. High School, and Class 1-A is requested to report for duty!", "poster": "assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg", "backdrop": "assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg", "played": false, "playCount": 2, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T04:04:50.0000000Z", "lastPlayedAt": "2026-10-19T17:47:09.3028025Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "My Hero Academia", "seasonNumber": 5, "episodeNumber": 1, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-erickerhead-2026-s9e6", "title": "Erickerhead", "year": 2026, "type": "episode", "runtimeSeconds": 1357, "seasonCount": null, "communityRating": 8.196, "criticRating": null, "genres": [], "overview": "Rick's stomach is mad, broh. But it's all-you-can-eat-and-drink here, broh!", "poster": "assets/episode-erickerhead-2026-s9e6-poster.jpg", "backdrop": "assets/episode-erickerhead-2026-s9e6-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-28T10:23:23.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Rick and Morty", "seasonNumber": 9, "episodeNumber": 6, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-girls-interrupted-2007-s1e7", "title": "Girls, Interrupted", "year": 2007, "type": "episode", "runtimeSeconds": 1642, "seasonCount": null, "communityRating": 7.4, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Hank is disturbed by Becca's negative reaction towards the concept of happy endings, even more so when he finds out that it is partially his fault.\nMarcy comes up with the idea of a threesome with Charlie and Dani,\u2026", "poster": "assets/episode-girls-interrupted-2007-s1e7-poster.jpg", "backdrop": "assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-05T02:46:18.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Californication", "seasonNumber": 1, "episodeNumber": 7, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-announcement-2022-s1e7", "title": "Announcement", "year": 2022, "type": "episode", "runtimeSeconds": 3012, "seasonCount": null, "communityRating": 7.8, "criticRating": null, "genres": ["Action", "Adventure", "Drama"], "overview": "The repercussions of the Aldhani heist reach all the way to Coruscant, while Cassian returns home.", "poster": "assets/episode-announcement-2022-s1e7-poster.jpg", "backdrop": "assets/episode-announcement-2022-s1e7-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T12:56:24.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Andor", "seasonNumber": 1, "episodeNumber": 7, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-south-american-biker-gangs-2026-s29e1", "title": "South American Biker Gangs", "year": 2026, "type": "episode", "runtimeSeconds": 1335, "seasonCount": null, "communityRating": 6.4, "criticRating": null, "genres": ["Animation", "Comedy"], "overview": "When e-bikes arrive in South America, the boys take over the streets and the parents of South America can hardly recognize South America.", "poster": "assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg", "backdrop": "assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-01T06:40:46.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "South Park", "seasonNumber": 29, "episodeNumber": 1, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}], "recentlyAdded": [{"id": "movie-steins-gate-the-movie-load-region-of-deja-vu-2013", "title": "Steins;Gate: The Movie - Load Region of D\u00e9j\u00e0 Vu", "year": 2013, "type": "movie", "runtimeSeconds": 5403, "seasonCount": null, "communityRating": 7.8, "criticRating": null, "genres": ["Animation", "Mystery", "Sci-Fi"], "overview": "After the events of the anime, Rintarou begins to feel the repercussions of extensive time travel, and eventually completely fades from reality. Kurisu, being the only companion to remember him, now must find a way\u2026", "poster": "assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg", "backdrop": "assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T14:55:54.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "tv-yu-yu-hakusho-1992", "title": "Yu Yu Hakusho", "year": 1992, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 4, "communityRating": 8.5, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "After dying to save a boy, delinquent tough guy Yusuke Urameshi is granted another chance at life by redeeming himself as a \"Spirit Detective.\"", "poster": "assets/tv-yu-yu-hakusho-1992-poster.jpg", "backdrop": "assets/tv-yu-yu-hakusho-1992-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-19T21:46:01.5733049Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 30669, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.475, "tmdbVotes": 519}}, {"id": "movie-memories-1995", "title": "Memories", "year": 1995, "type": "movie", "runtimeSeconds": 6805, "seasonCount": null, "communityRating": 7.465, "criticRating": null, "genres": ["Fantasy", "Animation", "Science Fiction"], "overview": "Three back-to-back anime films by three different directors make up this sci-fi trilogy three years in the making.", "poster": "assets/movie-memories-1995-poster.jpg", "backdrop": "assets/movie-memories-1995-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-30T21:36:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 42994, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 82, "rtAudienceLabel": "Upright", "imdb": 7.5, "imdbVotes": 26638, "tmdb": 7.459, "tmdbVotes": 622}}, {"id": "tv-firefly-2002", "title": "Firefly", "year": 2002, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.347, "criticRating": null, "genres": ["Drama", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In the year 2517, the universe is a vast frontier where lawlessness reigns and adventure awaits! Join Captain Malcolm Reynolds and his ragtag crew aboard the spaceship Serenity as they navigate the fringes of\u2026", "poster": "assets/tv-firefly-2002-poster.jpg", "backdrop": "assets/tv-firefly-2002-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T11:40:08.4202369Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1437, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 77, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.351, "tmdbVotes": 2550}}, {"id": "tv-love-death-robots-2019", "title": "Love, Death & Robots", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 8.205, "criticRating": null, "genres": ["Animation", "Sci-Fi & Fantasy"], "overview": "Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.", "poster": "assets/tv-love-death-robots-2019-poster.jpg", "backdrop": "assets/tv-love-death-robots-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T11:31:20.8818757Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 86831, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.203, "tmdbVotes": 4238}}, {"id": "tv-the-legend-of-korra-2012", "title": "The Legend of Korra", "year": 2012, "type": "tv", "runtimeSeconds": 1380, "seasonCount": 4, "communityRating": 8.2, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Avatar Korra, a headstrong, rebellious, feisty young woman who continually challenges and breaks with tradition, is on her quest to become a fully realized Avatar. In this story, the Avatar struggles to find balance\u2026", "poster": "assets/tv-the-legend-of-korra-2012-poster.jpg", "backdrop": "assets/tv-the-legend-of-korra-2012-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T00:19:23.2494945Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 33880, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.221, "tmdbVotes": 2482}}, {"id": "tv-spy-x-family-2022", "title": "SPY x FAMILY", "year": 2022, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 3, "communityRating": 8.506, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Comedy"], "overview": "A spy, an assassin and a telepath come together to pose as a family, each for their own reasons, while hiding their true identities from each other.", "poster": "assets/tv-spy-x-family-2022-poster.jpg", "backdrop": "assets/tv-spy-x-family-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2670364Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 120089, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.5, "tmdbVotes": 2382}}, {"id": "tv-sentenced-to-be-a-hero-2025", "title": "Sentenced to Be a Hero", "year": 2025, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 1, "communityRating": 8.338, "criticRating": null, "genres": ["Animation", "Sci-Fi & Fantasy", "Action & Adventure"], "overview": "In a world where heroism is a punishment, Xylo Forbartz, a condemned goddess killer, battles endless hordes of monstrous abominations as part of Penal Hero Unit 9004. Death is no escape, only a cycle of resurrection\u2026", "poster": "assets/tv-sentenced-to-be-a-hero-2025-poster.jpg", "backdrop": "assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2666791Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 249907, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.244, "tmdbVotes": 129}}, {"id": "tv-sakamoto-days-2025", "title": "SAKAMOTO DAYS", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.071, "criticRating": null, "genres": ["Animation", "Comedy", "Action & Adventure"], "overview": "Once the greatest hitman of all, Taro Sakamoto retired in the name of love. But when his past catches up, he must fight to protect his beloved family.", "poster": "assets/tv-sakamoto-days-2025-poster.jpg", "backdrop": "assets/tv-sakamoto-days-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.266066Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 207332, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.057, "tmdbVotes": 375}}, {"id": "tv-dan-da-dan-2024", "title": "Dan Da Dan", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.538, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Comedy"], "overview": "In a bet to prove whether ghosts or aliens exist, two high schoolers face terrifying paranormal threats, gain superpowers and maybe even fall in love?!", "poster": "assets/tv-dan-da-dan-2024-poster.jpg", "backdrop": "assets/tv-dan-da-dan-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2652252Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 240411, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.517, "tmdbVotes": 986}}, {"id": "tv-kaiju-no-8-2024", "title": "Kaiju No. 8", "year": 2024, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 2, "communityRating": 8.436, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In a world plagued by creatures known as Kaiju, Kafka Hibino aspired to enlist in The Defense Force. He makes a promise to enlist with his childhood friend, Mina Ashiro. Soon, life takes them in separate ways. While\u2026", "poster": "assets/tv-kaiju-no-8-2024-poster.jpg", "backdrop": "assets/tv-kaiju-no-8-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-22T10:31:16.2646566Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 207468, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.4, "tmdbVotes": 836}}, {"id": "tv-chainsaw-man-2022", "title": "Chainsaw Man", "year": 2022, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 1, "communityRating": 8.515, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Denji has a simple dream\u2014to live a happy and peaceful life, spending time with a girl he likes. This is a far cry from reality, however, as Denji is forced by the yakuza into killing devils in order to pay off his\u2026", "poster": "assets/tv-chainsaw-man-2022-poster.jpg", "backdrop": "assets/tv-chainsaw-man-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T22:28:13.2095501Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 114410, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.508, "tmdbVotes": 2364}}], "discover": [{"id": "movie-the-amazing-spider-man-2012", "title": "The Amazing Spider-Man", "year": 2012, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "A teenage Peter Parker grapples with both high school and amazing super-human crises as his alter-ego Spider-Man.", "poster": "assets/movie-the-amazing-spider-man-2012-poster.jpg", "backdrop": "assets/movie-the-amazing-spider-man-2012-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1930, "ratings": {"rtCritics": 71, "rtCriticsLabel": "Certified Fresh", "rtAudience": 77, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 791938, "tmdb": 6.768, "tmdbVotes": 19312}}, {"id": "movie-the-amazing-spider-man-2-2014", "title": "The Amazing Spider-Man 2", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026", "poster": "assets/movie-the-amazing-spider-man-2-2014-poster.jpg", "backdrop": "assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 102382, "ratings": {"rtCritics": 51, "rtCriticsLabel": "Rotten", "rtAudience": 64, "rtAudienceLabel": "Upright", "imdb": 6.6, "imdbVotes": 611981, "tmdb": 6.567, "tmdbVotes": 14919}}, {"id": "movie-the-animatrix-2003", "title": "The Animatrix", "year": 2003, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Science Fiction"], "overview": "Straight from the creators of the groundbreaking Matrix trilogy, this collection of short animated films from the world's leading anime directors fuses computer graphics and Japanese anime to provide the background\u2026", "poster": "assets/movie-the-animatrix-2003-poster.jpg", "backdrop": "assets/movie-the-animatrix-2003-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 55931, "ratings": {"rtCritics": 86, "rtCriticsLabel": "Fresh", "rtAudience": 78, "rtAudienceLabel": "Upright", "imdb": 7.3, "imdbVotes": 91929, "tmdb": 7.221, "tmdbVotes": 1872}}, {"id": "movie-ant-man-and-the-wasp-quantumania-2023", "title": "Ant-Man and the Wasp: Quantumania", "year": 2023, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026", "poster": "assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg", "backdrop": "assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 640146, "ratings": {"rtCritics": 46, "rtCriticsLabel": "Rotten", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 6, "imdbVotes": 281738, "tmdb": 6.197, "tmdbVotes": 6236}}, {"id": "movie-appleseed-alpha-2014", "title": "Appleseed Alpha", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Science Fiction", "Action"], "overview": "The Third World War (2099\u20132126) devastated part of the Earth. Former nations are reforming while new powers emerge, and groups of humans struggle to survive in ruined cities. The story follows Deunan Knute and her\u2026", "poster": "assets/movie-appleseed-alpha-2014-poster.jpg", "backdrop": "assets/movie-appleseed-alpha-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "center_crop", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 269650, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 59, "rtAudienceLabel": "Spilled", "imdb": 6.5, "imdbVotes": 9689, "tmdb": 6.808, "tmdbVotes": 436}}, {"id": "movie-aquaman-and-the-lost-kingdom-2023", "title": "Aquaman and the Lost Kingdom", "year": 2023, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Fantasy"], "overview": "Black Manta seeks revenge on Aquaman for his father's death. Wielding the Black Trident's power, he becomes a formidable foe. To defend Atlantis, Arthur (Aquaman) forges an alliance with his imprisoned brother. They\u2026", "poster": "assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg", "backdrop": "assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 572802, "ratings": {"rtCritics": 33, "rtCriticsLabel": "Rotten", "rtAudience": 79, "rtAudienceLabel": "Upright", "imdb": 5.6, "imdbVotes": 122422, "tmdb": 6.48, "tmdbVotes": 3748}}, {"id": "movie-the-avengers-2012", "title": "The Avengers", "year": 2012, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Science Fiction", "Action", "Adventure"], "overview": "When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026", "poster": "assets/movie-the-avengers-2012-poster.jpg", "backdrop": "assets/movie-the-avengers-2012-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 24428, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Certified Fresh", "rtAudience": 91, "rtAudienceLabel": "Upright", "imdb": 8, "imdbVotes": 1590693, "tmdb": 8.084, "tmdbVotes": 40262}}, {"id": "movie-avengers-confidential-black-widow-punisher-2014", "title": "Avengers Confidential: Black Widow & Punisher", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Science Fiction", "Action"], "overview": "When the Punisher takes out a black-market weapons dealer, he stumbles upon a far-reaching terrorist plot devised by a group known as Leviathan.", "poster": "assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg", "backdrop": "assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 257346, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 55, "rtAudienceLabel": "Spilled", "imdb": 5.7, "imdbVotes": 5166, "tmdb": 6.368, "tmdbVotes": 303}}, {"id": "movie-avengers-age-of-ultron-2015", "title": "Avengers: Age of Ultron", "year": 2015, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth\u2019s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron\u2026", "poster": "assets/movie-avengers-age-of-ultron-2015-poster.jpg", "backdrop": "assets/movie-avengers-age-of-ultron-2015-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 99861, "ratings": {"rtCritics": 75, "rtCriticsLabel": "Certified Fresh", "rtAudience": 82, "rtAudienceLabel": "Upright", "imdb": 7.3, "imdbVotes": 1025876, "tmdb": 7.278, "tmdbVotes": 25289}}, {"id": "movie-avengers-infinity-war-2018", "title": "Avengers: Infinity War", "year": 2018, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Adventure", "Action", "Science Fiction"], "overview": "As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his\u2026", "poster": "assets/movie-avengers-infinity-war-2018-poster.jpg", "backdrop": "assets/movie-avengers-infinity-war-2018-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 299536, "ratings": {"rtCritics": 85, "rtCriticsLabel": "Certified Fresh", "rtAudience": 92, "rtAudienceLabel": "Upright", "imdb": 8.4, "imdbVotes": 1421954, "tmdb": 8.246, "tmdbVotes": 33331}}, {"id": "movie-batman-ninja-2018", "title": "Batman Ninja", "year": 2018, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action", "Science Fiction"], "overview": "Batman, along with many of his allies and adversaries, finds himself transported to feudal Japan by Gorilla Grodd's time displacement machine.", "poster": "assets/movie-batman-ninja-2018-poster.jpg", "backdrop": "assets/movie-batman-ninja-2018-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 485942, "ratings": {"rtCritics": 84, "rtCriticsLabel": "Fresh", "rtAudience": 60, "rtAudienceLabel": "Upright", "imdb": 5.6, "imdbVotes": 24579, "tmdb": 5.9, "tmdbVotes": 1106}}, {"id": "movie-batman-v-superman-dawn-of-justice-2016", "title": "Batman v Superman: Dawn of Justice", "year": 2016, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Fantasy"], "overview": "Fearing the actions of a god-like Super Hero left unchecked, Gotham City\u2019s own formidable, forceful vigilante takes on Metropolis\u2019s most revered, modern-day savior, while the world wrestles with what sort of hero it\u2026", "poster": "assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg", "backdrop": "assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 209112, "ratings": {"rtCritics": 28, "rtCriticsLabel": "Rotten", "rtAudience": 63, "rtAudienceLabel": "Upright", "imdb": 6.4, "imdbVotes": 801382, "tmdb": 6.014, "tmdbVotes": 19527}}, {"id": "movie-batman-the-dark-knight-returns-part-1-2012", "title": "Batman: The Dark Knight Returns, Part 1", "year": 2012, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Science Fiction", "Action", "Animation"], "overview": "Batman has not been seen for ten years. A new breed of criminal ravages Gotham City, forcing 55-year-old Bruce Wayne back into the cape and cowl. But, does he still have what it takes to fight crime in a new era?", "poster": "assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg", "backdrop": "assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 123025, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": 93, "rtAudienceLabel": "Upright", "imdb": 7.9, "imdbVotes": 68943, "tmdb": 7.742, "tmdbVotes": 1766}}, {"id": "movie-black-adam-2022", "title": "Black Adam", "year": 2022, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods\u2014and imprisoned just as quickly\u2014Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the\u2026", "poster": "assets/movie-black-adam-2022-poster.jpg", "backdrop": "assets/movie-black-adam-2022-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 436270, "ratings": {"rtCritics": 39, "rtCriticsLabel": "Rotten", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 6.1, "imdbVotes": 309624, "tmdb": 6.747, "tmdbVotes": 7370}}, {"id": "movie-black-panther-2018", "title": "Black Panther", "year": 2018, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026", "poster": "assets/movie-black-panther-2018-poster.jpg", "backdrop": "assets/movie-black-panther-2018-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 284054, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Certified Fresh", "rtAudience": null, "rtAudienceLabel": "Spilled", "imdb": 7.3, "imdbVotes": 931120, "tmdb": 7.36, "tmdbVotes": 24190}}, {"id": "movie-black-panther-wakanda-forever-2022", "title": "Black Panther: Wakanda Forever", "year": 2022, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Queen Ramonda, Shuri, M\u2019Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T\u2019Challa\u2019s death. As the Wakandans strive to embrace their next chapter, the\u2026", "poster": "assets/movie-black-panther-wakanda-forever-2022-poster.jpg", "backdrop": "assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 505642, "ratings": {"rtCritics": 84, "rtCriticsLabel": "Certified Fresh", "rtAudience": 93, "rtAudienceLabel": "Upright", "imdb": 6.6, "imdbVotes": 405088, "tmdb": 6.984, "tmdbVotes": 7920}}, {"id": "movie-blame-2017", "title": "BLAME!", "year": 2017, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Drama", "Science Fiction"], "overview": "In the distant technological future, civilization has reached its ultimate Net-based form. An \"infection\" in the past caused the automated systems to spiral out of order, resulting in a multi-leveled city structure\u2026", "poster": "assets/movie-blame-2017-poster.jpg", "backdrop": "assets/movie-blame-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 409421, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 61, "rtAudienceLabel": "Upright", "imdb": 6.6, "imdbVotes": 10258, "tmdb": 6.523, "tmdbVotes": 370}}, {"id": "movie-bleach-the-movie-fade-to-black-2008", "title": "Bleach the Movie: Fade to Black", "year": 2008, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Animation", "Fantasy"], "overview": "A strange disturbance in the World of the Living sends Ichigo and Kon to the Soul Society, where the Seireitei has been devastated by an explosion of reishi and Rukia is missing. Stranger still, none of the Soul\u2026", "poster": "assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg", "backdrop": "assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 17165, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 67, "rtAudienceLabel": "Upright", "imdb": 7.1, "imdbVotes": 3693, "tmdb": 7.1, "tmdbVotes": 206}}, {"id": "tv-90-day-fiance-before-the-90-days-2017", "title": "90 Day Fianc\u00e9: Before the 90 Days", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026", "poster": "assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg", "backdrop": "assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 73319, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 45}}, {"id": "tv-90-day-fiance-the-other-way-2019", "title": "90 Day Fianc\u00e9: The Other Way", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?", "poster": "assets/tv-90-day-fiance-the-other-way-2019-poster.jpg", "backdrop": "assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 90046, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 34}}, {"id": "tv-the-100-2014", "title": "The 100", "year": 2014, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Action & Adventure", "Drama"], "overview": "100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026", "poster": "assets/tv-the-100-2014-poster.jpg", "backdrop": "assets/tv-the-100-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 48866, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.878, "tmdbVotes": 8855}}, {"id": "tv-the-abbott-and-costello-show-1952", "title": "The Abbott and Costello Show", "year": 1952, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy"], "overview": "Bud and Lou are unemployed actors living in Mr. Fields\u2019 boarding house. Lou\u2019s girlfriend Hillary lives across the hall. Many situations arise leading to slapstick and puns.", "poster": "assets/tv-the-abbott-and-costello-show-1952-poster.jpg", "backdrop": "assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 11821, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.2, "tmdbVotes": 22}}, {"id": "tv-accel-world-2012", "title": "Accel World", "year": 2012, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Drama"], "overview": "A short, overweight student uses the Neuro Link, an evolution of the Nerve Gear, to escape himself from the reality. But his skills in the virtual world brings him to the attention of Kuroyukihime, a popular student\u2026", "poster": "assets/tv-accel-world-2012-poster.jpg", "backdrop": "assets/tv-accel-world-2012-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "center_crop", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 43167, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.429, "tmdbVotes": 163}}, {"id": "tv-ahsoka-2023", "title": "Ahsoka", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Action & Adventure", "Drama"], "overview": "Former Jedi Knight Ahsoka Tano investigates an emerging threat to a vulnerable galaxy.", "poster": "assets/tv-ahsoka-2023-poster.jpg", "backdrop": "assets/tv-ahsoka-2023-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 114461, "ratings": {"rtCritics": 85, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.4, "tmdbVotes": 1351}}, {"id": "tv-american-dad-2005", "title": "American Dad!", "year": 2005, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Comedy"], "overview": "The series focuses on an eccentric motley crew that is the Smith family and their three housemates: Father, husband, and breadwinner Stan Smith; his better half housewife, Francine Smith; their college-aged daughter,\u2026", "poster": "assets/tv-american-dad-2005-poster.jpg", "backdrop": "assets/tv-american-dad-2005-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1433, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.022, "tmdbVotes": 2481}}, {"id": "tv-american-gods-2017", "title": "American Gods", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Mystery", "Sci-Fi & Fantasy"], "overview": "An ex-con becomes the traveling partner of a conman who turns out to be one of the older gods trying to recruit troops to battle the upstart deities. Based on Neil Gaiman's fantasy novel.", "poster": "assets/tv-american-gods-2017-poster.jpg", "backdrop": "assets/tv-american-gods-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 46639, "ratings": {"rtCritics": 75, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.1, "tmdbVotes": 1852}}, {"id": "tv-andromeda-2000", "title": "Andromeda", "year": 2000, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action & Adventure", "Drama", "Sci-Fi & Fantasy"], "overview": "Captain Dylan Hunt and his crew quest to restore a government that once presided over an extended peace and prosperity.", "poster": "assets/tv-andromeda-2000-poster.jpg", "backdrop": "assets/tv-andromeda-2000-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 2985, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.068, "tmdbVotes": 315}}, {"id": "tv-aqua-teen-hunger-force-2000", "title": "Aqua Teen Hunger Force", "year": 2000, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Comedy"], "overview": "The surreal adventures of three anthropomorphic fast food items: Master Shake, Frylock and Meatwad, and their human nextdoor neighbor, Carl Brutananadilewski.", "poster": "assets/tv-aqua-teen-hunger-force-2000-poster.jpg", "backdrop": "assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 251, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 272}}, {"id": "tv-arifureta-from-commonplace-to-world-s-strongest-2019", "title": "Arifureta: From Commonplace to World's Strongest", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "Hajime Nagumo and his high school class are suddenly summoned to a fantastical land as heroes. But while most of his classmates have powerful stats and abilities, Hajime does not. Underappreciated and unprepared, he\u2026", "poster": "assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg", "backdrop": "assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 86034, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.382, "tmdbVotes": 662}}, {"id": "tv-arrow-2012", "title": "Arrow", "year": 2012, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama", "Action & Adventure"], "overview": "Spoiled billionaire playboy Oliver Queen is missing and presumed dead when his yacht is lost at sea. He returns five years later a changed man, determined to clean up the city as a hooded vigilante armed with a bow.", "poster": "assets/tv-arrow-2012-poster.jpg", "backdrop": "assets/tv-arrow-2012-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1412, "ratings": {"rtCritics": 86, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.834, "tmdbVotes": 6537}}, {"id": "tv-avatar-the-last-airbender-2024", "title": "Avatar: The Last Airbender", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy", "Drama"], "overview": "A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.", "poster": "assets/tv-avatar-the-last-airbender-2024-poster.jpg", "backdrop": "assets/tv-avatar-the-last-airbender-2024-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 82452, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 1406}}, {"id": "tv-avatar-seven-havens-2026", "title": "Avatar: Seven Havens", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026", "poster": "assets/tv-avatar-seven-havens-2026-poster.jpg", "backdrop": "assets/tv-avatar-seven-havens-2026-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 284833, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.9, "tmdbVotes": 183}}, {"id": "tv-batman-the-brave-and-the-bold-2008", "title": "Batman: The Brave and the Bold", "year": 2008, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.", "poster": "assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg", "backdrop": "assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 15804, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.6, "tmdbVotes": 295}}, {"id": "tv-batwoman-2019", "title": "Batwoman", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Action & Adventure"], "overview": "Kate Kane, armed with a passion for social justice and a flair for speaking her mind, soars onto the streets of Gotham as Batwoman, an out lesbian and highly trained street fighter primed to snuff out the failing\u2026", "poster": "assets/tv-batwoman-2019-poster.jpg", "backdrop": "assets/tv-batwoman-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 89247, "ratings": {"rtCritics": 83, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.8, "tmdbVotes": 1619}}, {"id": "tv-big-mouth-2017", "title": "Big Mouth", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Animation"], "overview": "Teenage friends find their lives upended by the wonders and horrors of puberty in this edgy comedy from real-life pals Nick Kroll and Andrew Goldberg.", "poster": "assets/tv-big-mouth-2017-poster.jpg", "backdrop": "assets/tv-big-mouth-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 74204, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.591, "tmdbVotes": 1274}}, {"id": "tv-black-lightning-2018", "title": "Black Lightning", "year": 2018, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy", "Drama"], "overview": "Jefferson Pierce is a man wrestling with a secret. As the father of two daughters and principal of a charter high school that also serves as a safe haven for young people in a New Orleans neighborhood overrun by gang\u2026", "poster": "assets/tv-black-lightning-2018-poster.jpg", "backdrop": "assets/tv-black-lightning-2018-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 71663, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.907, "tmdbVotes": 991}}], "discoverySource": {"state": "materialized_snapshot", "rankKnown": false, "selection": "First 18 alphabetically indexed movie stubs and first 18 series stubs, not ranked feed order", "observedAt": "2026-11-04T20:42:12.030108+00:00"}}, {"key": "mindel", "name": "Mindel", "initial": "M", "color": "#ff9d72", "destinations": {"Home": {"kind": "home"}, "Movies": {"kind": "library", "collection": "movies"}, "Shows": {"kind": "library", "collection": "shows"}, "Discover": {"kind": "discover", "collection": "discover"}}, "movies": [{"id": "movie-chicago-2002", "title": "Chicago", "year": 2002, "type": "movie", "runtimeSeconds": 6808, "seasonCount": null, "communityRating": 7.131, "criticRating": 87, "genres": ["Comedy", "Crime", "Drama"], "overview": "Murderesses Velma Kelly and Roxie Hart find themselves on death row together and fight for the fame that will keep them from the gallows in 1920s Chicago.", "poster": "assets/movie-chicago-2002-poster.jpg", "backdrop": "assets/movie-chicago-2002-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:53.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1574, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 83, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 259272, "tmdb": 7.135, "tmdbVotes": 3025}}, {"id": "movie-coherence-2014", "title": "Coherence", "year": 2014, "type": "movie", "runtimeSeconds": 5286, "seasonCount": null, "communityRating": 7.21, "criticRating": 89, "genres": ["Thriller", "Science Fiction"], "overview": "Four couples gather for dinner the night a mysterious and powerful comet passes overhead.", "poster": "assets/movie-coherence-2014-poster.jpg", "backdrop": "assets/movie-coherence-2014-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2017-08-12T23:49:00.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 220289, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 7.1, "imdbVotes": 180354, "tmdb": 7.208, "tmdbVotes": 3702}}, {"id": "movie-the-killing-of-a-sacred-deer-2017", "title": "The Killing of a Sacred Deer", "year": 2017, "type": "movie", "runtimeSeconds": 7262, "seasonCount": null, "communityRating": 7.003, "criticRating": 79, "genres": ["Drama", "Thriller", "Mystery"], "overview": "Dr. Steven Murphy is a renowned cardiovascular surgeon who presides over a spotless household with his wife and two children. Lurking at the margins of his idyllic suburban existence is Martin, a fatherless teen who\u2026", "poster": "assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg", "backdrop": "assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T21:49:26.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 399057, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 79, "rtCriticsLabel": "Certified Fresh", "rtAudience": 63, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 219488, "tmdb": 7.001, "tmdbVotes": 4640}}, {"id": "movie-night-is-short-walk-on-girl-2017", "title": "Night Is Short, Walk On Girl", "year": 2017, "type": "movie", "runtimeSeconds": 5548, "seasonCount": null, "communityRating": 7.421, "criticRating": 90, "genres": ["Comedy", "Romance", "Animation"], "overview": "As a group of university students go out for a night on the town, a sophomore known only as \"The Girl with Black Hair\" experiences a series of surreal encounters with the local nightlife \u2013 all the while unaware of\u2026", "poster": "assets/movie-night-is-short-walk-on-girl-2017-poster.jpg", "backdrop": "assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T17:39:14.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 430214, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Fresh", "rtAudience": 84, "rtAudienceLabel": "Upright", "imdb": 7.5, "imdbVotes": 7640, "tmdb": 7.4, "tmdbVotes": 267}}, {"id": "movie-thelma-louise-1991", "title": "Thelma & Louise", "year": 1991, "type": "movie", "runtimeSeconds": 7786, "seasonCount": null, "communityRating": 7.596, "criticRating": 87, "genres": ["Drama", "Crime", "Adventure"], "overview": "Taking a break from their dreary lives, close friends Thelma and Louise embark on a short weekend trip that ends in unforeseen incriminating circumstances. As fugitives, both women rediscover the strength of their\u2026", "poster": "assets/movie-thelma-louise-1991-poster.jpg", "backdrop": "assets/movie-thelma-louise-1991-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:40.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1541, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 82, "rtAudienceLabel": "Upright", "imdb": 7.6, "imdbVotes": 197947, "tmdb": 7.594, "tmdbVotes": 4063}}, {"id": "movie-vicky-cristina-barcelona-2008", "title": "Vicky Cristina Barcelona", "year": 2008, "type": "movie", "runtimeSeconds": 5804, "seasonCount": null, "communityRating": 6.798, "criticRating": 80, "genres": ["Drama", "Romance"], "overview": "Two girlfriends on a summer holiday in Spain become enamored with the same painter, unaware that his ex-wife, with whom he has a tempestuous relationship, is about to re-enter the picture.", "poster": "assets/movie-vicky-cristina-barcelona-2008-poster.jpg", "backdrop": "assets/movie-vicky-cristina-barcelona-2008-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:25:34.0000000Z", "lastPlayedAt": "2026-08-11T20:12:57.3064089Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 5038, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Certified Fresh", "rtAudience": 74, "rtAudienceLabel": "Upright", "imdb": 7.1, "imdbVotes": 283286, "tmdb": 6.797, "tmdbVotes": 4164}}], "shows": [{"id": "tv-all-creatures-great-small-2020", "title": "All Creatures Great & Small", "year": 2020, "type": "tv", "runtimeSeconds": 3000, "seasonCount": 7, "communityRating": 8.2, "criticRating": null, "genres": ["Drama", "Comedy"], "overview": "The heartwarming and humorous adventures of a young country vet in the Yorkshire Dales in the 1930s. A remake of the 1978 series.", "poster": "assets/tv-all-creatures-great-small-2020-poster.jpg", "backdrop": "assets/tv-all-creatures-great-small-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:13:03.4266156Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 108255, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.2, "tmdbVotes": 168}}, {"id": "tv-back-to-life-2019", "title": "Back to Life", "year": 2019, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 2, "communityRating": 7.367, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "When Miri Matteson returns home after eighteen years, can she integrate back into her old life? With a terrible event from her past hanging over her, it won\u2019t be easy.", "poster": "assets/tv-back-to-life-2019-poster.jpg", "backdrop": "assets/tv-back-to-life-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0688758Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 88510, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.4, "tmdbVotes": 65}}, {"id": "tv-big-boys-2022", "title": "Big Boys", "year": 2022, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 3, "communityRating": 7.6, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Two boys from very different ends of the 'spectrum of masculinity' become best friends at Brent University freshers' week; in their first year of uni, they explore, experiment and try to discover themselves, helping\u2026", "poster": "assets/tv-big-boys-2022-poster.jpg", "backdrop": "assets/tv-big-boys-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:25:58.2156837Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 202851, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.6, "tmdbVotes": 49}}, {"id": "tv-call-the-midwife-2012", "title": "Call the Midwife", "year": 2012, "type": "tv", "runtimeSeconds": 3120, "seasonCount": 15, "communityRating": 7.5, "criticRating": null, "genres": ["Drama"], "overview": "Drama following the lives of a group of midwives working in the poverty-stricken East End of London during the 1950s, based on the best-selling memoirs of Jennifer Worth.", "poster": "assets/tv-call-the-midwife-2012-poster.jpg", "backdrop": "assets/tv-call-the-midwife-2012-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0377216Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 39793, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.577, "tmdbVotes": 267}}, {"id": "tv-derry-girls-2018", "title": "Derry Girls", "year": 2018, "type": "tv", "runtimeSeconds": 1380, "seasonCount": 3, "communityRating": 8.144, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Amidst the political conflict of Northern Ireland in the 1990s, five secondary school students square off with the universal challenges of being a teenager.", "poster": "assets/tv-derry-girls-2018-poster.jpg", "backdrop": "assets/tv-derry-girls-2018-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-02T20:01:55.8955537Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 76148, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.138, "tmdbVotes": 454}}, {"id": "tv-desperate-housewives-2004", "title": "Desperate Housewives", "year": 2004, "type": "tv", "runtimeSeconds": 2520, "seasonCount": 8, "communityRating": 7.896, "criticRating": null, "genres": ["Mystery", "Drama", "Comedy"], "overview": "Looking down on her friends and family isn't a way of life for Mary Alice Young... it's a way of death. One day, in her perfect house, in the loveliest of suburbs, Mary Alice ended it all. Now she's taking us into\u2026", "poster": "assets/tv-desperate-housewives-2004-poster.jpg", "backdrop": "assets/tv-desperate-housewives-2004-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-29T20:56:40.3505596Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 693, "artwork": {"posterKind": "poster", "backdropKind": "center_crop"}, "ratings": {"rtCritics": 72, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.896, "tmdbVotes": 2171}}, {"id": "tv-the-diplomat-2023", "title": "The Diplomat", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 7.569, "criticRating": null, "genres": ["Drama", "War & Politics"], "overview": "Amid an international crisis, a US diplomat contends with her high-profile job as ambassador to the UK and her strained marriage to a political star.", "poster": "assets/tv-the-diplomat-2023-poster.jpg", "backdrop": "assets/tv-the-diplomat-2023-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0640348Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 203857, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.652, "tmdbVotes": 521}}, {"id": "tv-the-durrells-2016", "title": "The Durrells", "year": 2016, "type": "tv", "runtimeSeconds": 2760, "seasonCount": 4, "communityRating": 7.402, "criticRating": null, "genres": ["Drama", "Comedy", "Family"], "overview": "In 1935, financially strapped widow Louisa Durrell, whose life has fallen apart, decides to move from England, with her four children (three sons, one daughter), to the island of Corfu, Greece. Once there, the family\u2026", "poster": "assets/tv-the-durrells-2016-poster.jpg", "backdrop": "assets/tv-the-durrells-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0619436Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 66134, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.4, "tmdbVotes": 127}}, {"id": "tv-etoile-2025", "title": "\u00c9toile", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.197, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "In an ambitious gambit to save their storied institutions, two world-renowned ballet companies in New York City and Paris swap their most talented stars.", "poster": "assets/tv-etoile-2025-poster.jpg", "backdrop": "assets/tv-etoile-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0601383Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 225383, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 86, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.3, "tmdbVotes": 40}}, {"id": "tv-gilmore-girls-2000", "title": "Gilmore Girls", "year": 2000, "type": "tv", "runtimeSeconds": 2640, "seasonCount": 7, "communityRating": 7.893, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Set in the charming town of Stars Hollow, Connecticut, the series follows the captivating lives of Lorelai and Rory Gilmore, a mother/daughter pair who have a relationship most people only dream of.", "poster": "assets/tv-gilmore-girls-2000-poster.jpg", "backdrop": "assets/tv-gilmore-girls-2000-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0631035Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 4586, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.9, "tmdbVotes": 1339}}, {"id": "tv-ginny-georgia-2021", "title": "Ginny & Georgia", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 8.026, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Free-spirited Georgia and her two kids, Ginny and Austin, move north in search of a fresh start but find that the road to new beginnings can be bumpy.", "poster": "assets/tv-ginny-georgia-2021-poster.jpg", "backdrop": "assets/tv-ginny-georgia-2021-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0694828Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 117581, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 68, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8, "tmdbVotes": 1840}}, {"id": "tv-hellcats-2010", "title": "Hellcats", "year": 2010, "type": "tv", "runtimeSeconds": 2460, "seasonCount": 1, "communityRating": 7.5, "criticRating": null, "genres": ["Drama"], "overview": "Pre-law student Marti Perkins' world is flipped upside down when she loses her scholarship, and realizes the only way she can stay in school is by reigniting her dormant teen gymnastic skills to win a place on Lancer\u2026", "poster": "assets/tv-hellcats-2010-poster.jpg", "backdrop": "assets/tv-hellcats-2010-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-22T20:39:02.7719318Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 32504, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 65, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 116}}, {"id": "tv-high-potential-2024", "title": "High Potential", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.201, "criticRating": null, "genres": ["Drama", "Crime", "Mystery"], "overview": "Morgan is a single mom with an exceptional mind, whose unconventional knack for solving crimes leads to an unusual and unstoppable partnership with a by-the-book seasoned detective.", "poster": "assets/tv-high-potential-2024-poster.jpg", "backdrop": "assets/tv-high-potential-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0686091Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 226637, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 98, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.3, "tmdbVotes": 707}}, {"id": "tv-how-to-get-to-heaven-from-belfast-2026", "title": "How to Get to Heaven from Belfast", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.027, "criticRating": null, "genres": ["Mystery", "Comedy", "Drama"], "overview": "Three lifelong pals embark on a chaotic quest to solve the mystery of their old friend's suspicious death and keep their own dark secret under wraps.", "poster": "assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg", "backdrop": "assets/tv-how-to-get-to-heaven-from-belfast-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0683192Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 233079, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.063, "tmdbVotes": 95}}, {"id": "tv-the-hunting-wives-2025", "title": "The Hunting Wives", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 6.932, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "Sophie trades New England for East Texas and falls into a wealthy socialite's magnetic orbit \u2014 where a clique of housewives hide deadly secrets.", "poster": "assets/tv-the-hunting-wives-2025-poster.jpg", "backdrop": "assets/tv-the-hunting-wives-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T19:42:46.3227892Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 244447, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.964, "tmdbVotes": 112}}, {"id": "tv-the-lost-flowers-of-alice-hart-2023", "title": "The Lost Flowers of Alice Hart", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.634, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "After losing her parents in a mysterious fire, nine-year-old Alice Hart is raised by her grandmother June on a flower farm where she learns there are secrets within secrets. But years on, an unearthed betrayal sees\u2026", "poster": "assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg", "backdrop": "assets/tv-the-lost-flowers-of-alice-hart-2023-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0607236Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 125816, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 82, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.64, "tmdbVotes": 161}}, {"id": "tv-love-is-blind-2020", "title": "Love Is Blind", "year": 2020, "type": "tv", "runtimeSeconds": null, "seasonCount": 11, "communityRating": 6.261, "criticRating": null, "genres": ["Reality"], "overview": "Nick and Vanessa Lachey host this social experiment where single men and women look for love and get engaged, all before meeting in person.", "poster": "assets/tv-love-is-blind-2020-poster.jpg", "backdrop": "assets/tv-love-is-blind-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-22T17:11:08.2447226Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 99353, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.233, "tmdbVotes": 118}}, {"id": "tv-maximum-pleasure-guaranteed-2026", "title": "Maximum Pleasure Guaranteed", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 6.676, "criticRating": null, "genres": ["Comedy"], "overview": "A newly divorced mom falls down a dangerous rabbit hole of blackmail, murder, and youth soccer.", "poster": "assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg", "backdrop": "assets/tv-maximum-pleasure-guaranteed-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-07T21:04:02.8277793Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 285404, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.799, "tmdbVotes": 92}}, {"id": "tv-minx-2022", "title": "Minx", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 6.833, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "An earnest young feminist joins forces with a low-rent publisher to make the world's first erotic magazine for women.", "poster": "assets/tv-minx-2022-poster.jpg", "backdrop": "assets/tv-minx-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0653613Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 118303, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.9, "tmdbVotes": 123}}, {"id": "tv-offspring-2010", "title": "Offspring", "year": 2010, "type": "tv", "runtimeSeconds": 3600, "seasonCount": 7, "communityRating": 6.9, "criticRating": null, "genres": ["Drama"], "overview": "An exuberant drama set in Melbourne's Fitzroy, centering on Nina Proudman and her struggle to deal with her fabulously messy family, her hunt for a decent love life and her tendency to overthink and fly off into fantasy.", "poster": "assets/tv-offspring-2010-poster.jpg", "backdrop": "assets/tv-offspring-2010-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0664581Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 33155, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.9, "tmdbVotes": 27}}, {"id": "tv-only-murders-in-the-building-2021", "title": "Only Murders in the Building", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 8.398, "criticRating": null, "genres": ["Comedy", "Mystery", "Crime"], "overview": "Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.", "poster": "assets/tv-only-murders-in-the-building-2021-poster.jpg", "backdrop": "assets/tv-only-murders-in-the-building-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:25:58.2163301Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 107113, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.398, "tmdbVotes": 2333}}, {"id": "tv-outlander-blood-of-my-blood-2025", "title": "Outlander: Blood of My Blood", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.542, "criticRating": null, "genres": ["Drama"], "overview": "From the battlefields of World War I to the rugged Highlands of 18th century Scotland, two fated couples must defy the forces that seek to tear them apart, unfolding in surprising and unforeseen ways in this romantic\u2026", "poster": "assets/tv-outlander-blood-of-my-blood-2025-poster.jpg", "backdrop": "assets/tv-outlander-blood-of-my-blood-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0691373Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 207484, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.738, "tmdbVotes": 124}}, {"id": "tv-rivals-2024", "title": "Rivals", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 7.44, "criticRating": null, "genres": ["Drama"], "overview": "It is 1986 and Britain is booming. \"Rivals\" dives headfirst into the cutthroat world of TV where hair-dos are big and ambitions are even bigger. Deals are brokered in boardrooms, as well as bedrooms. Nobody can be\u2026", "poster": "assets/tv-rivals-2024-poster.jpg", "backdrop": "assets/tv-rivals-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0645815Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 208921, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.4, "tmdbVotes": 93}}, {"id": "tv-rooster-2026", "title": "Rooster", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.431, "criticRating": null, "genres": ["Comedy"], "overview": "On a college campus, an author navigates a complicated relationship with his daughter.", "poster": "assets/tv-rooster-2026-poster.jpg", "backdrop": "assets/tv-rooster-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.063389Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 254953, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.423, "tmdbVotes": 144}}, {"id": "tv-sex-and-the-city-1998", "title": "Sex and the City", "year": 1998, "type": "tv", "runtimeSeconds": null, "seasonCount": 6, "communityRating": 7.365, "criticRating": null, "genres": ["Drama", "Comedy"], "overview": "Based on the bestselling book by Candace Bushnell, Sex and the City tells the story of four best friends, all single and in their late thirties, as they pursue their careers and talk about their sex lives, all while\u2026", "poster": "assets/tv-sex-and-the-city-1998-poster.jpg", "backdrop": "assets/tv-sex-and-the-city-1998-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0679324Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 105, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 70, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.351, "tmdbVotes": 2220}}, {"id": "tv-skymed-2022", "title": "SkyMed", "year": 2022, "type": "tv", "runtimeSeconds": 2640, "seasonCount": 4, "communityRating": 6.665, "criticRating": null, "genres": ["Drama"], "overview": "Life, death and drama at 20,000 feet. The series weaves together intense character journeys and high-stakes medical rescues, as we follow the triumphs, heartbreaks and tribulations of budding nurses and pilots flying\u2026", "poster": "assets/tv-skymed-2022-poster.jpg", "backdrop": "assets/tv-skymed-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-22T19:44:29.2431354Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 133908, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.699, "tmdbVotes": 98}}, {"id": "tv-so-you-think-you-can-dance-2008", "title": "So You Think You Can Dance", "year": 2008, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 9.8, "criticRating": null, "genres": ["Family", "Reality"], "overview": "So You Think You Can Dance is a joint Belgian-Dutch dance competition show broadcast on RTL 5 and vtm television stations. The first season was broadcast only in the Netherlands, whereas starting with season 2, the\u2026", "poster": "assets/tv-so-you-think-you-can-dance-2008-poster.jpg", "backdrop": "assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-23T11:11:35.4124418Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 40231, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 9.8, "tmdbVotes": 4}}, {"id": "tv-the-white-lotus-2021", "title": "The White Lotus", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.597, "criticRating": null, "genres": ["Comedy", "Drama", "Mystery"], "overview": "Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026", "poster": "assets/tv-the-white-lotus-2021-poster.jpg", "backdrop": "assets/tv-the-white-lotus-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T02:45:52.8742674Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 111803, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.599, "tmdbVotes": 1604}}, {"id": "tv-yellowjackets-2021", "title": "Yellowjackets", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.431, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026", "poster": "assets/tv-yellowjackets-2021-poster.jpg", "backdrop": "assets/tv-yellowjackets-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:31:47.4592756Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 117488, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.431, "tmdbVotes": 1070}}], "anime": [], "resume": [{"id": "episode-sex-and-the-country-2001-s4e9", "title": "Sex and the Country", "year": 2001, "type": "episode", "runtimeSeconds": 1751, "seasonCount": null, "communityRating": 5.9, "criticRating": null, "genres": ["Comedy", "Drama", "Romance"], "overview": "Carrie is dragged to Aidan's country home in Suffern, New York. Charlotte visits the MacDougal \"compound\" in Connecticut. Miranda yells at Steve when he tells her he has testicular cancer but has no clue about his\u2026", "poster": "assets/episode-sex-and-the-country-2001-s4e9-poster.jpg", "backdrop": "assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 96.08, "progress": 0.0549, "addedAt": "2026-07-17T10:53:15.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Sex and the City", "seasonNumber": 4, "episodeNumber": 9, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}], "nextUp": [{"id": "episode-what-more-do-i-need-2008-s5e7", "title": "What More Do I Need?", "year": 2008, "type": "episode", "runtimeSeconds": 2521, "seasonCount": null, "communityRating": 8, "criticRating": null, "genres": ["Comedy", "Drama", "Mystery"], "overview": "Lynette and Tom uncover the unsettling truth about son Porter. Gabrielle suspects Carlos' best client, Virginia, has ulterior motives. Bree's moment of weakness could bring her great embarrassment and shame.\u2026", "poster": "assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg", "backdrop": "assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-29T14:37:28.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Desperate Housewives", "seasonNumber": 5, "episodeNumber": 7, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}}, {"id": "episode-new-day-2021-s1e2", "title": "New Day", "year": 2021, "type": "episode", "runtimeSeconds": 3445, "seasonCount": null, "communityRating": 7.143, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "A cautiously optimistic Belinda walks a fine line with Tanya, while Armond brainstorms ways to deal with Shane. After her husband belittles her work, Rachel approaches Nicole for advice. Paula and Olivia lose track\u2026", "poster": "assets/episode-new-day-2021-s1e2-poster.jpg", "backdrop": "assets/episode-new-day-2021-s1e2-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-19T22:18:57.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "The White Lotus", "seasonNumber": 1, "episodeNumber": 2, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-the-uncomfortability-2010-s1e2", "title": "The Uncomfortability", "year": 2010, "type": "episode", "runtimeSeconds": 2687, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Nina's insecurities are revealed when a patient poses an awkward request. Meanwhile Geraldine decides to try online dating, and Jimmy is an unexpected saviour to Cherie and Darcy.", "poster": "assets/episode-the-uncomfortability-2010-s1e2-poster.jpg", "backdrop": "assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T11:47:42.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Offspring", "seasonNumber": 1, "episodeNumber": 2, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-episode-2-2016-s1e2", "title": "Episode 2", "year": 2016, "type": "episode", "runtimeSeconds": 2766, "seasonCount": null, "communityRating": 8.3, "criticRating": null, "genres": ["Biography", "Comedy", "Drama"], "overview": "Larry falls dangerously ill, and it falls to Theo and his neighbour Sven to put their rudimentary medical skills to the task of saving his life. The family's money worries continue, but Spiros brings them good news\u2026", "poster": "assets/episode-episode-2-2016-s1e2-poster.jpg", "backdrop": "assets/episode-episode-2-2016-s1e2-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-31T07:29:17.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "The Durrells", "seasonNumber": 1, "episodeNumber": 2, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-remembrance-day-2026-s2e1", "title": "Remembrance Day", "year": 2026, "type": "episode", "runtimeSeconds": 3822, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Fantasy", "Romance"], "overview": "The fiery crosses have been lit across the Highlands; lovers must part, as the call to arms brings friends and enemies home and the Jacobite Rebellion begins in earnest.", "poster": "assets/episode-remembrance-day-2026-s2e1-poster.jpg", "backdrop": "assets/episode-remembrance-day-2026-s2e1-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-19T03:50:07.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Outlander: Blood of My Blood", "seasonNumber": 2, "episodeNumber": 1, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-one-of-us-2024-s1e7", "title": "One of Us", "year": 2024, "type": "episode", "runtimeSeconds": 2555, "seasonCount": null, "communityRating": 7.7, "criticRating": null, "genres": [], "overview": "Morgan agrees to show Ava around the LAPD, but things quickly take a dangerous turn when the precinct is held hostage by the friends of a recently convicted man. Can Morgan and the detectives outsmart their captors\u2026", "poster": "assets/episode-one-of-us-2024-s1e7-poster.jpg", "backdrop": "assets/episode-one-of-us-2024-s1e7-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T13:57:22.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "High Potential", "seasonNumber": 1, "episodeNumber": 7, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-the-breakup-part-2-2001-s1e17", "title": "The Breakup, Part 2", "year": 2001, "type": "episode", "runtimeSeconds": 2669, "seasonCount": null, "communityRating": 7.4, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "After recent events, Rory keeps busy, refusing to give in to self-pity. Meanwhile, Stars Hollow's residents rally around her, much to her dismay.", "poster": "assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg", "backdrop": "assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T11:28:05.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Gilmore Girls", "seasonNumber": 1, "episodeNumber": 17, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-i-heart-ny-2002-s4e18", "title": "I Heart NY", "year": 2002, "type": "episode", "runtimeSeconds": 1905, "seasonCount": null, "communityRating": 6.667, "criticRating": null, "genres": ["Comedy", "Drama", "Romance"], "overview": "Carrie is shocked to find out that Big is moving to Napa, California and plans to give him a New York send-off. Charlotte begins dating another recent divorc\u00e9. Miranda prepares for the new baby as her delivery date\u2026", "poster": "assets/episode-i-heart-ny-2002-s4e18-poster.jpg", "backdrop": "assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T12:35:14.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Sex and the City", "seasonNumber": 4, "episodeNumber": 18, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}], "recentlyAdded": [{"id": "tv-yellowjackets-2021", "title": "Yellowjackets", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.431, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026", "poster": "assets/tv-yellowjackets-2021-poster.jpg", "backdrop": "assets/tv-yellowjackets-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:31:47.4592756Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 117488, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.431, "tmdbVotes": 1070}}, {"id": "tv-only-murders-in-the-building-2021", "title": "Only Murders in the Building", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 8.398, "criticRating": null, "genres": ["Comedy", "Mystery", "Crime"], "overview": "Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.", "poster": "assets/tv-only-murders-in-the-building-2021-poster.jpg", "backdrop": "assets/tv-only-murders-in-the-building-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:25:58.2163301Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 107113, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.398, "tmdbVotes": 2333}}, {"id": "tv-big-boys-2022", "title": "Big Boys", "year": 2022, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 3, "communityRating": 7.6, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Two boys from very different ends of the 'spectrum of masculinity' become best friends at Brent University freshers' week; in their first year of uni, they explore, experiment and try to discover themselves, helping\u2026", "poster": "assets/tv-big-boys-2022-poster.jpg", "backdrop": "assets/tv-big-boys-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:25:58.2156837Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 202851, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.6, "tmdbVotes": 49}}, {"id": "tv-all-creatures-great-small-2020", "title": "All Creatures Great & Small", "year": 2020, "type": "tv", "runtimeSeconds": 3000, "seasonCount": 7, "communityRating": 8.2, "criticRating": null, "genres": ["Drama", "Comedy"], "overview": "The heartwarming and humorous adventures of a young country vet in the Yorkshire Dales in the 1930s. A remake of the 1978 series.", "poster": "assets/tv-all-creatures-great-small-2020-poster.jpg", "backdrop": "assets/tv-all-creatures-great-small-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T20:13:03.4266156Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 108255, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.2, "tmdbVotes": 168}}, {"id": "movie-the-killing-of-a-sacred-deer-2017", "title": "The Killing of a Sacred Deer", "year": 2017, "type": "movie", "runtimeSeconds": 7262, "seasonCount": null, "communityRating": 7.003, "criticRating": 79, "genres": ["Drama", "Thriller", "Mystery"], "overview": "Dr. Steven Murphy is a renowned cardiovascular surgeon who presides over a spotless household with his wife and two children. Lurking at the margins of his idyllic suburban existence is Martin, a fatherless teen who\u2026", "poster": "assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg", "backdrop": "assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T21:49:26.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 399057, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 79, "rtCriticsLabel": "Certified Fresh", "rtAudience": 63, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 219488, "tmdb": 7.001, "tmdbVotes": 4640}}, {"id": "tv-derry-girls-2018", "title": "Derry Girls", "year": 2018, "type": "tv", "runtimeSeconds": 1380, "seasonCount": 3, "communityRating": 8.144, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Amidst the political conflict of Northern Ireland in the 1990s, five secondary school students square off with the universal challenges of being a teenager.", "poster": "assets/tv-derry-girls-2018-poster.jpg", "backdrop": "assets/tv-derry-girls-2018-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-02T20:01:55.8955537Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 76148, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.138, "tmdbVotes": 454}}, {"id": "tv-so-you-think-you-can-dance-2008", "title": "So You Think You Can Dance", "year": 2008, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 9.8, "criticRating": null, "genres": ["Family", "Reality"], "overview": "So You Think You Can Dance is a joint Belgian-Dutch dance competition show broadcast on RTL 5 and vtm television stations. The first season was broadcast only in the Netherlands, whereas starting with season 2, the\u2026", "poster": "assets/tv-so-you-think-you-can-dance-2008-poster.jpg", "backdrop": "assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-23T11:11:35.4124418Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 40231, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 9.8, "tmdbVotes": 4}}, {"id": "tv-hellcats-2010", "title": "Hellcats", "year": 2010, "type": "tv", "runtimeSeconds": 2460, "seasonCount": 1, "communityRating": 7.5, "criticRating": null, "genres": ["Drama"], "overview": "Pre-law student Marti Perkins' world is flipped upside down when she loses her scholarship, and realizes the only way she can stay in school is by reigniting her dormant teen gymnastic skills to win a place on Lancer\u2026", "poster": "assets/tv-hellcats-2010-poster.jpg", "backdrop": "assets/tv-hellcats-2010-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-22T20:39:02.7719318Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 32504, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 65, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 116}}, {"id": "tv-skymed-2022", "title": "SkyMed", "year": 2022, "type": "tv", "runtimeSeconds": 2640, "seasonCount": 4, "communityRating": 6.665, "criticRating": null, "genres": ["Drama"], "overview": "Life, death and drama at 20,000 feet. The series weaves together intense character journeys and high-stakes medical rescues, as we follow the triumphs, heartbreaks and tribulations of budding nurses and pilots flying\u2026", "poster": "assets/tv-skymed-2022-poster.jpg", "backdrop": "assets/tv-skymed-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-22T19:44:29.2431354Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 133908, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.699, "tmdbVotes": 98}}, {"id": "tv-love-is-blind-2020", "title": "Love Is Blind", "year": 2020, "type": "tv", "runtimeSeconds": null, "seasonCount": 11, "communityRating": 6.261, "criticRating": null, "genres": ["Reality"], "overview": "Nick and Vanessa Lachey host this social experiment where single men and women look for love and get engaged, all before meeting in person.", "poster": "assets/tv-love-is-blind-2020-poster.jpg", "backdrop": "assets/tv-love-is-blind-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-22T17:11:08.2447226Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 99353, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.233, "tmdbVotes": 118}}, {"id": "tv-the-hunting-wives-2025", "title": "The Hunting Wives", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 6.932, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "Sophie trades New England for East Texas and falls into a wealthy socialite's magnetic orbit \u2014 where a clique of housewives hide deadly secrets.", "poster": "assets/tv-the-hunting-wives-2025-poster.jpg", "backdrop": "assets/tv-the-hunting-wives-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T19:42:46.3227892Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 244447, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.964, "tmdbVotes": 112}}, {"id": "tv-the-white-lotus-2021", "title": "The White Lotus", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.597, "criticRating": null, "genres": ["Comedy", "Drama", "Mystery"], "overview": "Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026", "poster": "assets/tv-the-white-lotus-2021-poster.jpg", "backdrop": "assets/tv-the-white-lotus-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T02:45:52.8742674Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 111803, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.599, "tmdbVotes": 1604}}], "discover": [{"id": "movie-across-the-hall-2009", "title": "Across the Hall", "year": 2009, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Thriller", "Mystery"], "overview": "Convinced that his fiancee is cheating on him, a man follows her to a hotel and calls his best friend to help him avoid a calamity.", "poster": "assets/movie-across-the-hall-2009-poster.jpg", "backdrop": "assets/movie-across-the-hall-2009-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 21469, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 39, "rtAudienceLabel": "Spilled", "imdb": 5.8, "imdbVotes": 5130, "tmdb": 6, "tmdbVotes": 129}}, {"id": "movie-addicted-2014", "title": "Addicted", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.", "poster": "assets/movie-addicted-2014-poster.jpg", "backdrop": "assets/movie-addicted-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 235271, "ratings": {"rtCritics": 7, "rtCriticsLabel": "Rotten", "rtAudience": 51, "rtAudienceLabel": "Spilled", "imdb": 5, "imdbVotes": 13131, "tmdb": 5.928, "tmdbVotes": 466}}, {"id": "movie-adulterers-2016", "title": "Adulterers", "year": 2016, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Thriller", "Drama"], "overview": "On a steamy New Orleans afternoon, Sam Deuprey decides to surprise his beautiful wife, Ashley, for their anniversary and comes home early. But Ashley is not alone in the bedroom. She and her lover, Damien are about\u2026", "poster": "assets/movie-adulterers-2016-poster.jpg", "backdrop": "assets/movie-adulterers-2016-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 376004, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 37, "rtAudienceLabel": "Spilled", "imdb": 5.1, "imdbVotes": 2144, "tmdb": 6.2, "tmdbVotes": 116}}, {"id": "movie-alone-with-her-2006", "title": "Alone With Her", "year": 2006, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama", "Thriller"], "overview": "The harrowing story of a disturbed young man's attempts to win the affections of an unsuspecting young woman. When Doug first sees Amy, he instantly falls for her and begins to watch her every move, going so far as\u2026", "poster": "assets/movie-alone-with-her-2006-poster.jpg", "backdrop": "assets/movie-alone-with-her-2006-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 18206, "ratings": {"rtCritics": 70, "rtCriticsLabel": "Fresh", "rtAudience": 46, "rtAudienceLabel": "Spilled", "imdb": 6, "imdbVotes": 4359, "tmdb": 5.9, "tmdbVotes": 94}}, {"id": "movie-awake-2007", "title": "Awake", "year": 2007, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Thriller", "Crime", "Mystery"], "overview": "While undergoing heart surgery, a man experiences a phenomenon called \u2018anesthetic awareness\u2019, which leaves him awake but paralyzed throughout the operation. As various obstacles present themselves, his wife must make\u2026", "poster": "assets/movie-awake-2007-poster.jpg", "backdrop": "assets/movie-awake-2007-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 13483, "ratings": {"rtCritics": 23, "rtCriticsLabel": "Rotten", "rtAudience": 55, "rtAudienceLabel": "Spilled", "imdb": 6.5, "imdbVotes": 89389, "tmdb": 6.586, "tmdbVotes": 1649}}, {"id": "movie-black-bear-2020", "title": "Black Bear", "year": 2020, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026", "poster": "assets/movie-black-bear-2020-poster.jpg", "backdrop": "assets/movie-black-bear-2020-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 624788, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 64, "rtAudienceLabel": "Upright", "imdb": 6.5, "imdbVotes": 18408, "tmdb": 6.337, "tmdbVotes": 315}}, {"id": "movie-butterfly-on-a-wheel-2007", "title": "Butterfly on a Wheel", "year": 2007, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama", "Thriller"], "overview": "A sociopathic kidnapper methodically pushes a desperate pair of parents to their absolute breaking point.", "poster": "assets/movie-butterfly-on-a-wheel-2007-poster.jpg", "backdrop": "assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 13250, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": 61, "rtAudienceLabel": "Upright", "imdb": 6.6, "imdbVotes": 35409, "tmdb": 6.457, "tmdbVotes": 595}}, {"id": "movie-careful-what-you-wish-for-2015", "title": "Careful What You Wish For", "year": 2015, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.", "poster": "assets/movie-careful-what-you-wish-for-2015-poster.jpg", "backdrop": "assets/movie-careful-what-you-wish-for-2015-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 317744, "ratings": {"rtCritics": 17, "rtCriticsLabel": "Rotten", "rtAudience": 61, "rtAudienceLabel": "Upright", "imdb": 5.6, "imdbVotes": 9452, "tmdb": 6.129, "tmdbVotes": 446}}, {"id": "movie-crime-of-passion-1956", "title": "Crime of Passion", "year": 1956, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Crime", "Thriller"], "overview": "Kathy leaves the newspaper business to marry homicide detective Bill, but is frustrated by his lack of ambition and the banality of life in the suburbs. Her drive to advance Bill's career soon takes her down a\u2026", "poster": "assets/movie-crime-of-passion-1956-poster.jpg", "backdrop": "assets/movie-crime-of-passion-1956-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "center_crop", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1920, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Fresh", "rtAudience": 34, "rtAudienceLabel": "Spilled", "imdb": 6.4, "imdbVotes": 3750, "tmdb": 6.1, "tmdbVotes": 57}}, {"id": "movie-deep-water-2022", "title": "Deep Water", "year": 2022, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Mystery", "Thriller"], "overview": "A well-to-do husband who allows his wife to have affairs in order to avoid a divorce becomes a prime suspect in the disappearance of her lovers.", "poster": "assets/movie-deep-water-2022-poster.jpg", "backdrop": "assets/movie-deep-water-2022-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 619979, "ratings": {"rtCritics": 35, "rtCriticsLabel": "Rotten", "rtAudience": 25, "rtAudienceLabel": "Spilled", "imdb": 5.5, "imdbVotes": 74098, "tmdb": 5.834, "tmdbVotes": 1755}}, {"id": "movie-derailed-2005", "title": "Derailed", "year": 2005, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "When two married business executives having an affair are blackmailed by a violent criminal, they are forced to turn the tables on him to save their families.", "poster": "assets/movie-derailed-2005-poster.jpg", "backdrop": "assets/movie-derailed-2005-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 8999, "ratings": {"rtCritics": 20, "rtCriticsLabel": "Rotten", "rtAudience": 58, "rtAudienceLabel": "Spilled", "imdb": 6.6, "imdbVotes": 79563, "tmdb": 6.405, "tmdbVotes": 1193}}, {"id": "movie-die-my-love-2025", "title": "Die My Love", "year": 2025, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama"], "overview": "After inheriting a remote Montana house, Jackson moves there from New York with his partner Grace, and the couple soon welcome a child. As Jackson becomes increasingly absent and rural isolation sets in, Grace\u2026", "poster": "assets/movie-die-my-love-2025-poster.jpg", "backdrop": "assets/movie-die-my-love-2025-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1033148, "ratings": {"rtCritics": 74, "rtCriticsLabel": "Certified Fresh", "rtAudience": 45, "rtAudienceLabel": "Spilled", "imdb": 6, "imdbVotes": 50664, "tmdb": 6.1, "tmdbVotes": 683}}, {"id": "movie-dream-house-2011", "title": "Dream House", "year": 2011, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Mystery", "Drama", "Thriller"], "overview": "Publisher Will Atenton quits a lucrative job in New York to relocate his wife, Libby, and their daughters to a quaint town in New England. However, as they settle into their home the Atentons discover that a woman\u2026", "poster": "assets/movie-dream-house-2011-poster.jpg", "backdrop": "assets/movie-dream-house-2011-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 69668, "ratings": {"rtCritics": 7, "rtCriticsLabel": "Rotten", "rtAudience": 36, "rtAudienceLabel": "Spilled", "imdb": 6, "imdbVotes": 71386, "tmdb": 6.212, "tmdbVotes": 1480}}, {"id": "movie-dream-lover-1994", "title": "Dream Lover", "year": 1994, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Mystery", "Thriller"], "overview": "Not long after they cross paths at an art gallery, architect Ray Reardon and hypnotically sensual Lena are married with children. But as strange incidents occur, Ray begins to realize he may not really know the woman\u2026", "poster": "assets/movie-dream-lover-1994-poster.jpg", "backdrop": "assets/movie-dream-lover-1994-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 18190, "ratings": {"rtCritics": 57, "rtCriticsLabel": "Rotten", "rtAudience": 38, "rtAudienceLabel": "Spilled", "imdb": 6.2, "imdbVotes": 7865, "tmdb": 6.844, "tmdbVotes": 167}}, {"id": "movie-drop-2025", "title": "Drop", "year": 2025, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Mystery", "Thriller"], "overview": "Violet, a widowed mother on her first date in years, arrives at an upscale restaurant where she is relieved that her date, Henry, is more charming and handsome than she expected. But their chemistry begins to curdle\u2026", "poster": "assets/movie-drop-2025-poster.jpg", "backdrop": "assets/movie-drop-2025-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1249213, "ratings": {"rtCritics": 83, "rtCriticsLabel": "Certified Fresh", "rtAudience": 79, "rtAudienceLabel": "Upright", "imdb": 6.1, "imdbVotes": 61018, "tmdb": 6.275, "tmdbVotes": 1023}}, {"id": "movie-eastern-promises-2007", "title": "Eastern Promises", "year": 2007, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Thriller", "Crime", "Mystery"], "overview": "A Russian teenager living in London dies during childbirth but leaves clues in her diary that could tie her child to a rape involving a violent Russian mob family.", "poster": "assets/movie-eastern-promises-2007-poster.jpg", "backdrop": "assets/movie-eastern-promises-2007-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 2252, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 83, "rtAudienceLabel": "Upright", "imdb": 7.6, "imdbVotes": 281015, "tmdb": 7.349, "tmdbVotes": 3900}}, {"id": "movie-faces-1968", "title": "Faces", "year": 1968, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama"], "overview": "Middle-aged suburban husband Richard abruptly tells his wife, Maria, that he wants a divorce. As Richard takes up with a younger woman, Maria enjoys a night on the town with her friends and meets a younger man. As\u2026", "poster": "assets/movie-faces-1968-poster.jpg", "backdrop": "assets/movie-faces-1968-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "center_crop", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 753, "ratings": {"rtCritics": 83, "rtCriticsLabel": "Fresh", "rtAudience": 86, "rtAudienceLabel": "Upright", "imdb": 7.4, "imdbVotes": 12950, "tmdb": 7.093, "tmdbVotes": 227}}, {"id": "movie-fatale-2020", "title": "Fatale", "year": 2020, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "After a wild one-night stand, successful sports agent Derrick Tyler watches his perfect life slowly disappear when he discovers the sexy and mysterious woman he risked everything for is determined police detective\u2026", "poster": "assets/movie-fatale-2020-poster.jpg", "backdrop": "assets/movie-fatale-2020-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 549514, "ratings": {"rtCritics": 46, "rtCriticsLabel": "Rotten", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 5.5, "imdbVotes": 16392, "tmdb": 6, "tmdbVotes": 401}}, {"id": "tv-56-days-2026", "title": "56 Days", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Crime", "Mystery"], "overview": "When an unidentified body is found in a luxury apartment linked to Oliver Kennedy and his girlfriend Ciara Wyse, Detectives Lee Reardon and Karl Connolly reconstruct the couple's deadly romance across the past 56 days.", "poster": "assets/tv-56-days-2026-poster.jpg", "backdrop": "assets/tv-56-days-2026-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 241372, "ratings": {"rtCritics": 60, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.388, "tmdbVotes": 245}}, {"id": "tv-90-day-fiance-before-the-90-days-2017", "title": "90 Day Fianc\u00e9: Before the 90 Days", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026", "poster": "assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg", "backdrop": "assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 73319, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 45}}, {"id": "tv-90-day-fiance-the-other-way-2019", "title": "90 Day Fianc\u00e9: The Other Way", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?", "poster": "assets/tv-90-day-fiance-the-other-way-2019-poster.jpg", "backdrop": "assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 90046, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 34}}, {"id": "tv-absolutely-fabulous-1992", "title": "Absolutely Fabulous", "year": 1992, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy"], "overview": "Set in the world of fashion and PR, immature fun-loving mother Edina Monsoon and her best friend Patsy drive Eddie's sensible daughter, Saffron, up the wall with their constant drug abuse and outrageous selfishness.\u2026", "poster": "assets/tv-absolutely-fabulous-1992-poster.jpg", "backdrop": "assets/tv-absolutely-fabulous-1992-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 4493, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.6, "tmdbVotes": 198}}, {"id": "tv-the-afterparty-2022", "title": "The Afterparty", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Mystery", "Comedy", "Crime"], "overview": "Nothing is as it seems in this hilarious murder mystery where each suspect's story is told through a different film genre.", "poster": "assets/tv-the-afterparty-2022-poster.jpg", "backdrop": "assets/tv-the-afterparty-2022-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 106454, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.071, "tmdbVotes": 245}}, {"id": "tv-agatha-christie-s-marple-2004", "title": "Agatha Christie's Marple", "year": 2004, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Crime", "Mystery"], "overview": "The adventures of Miss Jane Marple, an elderly spinster living in the quiet little village of St Mary Mead. During her many visits to friends and relatives in other villages, Miss Marple often stumbles upon\u2026", "poster": "assets/tv-agatha-christie-s-marple-2004-poster.jpg", "backdrop": "assets/tv-agatha-christie-s-marple-2004-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 6127, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.9, "tmdbVotes": 152}}, {"id": "tv-all-american-homecoming-2022", "title": "All American: Homecoming", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama"], "overview": "Follow the journeys of Simone Hicks, a tennis hopeful from Beverly Hills, and elite baseball player Damon Sims from Chicago, as they navigate life at the prestigious HBCU Bringston University.", "poster": "assets/tv-all-american-homecoming-2022-poster.jpg", "backdrop": "assets/tv-all-american-homecoming-2022-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 153723, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.3, "tmdbVotes": 70}}, {"id": "tv-and-just-like-that-2021", "title": "And Just Like That\u2026", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "This new chapter of \u201cSex and the City\u201d follows Carrie, Miranda and Charlotte as they navigate the journey from the complicated reality of life and friendship in their 30s to the even more complicated reality of life\u2026", "poster": "assets/tv-and-just-like-that-2021-poster.jpg", "backdrop": "assets/tv-and-just-like-that-2021-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 116450, "ratings": {"rtCritics": 52, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.211, "tmdbVotes": 556}}, {"id": "tv-anne-with-an-e-2017", "title": "Anne with an E", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Family"], "overview": "A coming-of-age story about an outsider who, against all odds and numerous challenges, fights for love and acceptance and for her place in the world. The series centers on a young orphaned girl in the late 1890s,\u2026", "poster": "assets/tv-anne-with-an-e-2017-poster.jpg", "backdrop": "assets/tv-anne-with-an-e-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 70785, "ratings": {"rtCritics": 63, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.7, "tmdbVotes": 5019}}, {"id": "tv-awkward-2011", "title": "Awkward.", "year": 2011, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "An irreverent look at the conflict, chaos and humor that defines teenage life through the eyes of 15-year-old Jenna Hamilton whose life begins to change when a simple accident becomes an epic misunderstanding and is\u2026", "poster": "assets/tv-awkward-2011-poster.jpg", "backdrop": "assets/tv-awkward-2011-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 39859, "ratings": {"rtCritics": 78, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.4, "tmdbVotes": 427}}, {"id": "tv-the-baby-sitters-club-2020", "title": "The Baby-Sitters Club", "year": 2020, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Kids", "Drama"], "overview": "In this contemporary take on the beloved book series, five best friends launch a baby-sitting business that's big on fun and adventure.", "poster": "assets/tv-the-baby-sitters-club-2020-poster.jpg", "backdrop": "assets/tv-the-baby-sitters-club-2020-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 100412, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.9, "tmdbVotes": 506}}, {"id": "tv-ballers-2015", "title": "Ballers", "year": 2015, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Comedy"], "overview": "Looking at the lives of former and current football players, the show follows former superstar Spencer Strasmore as he gets his life on track in retirement while mentoring other current and former players through the\u2026", "poster": "assets/tv-ballers-2015-poster.jpg", "backdrop": "assets/tv-ballers-2015-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 62704, "ratings": {"rtCritics": 72, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.809, "tmdbVotes": 820}}, {"id": "tv-baretta-1975", "title": "Baretta", "year": 1975, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama", "Mystery"], "overview": "Baretta is an American detective television series which ran on ABC from 1975 to 1978. The show was a milder version of a successful 1973\u201374 ABC series, Toma, starring Tony Musante as chameleon-like, real-life New\u2026", "poster": "assets/tv-baretta-1975-poster.jpg", "backdrop": "assets/tv-baretta-1975-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 3216, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.5, "tmdbVotes": 43}}, {"id": "tv-based-on-a-true-story-2023", "title": "Based on a True Story", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "The lives of a realtor, a plumber and a former tennis star unexpectedly collide, exposing America\u2019s obsession with true crime, murder and the slow-close toilet seat.", "poster": "assets/tv-based-on-a-true-story-2023-poster.jpg", "backdrop": "assets/tv-based-on-a-true-story-2023-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 197373, "ratings": {"rtCritics": 79, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7, "tmdbVotes": 160}}, {"id": "tv-the-beast-in-me-2025", "title": "The Beast in Me", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "A famous author is pulled into a twisted mind game with her rich, powerful new neighbor \u2014 who might be a murderer.", "poster": "assets/tv-the-beast-in-me-2025-poster.jpg", "backdrop": "assets/tv-the-beast-in-me-2025-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 250504, "ratings": {"rtCritics": 85, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.072, "tmdbVotes": 334}}, {"id": "tv-big-city-greens-2018", "title": "Big City Greens", "year": 2018, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Family", "Animation", "Comedy"], "overview": "The offbeat adventures of 10-year-old Cricket Green, a mischievous and optimistic country boy who moves to the big city with his wildly out of place family \u2013 older sister Tilly, father Bill and Gramma Alice.", "poster": "assets/tv-big-city-greens-2018-poster.jpg", "backdrop": "assets/tv-big-city-greens-2018-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 80587, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.555, "tmdbVotes": 82}}, {"id": "tv-the-bold-type-2017", "title": "The Bold Type", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Comedy"], "overview": "A glimpse into the outrageous lives of Jane, Kat and Sutton, who are working at the nation's top women's magazine, Scarlet, while navigating their careers, identities and individual voices.", "poster": "assets/tv-the-bold-type-2017-poster.jpg", "backdrop": "assets/tv-the-bold-type-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 69630, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.7, "tmdbVotes": 204}}, {"id": "tv-brassic-2019", "title": "Brassic", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama", "Crime"], "overview": "A group of working-class friends finding unconventional ways to win at life in suburban northern England. These lads have dealt, scammed, bribed and conned their way through adolescence, but now, their dealing and\u2026", "poster": "assets/tv-brassic-2019-poster.jpg", "backdrop": "assets/tv-brassic-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 92621, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.336, "tmdbVotes": 263}}], "discoverySource": {"state": "materialized_snapshot", "rankKnown": false, "selection": "First 18 alphabetically indexed movie stubs and first 18 series stubs, not ranked feed order", "observedAt": "2026-11-04T20:42:12.030108+00:00"}}, {"key": "shared", "name": "Shared", "initial": "S", "color": "#9be38f", "destinations": {"Home": {"kind": "home"}, "Movies": {"kind": "library", "collection": "movies"}, "Shows": {"kind": "library", "collection": "shows"}, "Discover": {"kind": "discover", "collection": "discover"}}, "movies": [{"id": "movie-american-gangster-2007", "title": "American Gangster", "year": 2007, "type": "movie", "runtimeSeconds": 10546, "seasonCount": null, "communityRating": 7.595, "criticRating": 81, "genres": ["Drama", "Crime"], "overview": "Loosely based on the criminal career of Frank Lucas, a gangster from La Grange, North Carolina, who smuggled heroin into the United States on American service planes returning from the Vietnam War, before being\u2026", "poster": "assets/movie-american-gangster-2007-poster.jpg", "backdrop": "assets/movie-american-gangster-2007-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-08-23T10:22:36.0000000Z", "lastPlayedAt": "2026-08-30T21:05:39.7952451Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 4982, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Certified Fresh", "rtAudience": 90, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 495656, "tmdb": 7.599, "tmdbVotes": 6350}}, {"id": "movie-annie-hall-1977", "title": "Annie Hall", "year": 1977, "type": "movie", "runtimeSeconds": 5604, "seasonCount": null, "communityRating": 7.712, "criticRating": 97, "genres": ["Comedy", "Drama", "Romance"], "overview": "New York comedian Alvy Singer falls in love with the ditsy Annie Hall.", "poster": "assets/movie-annie-hall-1977-poster.jpg", "backdrop": "assets/movie-annie-hall-1977-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:23.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 703, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Certified Fresh", "rtAudience": 92, "rtAudienceLabel": "Upright", "imdb": 7.9, "imdbVotes": 293349, "tmdb": 7.71, "tmdbVotes": 4391}}, {"id": "movie-apocalypse-now-1979", "title": "Apocalypse Now", "year": 1979, "type": "movie", "runtimeSeconds": 8835, "seasonCount": null, "communityRating": 8.261, "criticRating": 91, "genres": ["Drama", "War"], "overview": "At the height of the Vietnam war, Captain Benjamin Willard is sent on a dangerous mission that, officially, \"does not exist, nor will it ever exist.\" His goal is to locate - and eliminate - a mysterious Green Beret\u2026", "poster": "assets/movie-apocalypse-now-1979-poster.jpg", "backdrop": "assets/movie-apocalypse-now-1979-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:25.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 28, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Certified Fresh", "rtAudience": 94, "rtAudienceLabel": "Upright", "imdb": 8.4, "imdbVotes": 781424, "tmdb": 8.3, "tmdbVotes": 9526}}, {"id": "movie-arrival-2016", "title": "Arrival", "year": 2016, "type": "movie", "runtimeSeconds": 6967, "seasonCount": null, "communityRating": 7.636, "criticRating": 94, "genres": ["Drama", "Science Fiction", "Mystery"], "overview": "Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.", "poster": "assets/movie-arrival-2016-poster.jpg", "backdrop": "assets/movie-arrival-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-14T19:54:12.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 329865, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Certified Fresh", "rtAudience": 83, "rtAudienceLabel": "Upright", "imdb": 7.9, "imdbVotes": 883621, "tmdb": 7.638, "tmdbVotes": 20125}}, {"id": "movie-avatar-aang-the-last-airbender-2026", "title": "Avatar Aang: The Last Airbender", "year": 2026, "type": "movie", "runtimeSeconds": 5930, "seasonCount": null, "communityRating": 9.147, "criticRating": 90, "genres": ["Animation", "Adventure", "Fantasy"], "overview": "Avatar Aang, the world's last Airbender, learns of an ancient power that could save his culture from extinction. With the help of his friends, he embarks on a global quest to find it before it falls into the wrong\u2026", "poster": "assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg", "backdrop": "assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-25T09:53:09.0000000Z", "lastPlayedAt": "2026-08-02T19:22:33.6155383Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 980431, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 99, "rtAudienceLabel": "Upright", "imdb": 7.7, "imdbVotes": 30792, "tmdb": 9, "tmdbVotes": 1417}}, {"id": "movie-bohemian-rhapsody-2018", "title": "Bohemian Rhapsody", "year": 2018, "type": "movie", "runtimeSeconds": 8077, "seasonCount": null, "communityRating": 7.95, "criticRating": 60, "genres": ["Music", "Drama"], "overview": "Singer Freddie Mercury, guitarist Brian May, drummer Roger Taylor and bass guitarist John Deacon take the music world by storm when they form the rock 'n' roll band Queen in 1970. Hit songs become instant classics.\u2026", "poster": "assets/movie-bohemian-rhapsody-2018-poster.jpg", "backdrop": "assets/movie-bohemian-rhapsody-2018-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:28:20.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 424694, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 60, "rtCriticsLabel": "Fresh", "rtAudience": 85, "rtAudienceLabel": "Upright", "imdb": 7.9, "imdbVotes": 655894, "tmdb": 7.947, "tmdbVotes": 18196}}, {"id": "movie-the-boy-and-the-heron-2023", "title": "The Boy and the Heron", "year": 2023, "type": "movie", "runtimeSeconds": 7437, "seasonCount": null, "communityRating": 7.4, "criticRating": 96, "genres": ["Animation", "Fantasy", "Drama"], "overview": "While the Second World War rages, the teenage Mahito, haunted by his mother's tragic death, is relocated from Tokyo to the serene rural home of his new stepmother Natsuko, a woman who bears a striking resemblance to\u2026", "poster": "assets/movie-the-boy-and-the-heron-2023-poster.jpg", "backdrop": "assets/movie-the-boy-and-the-heron-2023-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:25:15.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 508883, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Certified Fresh", "rtAudience": 88, "rtAudienceLabel": "Upright", "imdb": 7.3, "imdbVotes": 152169, "tmdb": 7.387, "tmdbVotes": 2893}}, {"id": "movie-braveheart-1995", "title": "Braveheart", "year": 1995, "type": "movie", "runtimeSeconds": 10665, "seasonCount": null, "communityRating": 7.945, "criticRating": 74, "genres": ["Drama", "History", "War"], "overview": "Enraged at the slaughter of Murron, his new bride and childhood love, Scottish warrior William Wallace slays a platoon of the local English lord's soldiers. This leads the village to revolt and, eventually, the\u2026", "poster": "assets/movie-braveheart-1995-poster.jpg", "backdrop": "assets/movie-braveheart-1995-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T21:24:28.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 197, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 74, "rtCriticsLabel": "Certified Fresh", "rtAudience": 85, "rtAudienceLabel": "Upright", "imdb": 8.3, "imdbVotes": 1174990, "tmdb": 7.947, "tmdbVotes": 11584}}, {"id": "movie-the-breakfast-club-1985", "title": "The Breakfast Club", "year": 1985, "type": "movie", "runtimeSeconds": 5833, "seasonCount": null, "communityRating": 7.7, "criticRating": 87, "genres": ["Comedy", "Drama"], "overview": "Five high school students from different walks of life endure a Saturday detention under a power-hungry principal. The disparate group includes rebel John, princess Claire, outcast Allison, brainy Brian and Andrew,\u2026", "poster": "assets/movie-the-breakfast-club-1985-poster.jpg", "backdrop": "assets/movie-the-breakfast-club-1985-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:25:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 2108, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 92, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 478743, "tmdb": 7.713, "tmdbVotes": 8815}}, {"id": "movie-castle-in-the-sky-1986", "title": "Castle in the Sky", "year": 1986, "type": "movie", "runtimeSeconds": 7474, "seasonCount": null, "communityRating": 8, "criticRating": 97, "genres": ["Adventure", "Fantasy", "Animation"], "overview": "A young boy and a girl with a magic crystal must race against pirates and foreign agents in a search for a legendary floating castle.", "poster": "assets/movie-castle-in-the-sky-1986-poster.jpg", "backdrop": "assets/movie-castle-in-the-sky-1986-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:23:09.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 10515, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": 91, "rtAudienceLabel": "Upright", "imdb": 8, "imdbVotes": 201545, "tmdb": 7.972, "tmdbVotes": 4868}}, {"id": "movie-citizen-kane-1941", "title": "Citizen Kane", "year": 1941, "type": "movie", "runtimeSeconds": 7173, "seasonCount": null, "communityRating": 7.966, "criticRating": 99, "genres": ["Mystery", "Drama"], "overview": "Newspaper magnate Charles Foster Kane is taken from his mother as a boy and made the ward of a rich industrialist. As a result, every well-meaning, tyrannical or self-destructive move he makes for the rest of his\u2026", "poster": "assets/movie-citizen-kane-1941-poster.jpg", "backdrop": "assets/movie-citizen-kane-1941-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:16.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 15, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Certified Fresh", "rtAudience": 98, "rtAudienceLabel": "Upright", "imdb": 8.2, "imdbVotes": 498321, "tmdb": 7.962, "tmdbVotes": 6324}}, {"id": "movie-conclave-2024", "title": "Conclave", "year": 2024, "type": "movie", "runtimeSeconds": 7230, "seasonCount": null, "communityRating": 7.19, "criticRating": 93, "genres": ["Drama", "Thriller"], "overview": "After the unexpected death of the Pope, Cardinal Lawrence is tasked with managing the covert and ancient ritual of electing a new one. Sequestered in the Vatican with the Catholic Church\u2019s most powerful leaders until\u2026", "poster": "assets/movie-conclave-2024-poster.jpg", "backdrop": "assets/movie-conclave-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:30:41.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 974576, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Certified Fresh", "rtAudience": 86, "rtAudienceLabel": "Upright", "imdb": 7.4, "imdbVotes": 290498, "tmdb": 7.19, "tmdbVotes": 3744}}, {"id": "movie-the-dry-2021", "title": "The Dry", "year": 2021, "type": "movie", "runtimeSeconds": 7037, "seasonCount": null, "communityRating": 6.779, "criticRating": null, "genres": ["Crime", "Thriller", "Mystery"], "overview": "Aaron Falk returns to his drought-stricken hometown to attend a tragic funeral. But his return opens a decades-old wound - the unsolved death of a teenage girl.", "poster": "assets/movie-the-dry-2021-poster.jpg", "backdrop": "assets/movie-the-dry-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:29:20.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 567797, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 89, "rtAudienceLabel": "Upright", "imdb": 6.8, "imdbVotes": 41935, "tmdb": 6.8, "tmdbVotes": 892}}, {"id": "movie-easy-a-2010", "title": "Easy A", "year": 2010, "type": "movie", "runtimeSeconds": 5545, "seasonCount": null, "communityRating": 6.8, "criticRating": 85, "genres": ["Comedy"], "overview": "Olive, an average high school student, sees her below-the-radar existence turn around overnight once she decides to use the school's gossip grapevine to advance her social standing. Now her classmates are turning\u2026", "poster": "assets/movie-easy-a-2010-poster.jpg", "backdrop": "assets/movie-easy-a-2010-backdrop.jpg", "played": true, "playCount": 1, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-17T19:59:04.0000000Z", "lastPlayedAt": "2026-10-17T20:02:51.1083337Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 37735, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 85, "rtCriticsLabel": "Certified Fresh", "rtAudience": 77, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 448780, "tmdb": 6.8, "tmdbVotes": 7619}}, {"id": "movie-el-camino-a-breaking-bad-movie-2019", "title": "El Camino: A Breaking Bad Movie", "year": 2019, "type": "movie", "runtimeSeconds": 7362, "seasonCount": null, "communityRating": 7.055, "criticRating": 92, "genres": ["Crime", "Drama", "Thriller"], "overview": "In the wake of his dramatic escape from captivity, Jesse Pinkman must come to terms with his past in order to forge some kind of future.", "poster": "assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg", "backdrop": "assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-21T23:31:11.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 559969, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Certified Fresh", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 7.3, "imdbVotes": 354488, "tmdb": 7.056, "tmdbVotes": 5950}}, {"id": "movie-emancipation-2022", "title": "Emancipation", "year": 2022, "type": "movie", "runtimeSeconds": 7949, "seasonCount": null, "communityRating": 7.7, "criticRating": 45, "genres": ["Drama", "War", "History"], "overview": "Inspired by the gripping true story of a man who would do anything for his family\u2014and for freedom. When Peter, an enslaved man, risks his life to escape and return to his family, he embarks on a perilous journey of\u2026", "poster": "assets/movie-emancipation-2022-poster.jpg", "backdrop": "assets/movie-emancipation-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T20:37:34.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 715931, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 45, "rtCriticsLabel": "Rotten", "rtAudience": 58, "rtAudienceLabel": "Spilled", "imdb": 6.3, "imdbVotes": 32079, "tmdb": 7.744, "tmdbVotes": 1319}}, {"id": "movie-fighting-with-my-family-2019", "title": "Fighting with My Family", "year": 2019, "type": "movie", "runtimeSeconds": 6509, "seasonCount": null, "communityRating": 6.84, "criticRating": 93, "genres": ["Comedy"], "overview": "Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training\u2026", "poster": "assets/movie-fighting-with-my-family-2019-poster.jpg", "backdrop": "assets/movie-fighting-with-my-family-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T22:10:59.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 445629, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Certified Fresh", "rtAudience": 86, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 94576, "tmdb": 6.837, "tmdbVotes": 1952}}, {"id": "movie-force-of-nature-the-dry-2-2024", "title": "Force of Nature: The Dry 2", "year": 2024, "type": "movie", "runtimeSeconds": 6753, "seasonCount": null, "communityRating": 6.3, "criticRating": 66, "genres": ["Mystery", "Thriller"], "overview": "When five women take part in a corporate hiking retreat and only four come out on the other side, Federal Agents Aaron Falk and Carmen Cooper head deep into the Victorian mountain ranges to investigate in the hopes\u2026", "poster": "assets/movie-force-of-nature-the-dry-2-2024-poster.jpg", "backdrop": "assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:29:49.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 832262, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": 43, "rtAudienceLabel": "Spilled", "imdb": 5.9, "imdbVotes": 10343, "tmdb": 6.269, "tmdbVotes": 305}}, {"id": "movie-free-state-of-jones-2016", "title": "Free State of Jones", "year": 2016, "type": "movie", "runtimeSeconds": 8376, "seasonCount": null, "communityRating": 6.79, "criticRating": 48, "genres": ["Action", "Drama", "History"], "overview": "In 1863, Mississippi farmer Newt Knight serves as a medic for the Confederate Army. Opposed to slavery, Knight would rather help the wounded than fight the Union. After his nephew dies in battle, Newt returns home to\u2026", "poster": "assets/movie-free-state-of-jones-2016-poster.jpg", "backdrop": "assets/movie-free-state-of-jones-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-11T17:56:53.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 316152, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 48, "rtCriticsLabel": "Rotten", "rtAudience": 64, "rtAudienceLabel": "Upright", "imdb": 6.9, "imdbVotes": 68931, "tmdb": 6.788, "tmdbVotes": 1730}}, {"id": "movie-from-up-on-poppy-hill-2011", "title": "From Up on Poppy Hill", "year": 2011, "type": "movie", "runtimeSeconds": 5482, "seasonCount": null, "communityRating": 7.5, "criticRating": 87, "genres": ["Animation", "Drama"], "overview": "Yokohama, 1963. Japan is picking itself up from the devastation of World War II and preparing to host the 1964 Olympics\u2014and the mood is one of both optimism and conflict as the young generation struggles to throw off\u2026", "poster": "assets/movie-from-up-on-poppy-hill-2011-poster.jpg", "backdrop": "assets/movie-from-up-on-poppy-hill-2011-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-07T14:41:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 83389, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 80, "rtAudienceLabel": "Upright", "imdb": 7.4, "imdbVotes": 60769, "tmdb": 7.509, "tmdbVotes": 2026}}, {"id": "movie-ghost-in-the-shell-1995", "title": "Ghost in the Shell", "year": 1995, "type": "movie", "runtimeSeconds": 4959, "seasonCount": null, "communityRating": 7.929, "criticRating": 95, "genres": ["Action", "Animation", "Science Fiction"], "overview": "In the year 2029, the barriers of our world have been broken down by the net and by cybernetics, but this brings new vulnerability to humans in the form of brain-hacking. When a highly-wanted hacker known as 'The\u2026", "poster": "assets/movie-ghost-in-the-shell-1995-poster.jpg", "backdrop": "assets/movie-ghost-in-the-shell-1995-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:57.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 9323, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Certified Fresh", "rtAudience": 90, "rtAudienceLabel": "Upright", "imdb": 7.9, "imdbVotes": 178061, "tmdb": 7.928, "tmdbVotes": 4130}}, {"id": "movie-ghost-in-the-shell-2-innocence-2004", "title": "Ghost in the Shell 2: Innocence", "year": 2004, "type": "movie", "runtimeSeconds": 5950, "seasonCount": null, "communityRating": 7.352, "criticRating": 65, "genres": ["Animation", "Drama", "Science Fiction"], "overview": "Cyborg detective Batou is assigned to investigate a series of murders committed by gynoids\u2014doll-like cyborgs, which all malfunctioned, killed, then self-destructed afterwards. The brains of the gynoids initialize in\u2026", "poster": "assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg", "backdrop": "assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:23:38.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 12140, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 65, "rtCriticsLabel": "Fresh", "rtAudience": 80, "rtAudienceLabel": "Upright", "imdb": 7.4, "imdbVotes": 43280, "tmdb": 7.356, "tmdbVotes": 1049}}, {"id": "movie-the-godfather-1972", "title": "The Godfather", "year": 1972, "type": "movie", "runtimeSeconds": 10603, "seasonCount": null, "communityRating": 8.687, "criticRating": 97, "genres": ["Drama", "Crime"], "overview": "Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son,\u2026", "poster": "assets/movie-the-godfather-1972-poster.jpg", "backdrop": "assets/movie-the-godfather-1972-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:12.0000000Z", "lastPlayedAt": "2026-09-18T19:24:44.9855867Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 238, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Certified Fresh", "rtAudience": 98, "rtAudienceLabel": "Upright", "imdb": 9.2, "imdbVotes": 2262024, "tmdb": 8.688, "tmdbVotes": 23907}}, {"id": "movie-the-godfather-part-ii-1974", "title": "The Godfather Part II", "year": 1974, "type": "movie", "runtimeSeconds": 12031, "seasonCount": null, "communityRating": 8.573, "criticRating": 96, "genres": ["Drama", "Crime"], "overview": "In the continuing saga of the Corleone crime family, a young Vito Corleone grows up in Sicily and in 1910s New York. In the 1950s, Michael Corleone attempts to expand the family business into Las Vegas, Hollywood and\u2026", "poster": "assets/movie-the-godfather-part-ii-1974-poster.jpg", "backdrop": "assets/movie-the-godfather-part-ii-1974-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:18.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 240, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Certified Fresh", "rtAudience": 97, "rtAudienceLabel": "Upright", "imdb": 9, "imdbVotes": 1521308, "tmdb": 8.571, "tmdbVotes": 14559}}, {"id": "movie-the-godfather-part-iii-1990", "title": "The Godfather Part III", "year": 1990, "type": "movie", "runtimeSeconds": 9476, "seasonCount": null, "communityRating": 7.416, "criticRating": 67, "genres": ["Crime", "Drama", "Thriller"], "overview": "In the midst of trying to legitimize his business dealings in 1979 New York and Italy, aging mafia don, Michael Corleone seeks forgiveness for his sins while taking a young protege under his wing.", "poster": "assets/movie-the-godfather-part-iii-1990-poster.jpg", "backdrop": "assets/movie-the-godfather-part-iii-1990-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:26:29.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 242, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 68, "rtCriticsLabel": "Fresh", "rtAudience": 68, "rtAudienceLabel": "Upright", "imdb": 7.5, "imdbVotes": 462448, "tmdb": 7.413, "tmdbVotes": 7159}}, {"id": "movie-gosford-park-2001", "title": "Gosford Park", "year": 2001, "type": "movie", "runtimeSeconds": 8247, "seasonCount": null, "communityRating": 6.815, "criticRating": 87, "genres": ["Drama", "Mystery", "Thriller"], "overview": "In 1930s England, a group of pretentious rich and famous gather together for a weekend of relaxation at a hunting resort. But when a murder occurs, each one of these interesting characters becomes a suspect.", "poster": "assets/movie-gosford-park-2001-poster.jpg", "backdrop": "assets/movie-gosford-park-2001-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:25:49.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 5279, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 78, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 103883, "tmdb": 6.822, "tmdbVotes": 1317}}, {"id": "movie-grave-of-the-fireflies-1988", "title": "Grave of the Fireflies", "year": 1988, "type": "movie", "runtimeSeconds": 5313, "seasonCount": null, "communityRating": 8.45, "criticRating": 100, "genres": ["Animation", "Drama", "War"], "overview": "In the final months of World War II, 14-year-old Seita and his sister Setsuko are orphaned when their mother is killed during an air raid in Kobe, Japan. After a falling out with their aunt, they move into an\u2026", "poster": "assets/movie-grave-of-the-fireflies-1988-poster.jpg", "backdrop": "assets/movie-grave-of-the-fireflies-1988-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:14.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 12477, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Certified Fresh", "rtAudience": 95, "rtAudienceLabel": "Upright", "imdb": 8.5, "imdbVotes": 399802, "tmdb": 8.45, "tmdbVotes": 6938}}, {"id": "movie-the-great-gatsby-2013", "title": "The Great Gatsby", "year": 2013, "type": "movie", "runtimeSeconds": 8525, "seasonCount": null, "communityRating": 7.346, "criticRating": 49, "genres": ["Drama", "Romance"], "overview": "Nick Carraway arrives in New York in 1922, an era of loose morals, glittering jazz and bootleg kings. Chasing his own American Dream, Nick encounters the mysterious millionaire Gatsby and his bewitching cousin Daisy.\u2026", "poster": "assets/movie-the-great-gatsby-2013-poster.jpg", "backdrop": "assets/movie-the-great-gatsby-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:26:50.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 64682, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 48, "rtCriticsLabel": "Rotten", "rtAudience": 67, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 651882, "tmdb": 7.346, "tmdbVotes": 13290}}, {"id": "movie-the-hours-2002", "title": "The Hours", "year": 2002, "type": "movie", "runtimeSeconds": 6887, "seasonCount": null, "communityRating": 7.25, "criticRating": 79, "genres": ["Drama"], "overview": "The story of three women searching for more potent, meaningful lives. Each is alive at a different time and place, all are linked by their yearnings and their fears. Their stories intertwine, and finally come\u2026", "poster": "assets/movie-the-hours-2002-poster.jpg", "backdrop": "assets/movie-the-hours-2002-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:23:27.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 590, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 79, "rtCriticsLabel": "Certified Fresh", "rtAudience": 84, "rtAudienceLabel": "Upright", "imdb": 7.5, "imdbVotes": 148367, "tmdb": 7.249, "tmdbVotes": 1931}}, {"id": "movie-howl-s-moving-castle-2004", "title": "Howl's Moving Castle", "year": 2004, "type": "movie", "runtimeSeconds": 7149, "seasonCount": null, "communityRating": 8.4, "criticRating": 88, "genres": ["Fantasy", "Animation", "Adventure"], "overview": "Sophie, a young milliner, is turned into an elderly woman by a witch who enters her shop and curses her. She encounters a wizard named Howl and gets caught up in his resistance to fighting for the king.", "poster": "assets/movie-howl-s-moving-castle-2004-poster.jpg", "backdrop": "assets/movie-howl-s-moving-castle-2004-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:47.0000000Z", "lastPlayedAt": "2026-05-21T18:46:08.0000000Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 4935, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 93, "rtAudienceLabel": "Upright", "imdb": 8.2, "imdbVotes": 524520, "tmdb": 8.391, "tmdbVotes": 11642}}, {"id": "movie-in-the-heights-2021", "title": "In the Heights", "year": 2021, "type": "movie", "runtimeSeconds": 8579, "seasonCount": null, "communityRating": 7.111, "criticRating": null, "genres": ["Drama", "Romance"], "overview": "The story of Usnavi, a bodega owner who has mixed feelings about closing his store and retiring to the Dominican Republic or staying in Washington Heights.", "poster": "assets/movie-in-the-heights-2021-poster.jpg", "backdrop": "assets/movie-in-the-heights-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:29:11.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 467909, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Certified Fresh", "rtAudience": 94, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 56513, "tmdb": 7.104, "tmdbVotes": 826}}, {"id": "movie-jennifer-s-body-2009", "title": "Jennifer's Body", "year": 2009, "type": "movie", "runtimeSeconds": 6450, "seasonCount": null, "communityRating": 6.012, "criticRating": 47, "genres": ["Horror", "Comedy"], "overview": "Jennifer, a gorgeous, seductive cheerleader takes evil to a whole new level after she's possessed by a sinister demon. Now it's up to her best friend to stop Jennifer's reign of terror before it's too late.", "poster": "assets/movie-jennifer-s-body-2009-poster.jpg", "backdrop": "assets/movie-jennifer-s-body-2009-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-30T03:37:04.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 19994, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 47, "rtCriticsLabel": "Rotten", "rtAudience": 36, "rtAudienceLabel": "Spilled", "imdb": 5.6, "imdbVotes": 179710, "tmdb": 6.013, "tmdbVotes": 4481}}, {"id": "movie-kiki-s-delivery-service-1989", "title": "Kiki's Delivery Service", "year": 1989, "type": "movie", "runtimeSeconds": 6183, "seasonCount": null, "communityRating": 7.839, "criticRating": null, "genres": ["Animation", "Family", "Fantasy"], "overview": "A young witch, on her mandatory year of independent life, finds fitting into a new community difficult while she supports herself by running an air courier service.", "poster": "assets/movie-kiki-s-delivery-service-1989-poster.jpg", "backdrop": "assets/movie-kiki-s-delivery-service-1989-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:20.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 16859, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 98, "rtCriticsLabel": "Certified Fresh", "rtAudience": 89, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 192477, "tmdb": 7.839, "tmdbVotes": 4811}}, {"id": "movie-the-last-of-the-mohicans-1992", "title": "The Last of the Mohicans", "year": 1992, "type": "movie", "runtimeSeconds": 6874, "seasonCount": null, "communityRating": 7.388, "criticRating": 88, "genres": ["History", "War", "Drama"], "overview": "In war-torn colonial America, in the midst of a bloody battle between British, the French and Native American allies, the aristocratic daughter of a British Colonel and her party are captured by a group of Huron\u2026", "poster": "assets/movie-the-last-of-the-mohicans-1992-poster.jpg", "backdrop": "assets/movie-the-last-of-the-mohicans-1992-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T20:59:22.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 9361, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Certified Fresh", "rtAudience": 88, "rtAudienceLabel": "Upright", "imdb": 7.6, "imdbVotes": 210791, "tmdb": 7.39, "tmdbVotes": 3593}}, {"id": "movie-the-last-samurai-2003", "title": "The Last Samurai", "year": 2003, "type": "movie", "runtimeSeconds": 9251, "seasonCount": null, "communityRating": 7.61, "criticRating": 65, "genres": ["Drama", "Action", "War"], "overview": "Nathan Algren is an American hired to instruct the Japanese army in the ways of modern warfare, which finds him learning to respect the samurai and the honorable principles that rule them. Pressed to destroy the\u2026", "poster": "assets/movie-the-last-samurai-2003-poster.jpg", "backdrop": "assets/movie-the-last-samurai-2003-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-26T05:25:10.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 616, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": 83, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 512982, "tmdb": 7.614, "tmdbVotes": 7887}}, {"id": "movie-lemony-snicket-s-a-series-of-unfortunate-events-2004", "title": "Lemony Snicket's A Series of Unfortunate Events", "year": 2004, "type": "movie", "runtimeSeconds": 6479, "seasonCount": null, "communityRating": 6.8, "criticRating": 72, "genres": ["Adventure", "Comedy", "Family"], "overview": "Three wealthy children's parents are killed in a fire. When they are sent to a distant relative, they find out that he is plotting to kill them and seize their fortune.", "poster": "assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg", "backdrop": "assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:26:09.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 11774, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 72, "rtCriticsLabel": "Fresh", "rtAudience": 63, "rtAudienceLabel": "Upright", "imdb": 6.8, "imdbVotes": 233636, "tmdb": 6.82, "tmdbVotes": 5447}}, {"id": "movie-the-lighthouse-2019", "title": "The Lighthouse", "year": 2019, "type": "movie", "runtimeSeconds": 6549, "seasonCount": null, "communityRating": 7.463, "criticRating": 90, "genres": ["Drama", "Fantasy", "Thriller"], "overview": "Two lighthouse keepers try to maintain their sanity while living on a remote and mysterious New England island in the 1890s.", "poster": "assets/movie-the-lighthouse-2019-poster.jpg", "backdrop": "assets/movie-the-lighthouse-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:29:01.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 503919, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 72, "rtAudienceLabel": "Upright", "imdb": 7.3, "imdbVotes": 312344, "tmdb": 7.46, "tmdbVotes": 5895}}, {"id": "movie-locke-2014", "title": "Locke", "year": 2014, "type": "movie", "runtimeSeconds": 5090, "seasonCount": null, "communityRating": 6.888, "criticRating": 91, "genres": ["Drama", "Thriller"], "overview": "Ivan Locke has worked hard to craft a good life for himself. Tonight, that life will collapse around him. On the eve of the biggest challenge of his career, Ivan receives a phone call that sets in motion a series of\u2026", "poster": "assets/movie-locke-2014-poster.jpg", "backdrop": "assets/movie-locke-2014-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T17:38:48.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 210479, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 72, "rtAudienceLabel": "Upright", "imdb": 7.1, "imdbVotes": 171265, "tmdb": 6.889, "tmdbVotes": 3088}}, {"id": "movie-the-matrix-1999", "title": "The Matrix", "year": 1999, "type": "movie", "runtimeSeconds": 8179, "seasonCount": null, "communityRating": 8.259, "criticRating": 83, "genres": ["Action", "Science Fiction"], "overview": "Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.", "poster": "assets/movie-the-matrix-1999-poster.jpg", "backdrop": "assets/movie-the-matrix-1999-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:18:12.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 603, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 83, "rtCriticsLabel": "Certified Fresh", "rtAudience": 85, "rtAudienceLabel": "Upright", "imdb": 8.7, "imdbVotes": 2289808, "tmdb": 8.262, "tmdbVotes": 29153}}, {"id": "movie-the-matrix-reloaded-2003", "title": "The Matrix Reloaded", "year": 2003, "type": "movie", "runtimeSeconds": 8298, "seasonCount": null, "communityRating": 7.085, "criticRating": 74, "genres": ["Adventure", "Action", "Thriller"], "overview": "The Resistance builds in numbers as humans are freed from the Matrix and brought to the city of Zion. Neo discovers his superpowers, including the ability to see the code inside the Matrix. With machine sentinels\u2026", "poster": "assets/movie-the-matrix-reloaded-2003-poster.jpg", "backdrop": "assets/movie-the-matrix-reloaded-2003-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 604, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 73, "rtCriticsLabel": "Certified Fresh", "rtAudience": 72, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 678013, "tmdb": 7.087, "tmdbVotes": 12504}}, {"id": "movie-the-matrix-resurrections-2021", "title": "The Matrix Resurrections", "year": 2021, "type": "movie", "runtimeSeconds": 8875, "seasonCount": null, "communityRating": 6.303, "criticRating": 63, "genres": ["Science Fiction", "Action", "Adventure"], "overview": "Plagued by strange memories, Neo's life takes an unexpected turn when he finds himself back inside the Matrix.", "poster": "assets/movie-the-matrix-resurrections-2021-poster.jpg", "backdrop": "assets/movie-the-matrix-resurrections-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:31:22.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 624860, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 63, "rtCriticsLabel": "Fresh", "rtAudience": 63, "rtAudienceLabel": "Upright", "imdb": 5.6, "imdbVotes": 307780, "tmdb": 6.294, "tmdbVotes": 6842}}, {"id": "movie-the-matrix-revolutions-2003", "title": "The Matrix Revolutions", "year": 2003, "type": "movie", "runtimeSeconds": 7756, "seasonCount": null, "communityRating": 6.758, "criticRating": 33, "genres": ["Adventure", "Action", "Thriller"], "overview": "The human city of Zion defends itself against the massive invasion of the machines as Neo fights to end the war at another front while also opposing the rogue Agent Smith.", "poster": "assets/movie-the-matrix-revolutions-2003-poster.jpg", "backdrop": "assets/movie-the-matrix-revolutions-2003-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:25:47.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 605, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 34, "rtCriticsLabel": "Rotten", "rtAudience": 60, "rtAudienceLabel": "Upright", "imdb": 6.8, "imdbVotes": 581553, "tmdb": 6.762, "tmdbVotes": 11292}}, {"id": "movie-mercy-2026", "title": "Mercy", "year": 2026, "type": "movie", "runtimeSeconds": 5968, "seasonCount": null, "communityRating": 7.077, "criticRating": null, "genres": ["Science Fiction", "Thriller", "Crime"], "overview": "In the near future, a detective stands on trial accused of murdering his wife. He has ninety minutes to prove his innocence to the advanced AI Judge he once championed, before it determines his fate.", "poster": "assets/movie-mercy-2026-poster.jpg", "backdrop": "assets/movie-mercy-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:31:40.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1236153, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 24, "rtCriticsLabel": "Rotten", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 6.2, "imdbVotes": 83878, "tmdb": 7.054, "tmdbVotes": 1812}}, {"id": "movie-normal-2026", "title": "Normal", "year": 2026, "type": "movie", "runtimeSeconds": 5514, "seasonCount": null, "communityRating": 7.267, "criticRating": 77, "genres": ["Action", "Crime", "Thriller"], "overview": "An unassuming substitute sheriff with a troubled past responds to a bank robbery in a small, sleepy town and unknowingly uncovers something far more explosive.", "poster": "assets/movie-normal-2026-poster.jpg", "backdrop": "assets/movie-normal-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-08-21T08:33:18.0000000Z", "lastPlayedAt": "2026-09-24T23:07:52.1161581Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1242332, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 78, "rtCriticsLabel": "Certified Fresh", "rtAudience": 75, "rtAudienceLabel": "Upright", "imdb": 6.3, "imdbVotes": 37501, "tmdb": 7.241, "tmdbVotes": 786}}, {"id": "movie-outlaw-king-2018", "title": "Outlaw King", "year": 2018, "type": "movie", "runtimeSeconds": 7297, "seasonCount": null, "communityRating": 6.829, "criticRating": 63, "genres": ["Action", "History", "War"], "overview": "Forced into exile by the English after being crowned King of Scotland, legendary warrior Robert the Bruce fights to reclaim the throne.", "poster": "assets/movie-outlaw-king-2018-poster.jpg", "backdrop": "assets/movie-outlaw-king-2018-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:28:32.0000000Z", "lastPlayedAt": "2026-07-26T22:35:35.0000000Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 454227, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 63, "rtCriticsLabel": "Fresh", "rtAudience": 69, "rtAudienceLabel": "Upright", "imdb": 6.9, "imdbVotes": 89876, "tmdb": 6.844, "tmdbVotes": 1780}}, {"id": "movie-panic-room-2002", "title": "Panic Room", "year": 2002, "type": "movie", "runtimeSeconds": 6717, "seasonCount": null, "communityRating": 6.794, "criticRating": 76, "genres": ["Crime", "Drama", "Thriller"], "overview": "Trapped in their New York brownstone's panic room, a hidden chamber built as a sanctuary in the event of break-ins, newly divorced Meg Altman and her young daughter Sarah play a deadly game of cat-and-mouse with\u2026", "poster": "assets/movie-panic-room-2002-poster.jpg", "backdrop": "assets/movie-panic-room-2002-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-19T20:50:01.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 4547, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 76, "rtCriticsLabel": "Certified Fresh", "rtAudience": 64, "rtAudienceLabel": "Upright", "imdb": 6.8, "imdbVotes": 317468, "tmdb": 6.796, "tmdbVotes": 5653}}, {"id": "movie-the-patriot-2000", "title": "The Patriot", "year": 2000, "type": "movie", "runtimeSeconds": 9887, "seasonCount": null, "communityRating": 7.214, "criticRating": 61, "genres": ["Drama", "History", "War"], "overview": "After proving himself on the field of battle in the French and Indian War, Benjamin Martin wants nothing more to do with such things, preferring the simple life of a farmer. But when his son Gabriel enlists in the\u2026", "poster": "assets/movie-the-patriot-2000-poster.jpg", "backdrop": "assets/movie-the-patriot-2000-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T20:50:00.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 2024, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 61, "rtCriticsLabel": "Fresh", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 317639, "tmdb": 7.215, "tmdbVotes": 4535}}, {"id": "movie-peaky-blinders-the-immortal-man-2026", "title": "Peaky Blinders: The Immortal Man", "year": 2026, "type": "movie", "runtimeSeconds": 6844, "seasonCount": null, "communityRating": 7.187, "criticRating": 90, "genres": ["Crime", "Drama"], "overview": "After his estranged son gets embroiled in a Nazi plot, self-exiled gangster Tommy Shelby must return to Birmingham to save his family \u2014 and his nation.", "poster": "assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg", "backdrop": "assets/movie-peaky-blinders-the-immortal-man-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:30:01.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 875828, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 6.5, "imdbVotes": 92130, "tmdb": 7.189, "tmdbVotes": 1304}}, {"id": "movie-ponyo-2008", "title": "Ponyo", "year": 2008, "type": "movie", "runtimeSeconds": 6530, "seasonCount": null, "communityRating": 7.763, "criticRating": 91, "genres": ["Animation", "Fantasy", "Family"], "overview": "When Sosuke, a young boy who lives on a clifftop overlooking the sea, rescues a stranded goldfish named Ponyo, he discovers more than he bargained for. Ponyo is a curious, energetic young creature who yearns to be\u2026", "poster": "assets/movie-ponyo-2008-poster.jpg", "backdrop": "assets/movie-ponyo-2008-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:23:53.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 12429, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Certified Fresh", "rtAudience": 84, "rtAudienceLabel": "Upright", "imdb": 7.6, "imdbVotes": 190294, "tmdb": 7.759, "tmdbVotes": 5050}}, {"id": "movie-porco-rosso-1992", "title": "Porco Rosso", "year": 1992, "type": "movie", "runtimeSeconds": 5600, "seasonCount": null, "communityRating": 7.794, "criticRating": 96, "genres": ["Animation", "Adventure", "Fantasy"], "overview": "In Italy in the 1930s, sky pirates in biplanes terrorize wealthy cruise ships as they sail the Adriatic Sea. The only pilot brave enough to stop the scourge is the mysterious Porco Rosso, a former World War I flying\u2026", "poster": "assets/movie-porco-rosso-1992-poster.jpg", "backdrop": "assets/movie-porco-rosso-1992-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:23:22.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 11621, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 7.7, "imdbVotes": 119396, "tmdb": 7.789, "tmdbVotes": 3879}}, {"id": "movie-princess-mononoke-1997", "title": "Princess Mononoke", "year": 1997, "type": "movie", "runtimeSeconds": 8003, "seasonCount": null, "communityRating": 8.321, "criticRating": 93, "genres": ["Adventure", "Fantasy", "Animation"], "overview": "Ashitaka, a prince of the disappearing Emishi people, is cursed by a demonized boar god and must journey to the west to find a cure. Along the way, he encounters San, a young human woman fighting to protect the\u2026", "poster": "assets/movie-princess-mononoke-1997-poster.jpg", "backdrop": "assets/movie-princess-mononoke-1997-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:28.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 128, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Certified Fresh", "rtAudience": 99, "rtAudienceLabel": "Upright", "imdb": 8.3, "imdbVotes": 493735, "tmdb": 8.321, "tmdbVotes": 9438}}, {"id": "movie-prisoners-2013", "title": "Prisoners", "year": 2013, "type": "movie", "runtimeSeconds": 9192, "seasonCount": null, "communityRating": 8.106, "criticRating": 81, "genres": ["Drama", "Thriller", "Crime"], "overview": "After his six-year-old daughter is abducted, a desperate father takes justice into his own hands when he loses faith in the detective leading the investigation, setting both men on a collision course as the search\u2026", "poster": "assets/movie-prisoners-2013-poster.jpg", "backdrop": "assets/movie-prisoners-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:27:33.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 146233, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 82, "rtCriticsLabel": "Certified Fresh", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 8.2, "imdbVotes": 963062, "tmdb": 8.105, "tmdbVotes": 13722}}, {"id": "movie-racing-stripes-2005", "title": "Racing Stripes", "year": 2005, "type": "movie", "runtimeSeconds": 6117, "seasonCount": null, "communityRating": 5.945, "criticRating": 35, "genres": ["Comedy", "Family"], "overview": "Shattered illusions are hard to repair -- especially for a good-hearted zebra named Stripes who's spent his life on a Kentucky farm amidst the sorely mistaken notion that he's a debonair thoroughbred. Once he faces\u2026", "poster": "assets/movie-racing-stripes-2005-poster.jpg", "backdrop": "assets/movie-racing-stripes-2005-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:26:02.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 6439, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 35, "rtCriticsLabel": "Rotten", "rtAudience": 42, "rtAudienceLabel": "Spilled", "imdb": 5.3, "imdbVotes": 19569, "tmdb": 5.962, "tmdbVotes": 705}}, {"id": "movie-requiem-for-a-dream-2000", "title": "Requiem for a Dream", "year": 2000, "type": "movie", "runtimeSeconds": 6086, "seasonCount": null, "communityRating": 8.005, "criticRating": 80, "genres": ["Crime", "Drama"], "overview": "The drug-induced utopias of four Coney Island residents are shattered when their addictions run deep.", "poster": "assets/movie-requiem-for-a-dream-2000-poster.jpg", "backdrop": "assets/movie-requiem-for-a-dream-2000-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:15.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 641, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Certified Fresh", "rtAudience": 93, "rtAudienceLabel": "Upright", "imdb": 8.3, "imdbVotes": 989090, "tmdb": 8, "tmdbVotes": 11454}}, {"id": "movie-rogue-one-a-star-wars-story-2016", "title": "Rogue One: A Star Wars Story", "year": 2016, "type": "movie", "runtimeSeconds": 8038, "seasonCount": null, "communityRating": 7.51, "criticRating": 84, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.", "poster": "assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg", "backdrop": "assets/movie-rogue-one-a-star-wars-story-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:28:00.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 330459, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 84, "rtCriticsLabel": "Certified Fresh", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 768365, "tmdb": 7.512, "tmdbVotes": 16937}}, {"id": "movie-secondhand-lions-2003", "title": "Secondhand Lions", "year": 2003, "type": "movie", "runtimeSeconds": 6547, "seasonCount": null, "communityRating": 7.237, "criticRating": 60, "genres": ["Family", "Comedy", "Drama"], "overview": "The comedic adventures of an introverted boy left on the doorstep of a pair of reluctant, eccentric great-uncles, whose exotic remembrances stir the boy's spirit and re-ignite the men's lives.", "poster": "assets/movie-secondhand-lions-2003-poster.jpg", "backdrop": "assets/movie-secondhand-lions-2003-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-12T07:16:13.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 13156, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 59, "rtCriticsLabel": "Rotten", "rtAudience": 84, "rtAudienceLabel": "Upright", "imdb": 7.5, "imdbVotes": 66911, "tmdb": 7.242, "tmdbVotes": 872}}, {"id": "movie-the-secret-life-of-pets-2-2019", "title": "The Secret Life of Pets 2", "year": 2019, "type": "movie", "runtimeSeconds": 5164, "seasonCount": null, "communityRating": 6.95, "criticRating": 60, "genres": ["Animation", "Comedy", "Family"], "overview": "Max the terrier must cope with some major life changes when his owner gets married and has a baby. When the family takes a trip to the countryside, nervous Max has numerous run-ins with canine-intolerant cows,\u2026", "poster": "assets/movie-the-secret-life-of-pets-2-2019-poster.jpg", "backdrop": "assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-05T07:00:53.0000000Z", "lastPlayedAt": "2026-09-17T18:07:00.7024231Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 412117, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 60, "rtCriticsLabel": "Fresh", "rtAudience": 90, "rtAudienceLabel": "Upright", "imdb": 6.4, "imdbVotes": 80464, "tmdb": 6.948, "tmdbVotes": 3450}}, {"id": "movie-self-reliance-2024", "title": "Self Reliance", "year": 2024, "type": "movie", "runtimeSeconds": 5461, "seasonCount": null, "communityRating": 5.888, "criticRating": 73, "genres": ["Comedy", "Thriller"], "overview": "When a man is offered a million dollars to play a game in which hunters try to kill him, he thinks he has found the perfect loophole: they can only attack when he\u2019s alone. His only problem is that none of his friends\u2026", "poster": "assets/movie-self-reliance-2024-poster.jpg", "backdrop": "assets/movie-self-reliance-2024-backdrop.jpg", "played": false, "playCount": 1, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:31:00.0000000Z", "lastPlayedAt": "2026-10-02T19:59:16.9286868Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1072876, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 73, "rtCriticsLabel": "Certified Fresh", "rtAudience": 56, "rtAudienceLabel": "Spilled", "imdb": 6, "imdbVotes": 23771, "tmdb": 5.866, "tmdbVotes": 346}}, {"id": "movie-serpico-1973", "title": "Serpico", "year": 1973, "type": "movie", "runtimeSeconds": 7805, "seasonCount": null, "communityRating": 7.5, "criticRating": 93, "genres": ["Crime", "Drama", "History"], "overview": "New York cop Frank Serpico blows the whistle on the rampant corruption in the force only to have his comrades turn against him.", "poster": "assets/movie-serpico-1973-poster.jpg", "backdrop": "assets/movie-serpico-1973-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T21:45:02.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 9040, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Certified Fresh", "rtAudience": 88, "rtAudienceLabel": "Upright", "imdb": 7.7, "imdbVotes": 149750, "tmdb": 7.525, "tmdbVotes": 2323}}, {"id": "movie-shaft-2000", "title": "Shaft", "year": 2000, "type": "movie", "runtimeSeconds": 5960, "seasonCount": null, "communityRating": 6.026, "criticRating": 67, "genres": ["Action", "Adventure", "Crime"], "overview": "New York police detective John Shaft arrests Walter Wade Jr. for a racially motivated slaying. But the only eyewitness disappears, and Wade jumps bail for Switzerland. Two years later Wade returns to face trial,\u2026", "poster": "assets/movie-shaft-2000-poster.jpg", "backdrop": "assets/movie-shaft-2000-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T21:30:40.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 479, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 68, "rtCriticsLabel": "Fresh", "rtAudience": 40, "rtAudienceLabel": "Spilled", "imdb": 6, "imdbVotes": 86257, "tmdb": 6.024, "tmdbVotes": 1531}}, {"id": "movie-a-silent-voice-the-movie-2016", "title": "A Silent Voice: The Movie", "year": 2016, "type": "movie", "runtimeSeconds": 7804, "seasonCount": null, "communityRating": 8.398, "criticRating": null, "genres": ["Animation", "Drama", "Romance"], "overview": "Shouya Ishida starts bullying the new girl in class, Shouko Nishimiya, because she is deaf. But as the teasing continues, the rest of the class starts to turn on Shouya for his lack of compassion. When they leave\u2026", "poster": "assets/movie-a-silent-voice-the-movie-2016-poster.jpg", "backdrop": "assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T17:39:46.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 378064, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": 94, "rtAudienceLabel": "Upright", "imdb": 8.2, "imdbVotes": 151926, "tmdb": 8.396, "tmdbVotes": 4764}}, {"id": "movie-sinbad-legend-of-the-seven-seas-2003", "title": "Sinbad: Legend of the Seven Seas", "year": 2003, "type": "movie", "runtimeSeconds": 5143, "seasonCount": null, "communityRating": 7.067, "criticRating": 44, "genres": ["Action", "Adventure", "Animation"], "overview": "The sailor of legend is framed by the goddess Eris for the theft of the Book of Peace, and must travel to her realm at the end of the world to retrieve it and save the life of his childhood friend Prince Proteus.", "poster": "assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg", "backdrop": "assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:26:25.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 14411, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 46, "rtCriticsLabel": "Rotten", "rtAudience": 57, "rtAudienceLabel": "Spilled", "imdb": 6.8, "imdbVotes": 65298, "tmdb": 7.071, "tmdbVotes": 2323}}, {"id": "movie-sleeping-with-other-people-2015", "title": "Sleeping with Other People", "year": 2015, "type": "movie", "runtimeSeconds": 6076, "seasonCount": null, "communityRating": 6.274, "criticRating": 64, "genres": ["Comedy", "Romance"], "overview": "Can two serial cheaters get a second chance at love? After a one-night stand in college, New Yorkers Lainey and Jake meet by chance twelve years later and discover they each have the same problem: because of their\u2026", "poster": "assets/movie-sleeping-with-other-people-2015-poster.jpg", "backdrop": "assets/movie-sleeping-with-other-people-2015-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:27:42.0000000Z", "lastPlayedAt": "2026-07-17T17:50:44.0000000Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 288036, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 64, "rtCriticsLabel": "Fresh", "rtAudience": 56, "rtAudienceLabel": "Spilled", "imdb": 6.5, "imdbVotes": 52275, "tmdb": 6.275, "tmdbVotes": 1020}}, {"id": "movie-snowpiercer-2013", "title": "Snowpiercer", "year": 2013, "type": "movie", "runtimeSeconds": 7573, "seasonCount": null, "communityRating": 6.909, "criticRating": 94, "genres": ["Action", "Science Fiction", "Drama"], "overview": "In a future where a failed global-warming experiment kills off most life on the planet, a class system evolves aboard the Snowpiercer; a train that travels around the globe via a perpetual-motion engine.", "poster": "assets/movie-snowpiercer-2013-poster.jpg", "backdrop": "assets/movie-snowpiercer-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:27:07.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 110415, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Certified Fresh", "rtAudience": 72, "rtAudienceLabel": "Upright", "imdb": 7.1, "imdbVotes": 426976, "tmdb": 6.9, "tmdbVotes": 10807}}, {"id": "movie-spider-man-brand-new-day-2026", "title": "Spider-Man: Brand New Day", "year": 2026, "type": "movie", "runtimeSeconds": 8701, "seasonCount": null, "communityRating": 8.374, "criticRating": 90, "genres": ["Science Fiction", "Action", "Adventure"], "overview": "Fighting crime full-time as Spider-Man in a world that doesn't remember him\u2014and the pressure of seeing his old friends move on without him\u2014sparks a change in Peter Parker he may not have the power to control. But\u2026", "poster": "assets/movie-spider-man-brand-new-day-2026-poster.jpg", "backdrop": "assets/movie-spider-man-brand-new-day-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T14:14:27.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 969681, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 97, "rtAudienceLabel": "Upright", "imdb": 7.9, "imdbVotes": 366886, "tmdb": 8.5, "tmdbVotes": 6537}}, {"id": "movie-spider-man-far-from-home-2019", "title": "Spider-Man: Far From Home", "year": 2019, "type": "movie", "runtimeSeconds": 7762, "seasonCount": null, "communityRating": 7.389, "criticRating": 91, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and\u2026", "poster": "assets/movie-spider-man-far-from-home-2019-poster.jpg", "backdrop": "assets/movie-spider-man-far-from-home-2019-backdrop.jpg", "played": true, "playCount": 1, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T14:25:47.0000000Z", "lastPlayedAt": "2026-10-10T17:50:35.6193119Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 429617, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Certified Fresh", "rtAudience": 95, "rtAudienceLabel": "Upright", "imdb": 7.3, "imdbVotes": 682887, "tmdb": 7.387, "tmdbVotes": 17847}}, {"id": "movie-spider-man-homecoming-2017", "title": "Spider-Man: Homecoming", "year": 2017, "type": "movie", "runtimeSeconds": 8008, "seasonCount": null, "communityRating": 7.333, "criticRating": 92, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Following the events of Captain America: Civil War, Peter Parker, with the help of his mentor Tony Stark, tries to balance his life as an ordinary high school student in Queens, New York City, with fighting crime as\u2026", "poster": "assets/movie-spider-man-homecoming-2017-poster.jpg", "backdrop": "assets/movie-spider-man-homecoming-2017-backdrop.jpg", "played": true, "playCount": 1, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T14:33:13.0000000Z", "lastPlayedAt": "2026-10-10T20:31:23.4444213Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 315635, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Certified Fresh", "rtAudience": 87, "rtAudienceLabel": "Upright", "imdb": 7.4, "imdbVotes": 840974, "tmdb": 7.332, "tmdbVotes": 24183}}, {"id": "movie-spider-man-no-way-home-2021", "title": "Spider-Man: No Way Home", "year": 2021, "type": "movie", "runtimeSeconds": 8890, "seasonCount": null, "communityRating": 7.947, "criticRating": 93, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Peter Parker is unmasked and no longer able to separate his normal life from the high-stakes of being a super-hero. When he asks for help from Doctor Strange the stakes become even more dangerous, forcing him to\u2026", "poster": "assets/movie-spider-man-no-way-home-2021-poster.jpg", "backdrop": "assets/movie-spider-man-no-way-home-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-10T14:21:05.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 634649, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Certified Fresh", "rtAudience": 97, "rtAudienceLabel": "Upright", "imdb": 8.1, "imdbVotes": 1082660, "tmdb": 7.947, "tmdbVotes": 23505}}, {"id": "movie-spirited-away-2001", "title": "Spirited Away", "year": 2001, "type": "movie", "runtimeSeconds": 7472, "seasonCount": null, "communityRating": 8.535, "criticRating": 96, "genres": ["Animation", "Family", "Fantasy"], "overview": "A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family.", "poster": "assets/movie-spirited-away-2001-poster.jpg", "backdrop": "assets/movie-spirited-away-2001-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:42.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 129, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Certified Fresh", "rtAudience": 99, "rtAudienceLabel": "Upright", "imdb": 8.6, "imdbVotes": 987658, "tmdb": 8.536, "tmdbVotes": 19176}}, {"id": "movie-the-substance-2024", "title": "The Substance", "year": 2024, "type": "movie", "runtimeSeconds": 8531, "seasonCount": null, "communityRating": 7.135, "criticRating": 89, "genres": ["Horror", "Science Fiction"], "overview": "A fading celebrity decides to use a black market drug, a cell-replicating substance that temporarily creates a younger, better version of herself.", "poster": "assets/movie-the-substance-2024-poster.jpg", "backdrop": "assets/movie-the-substance-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:30:22.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 933260, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 76, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 466542, "tmdb": 7.13, "tmdbVotes": 6746}}, {"id": "movie-the-tale-of-the-princess-kaguya-2013", "title": "The Tale of The Princess Kaguya", "year": 2013, "type": "movie", "runtimeSeconds": 8218, "seasonCount": null, "communityRating": 8.1, "criticRating": 100, "genres": ["Animation", "Drama", "Fantasy"], "overview": "Found inside a shining stalk of bamboo by an old bamboo cutter and his wife, a tiny girl grows rapidly into an exquisite young lady. The mysterious young princess enthrals all who encounter her. But, ultimately, she\u2026", "poster": "assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg", "backdrop": "assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-07T13:40:15.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 149871, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Certified Fresh", "rtAudience": 90, "rtAudienceLabel": "Upright", "imdb": 8, "imdbVotes": 63574, "tmdb": 8.083, "tmdbVotes": 2079}}, {"id": "movie-taxi-driver-1976", "title": "Taxi Driver", "year": 1976, "type": "movie", "runtimeSeconds": 6831, "seasonCount": null, "communityRating": 8.121, "criticRating": 89, "genres": ["Crime", "Drama"], "overview": "Suffering from insomnia, disturbed loner Travis Bickle takes a job as a New York City cabbie, haunting the streets nightly, growing increasingly detached from reality as he dreams of cleaning up the filthy city.", "poster": "assets/movie-taxi-driver-1976-poster.jpg", "backdrop": "assets/movie-taxi-driver-1976-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:23:04.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 103, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 93, "rtAudienceLabel": "Upright", "imdb": 8.2, "imdbVotes": 1046700, "tmdb": 8.116, "tmdbVotes": 13973}}, {"id": "movie-triangle-of-sadness-2022", "title": "Triangle of Sadness", "year": 2022, "type": "movie", "runtimeSeconds": 8833, "seasonCount": null, "communityRating": 7.005, "criticRating": 71, "genres": ["Comedy", "Drama"], "overview": "A celebrity model couple are invited on a luxury cruise for the uber-rich, helmed by an unhinged, alcoholic captain. What first appears Instagrammable ends catastrophically, leaving the survivors stranded on a desert\u2026", "poster": "assets/movie-triangle-of-sadness-2022-poster.jpg", "backdrop": "assets/movie-triangle-of-sadness-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:29:00.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 497828, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 72, "rtCriticsLabel": "Fresh", "rtAudience": 80, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 221069, "tmdb": 7, "tmdbVotes": 3104}}, {"id": "movie-tsotsi-2005", "title": "Tsotsi", "year": 2005, "type": "movie", "runtimeSeconds": 5663, "seasonCount": null, "communityRating": 6.937, "criticRating": 82, "genres": ["Crime", "Drama"], "overview": "A young South African boy from the Johannesburg ghetto named Tsotsi, meaning Gangster, leaves home as a child to get away from his helpless parents. Now a teenage thug, Tsotsi finds a baby in the back seat of a car\u2026", "poster": "assets/movie-tsotsi-2005-poster.jpg", "backdrop": "assets/movie-tsotsi-2005-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:58.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 868, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 82, "rtCriticsLabel": "Certified Fresh", "rtAudience": 86, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 32526, "tmdb": 6.941, "tmdbVotes": 372}}, {"id": "movie-unknown-number-the-high-school-catfish-2025", "title": "Unknown Number: The High School Catfish", "year": 2025, "type": "movie", "runtimeSeconds": 5691, "seasonCount": null, "communityRating": 6.746, "criticRating": 85, "genres": ["Documentary"], "overview": "Vulgar, taunting texts blow up the phones of a teen and her boyfriend. Who's sending them \u2014 and why? This twisty documentary reveals the shocking answer.", "poster": "assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg", "backdrop": "assets/movie-unknown-number-the-high-school-catfish-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:32:10.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1517772, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 85, "rtCriticsLabel": "Fresh", "rtAudience": 69, "rtAudienceLabel": "Upright", "imdb": 6.7, "imdbVotes": 23039, "tmdb": 6.737, "tmdbVotes": 257}}, {"id": "movie-the-uprising-2026", "title": "The Uprising", "year": 2026, "type": "movie", "runtimeSeconds": 7684, "seasonCount": null, "communityRating": 7.846, "criticRating": 55, "genres": ["Action", "Drama", "History"], "overview": "As war and plague devastate 14th-century England, a humble peasant ignites a rebellion, uniting an army of commoners to defy the King\u2019s might in a desperate fight for justice and survival.", "poster": "assets/movie-the-uprising-2026-poster.jpg", "backdrop": "assets/movie-the-uprising-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-02T16:37:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 977942, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 57, "rtCriticsLabel": "Rotten", "rtAudience": 65, "rtAudienceLabel": "Upright", "imdb": 6.2, "imdbVotes": 6622, "tmdb": 7.8, "tmdbVotes": 404}}, {"id": "movie-vice-2018", "title": "Vice", "year": 2018, "type": "movie", "runtimeSeconds": 7948, "seasonCount": null, "communityRating": 7.013, "criticRating": 64, "genres": ["Drama", "Comedy", "History"], "overview": "George W. Bush picks Dick Cheney, the CEO of Halliburton Co., to be his Republican running mate in the 2000 presidential election. No stranger to politics, Cheney's impressive r\u00e9sum\u00e9 includes stints as White House\u2026", "poster": "assets/movie-vice-2018-poster.jpg", "backdrop": "assets/movie-vice-2018-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:28:30.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 429197, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 64, "rtCriticsLabel": "Fresh", "rtAudience": 61, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 177299, "tmdb": 7.014, "tmdbVotes": 3723}}, {"id": "movie-warriors-of-the-wind-1984", "title": "Warriors of the Wind", "year": 1984, "type": "movie", "runtimeSeconds": 7022, "seasonCount": null, "communityRating": 7.941, "criticRating": 87, "genres": ["Adventure", "Animation", "Fantasy"], "overview": "After a global war, the seaside kingdom known as the Valley of the Wind remains one of the last strongholds on Earth untouched by a poisonous jungle and the powerful insects that guard it. Led by the courageous\u2026", "poster": "assets/movie-warriors-of-the-wind-1984-poster.jpg", "backdrop": "assets/movie-warriors-of-the-wind-1984-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:22:20.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 81, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Fresh", "rtAudience": 91, "rtAudienceLabel": "Upright", "imdb": 8, "imdbVotes": 200154, "tmdb": 7.94, "tmdbVotes": 4260}}, {"id": "movie-we-bury-the-dead-2026", "title": "We Bury the Dead", "year": 2026, "type": "movie", "runtimeSeconds": 5693, "seasonCount": null, "communityRating": 6.153, "criticRating": 87, "genres": ["Horror", "Thriller"], "overview": "After a catastrophic military disaster reanimates the dead, Ava searches for her missing husband amidst an increasingly hostile world.", "poster": "assets/movie-we-bury-the-dead-2026-poster.jpg", "backdrop": "assets/movie-we-bury-the-dead-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-08-26T05:36:00.4272328Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1198984, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Certified Fresh", "rtAudience": 45, "rtAudienceLabel": "Spilled", "imdb": 5.6, "imdbVotes": 20693, "tmdb": 6.137, "tmdbVotes": 433}}, {"id": "movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022", "title": "White Hot: The Rise & Fall of Abercrombie & Fitch", "year": 2022, "type": "movie", "runtimeSeconds": 5187, "seasonCount": null, "communityRating": 5.663, "criticRating": 66, "genres": ["Documentary"], "overview": "All the cool kids were wearing it. This documentary explores A&F's pop culture reign in the late '90s and early 2000s and how it thrived on exclusion.", "poster": "assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg", "backdrop": "assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:30:34.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 953371, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": 29, "rtAudienceLabel": "Spilled", "imdb": 5.8, "imdbVotes": 5655, "tmdb": 5.7, "tmdbVotes": 102}}, {"id": "movie-the-wild-robot-2024", "title": "The Wild Robot", "year": 2024, "type": "movie", "runtimeSeconds": 6101, "seasonCount": null, "communityRating": 8.308, "criticRating": 97, "genres": ["Family", "Animation", "Science Fiction"], "overview": "After a shipwreck, an intelligent robot called Roz is stranded on an uninhabited island. To survive the harsh environment, Roz bonds with the island's animals and cares for an orphaned baby goose.", "poster": "assets/movie-the-wild-robot-2024-poster.jpg", "backdrop": "assets/movie-the-wild-robot-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:31:49.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1184918, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Certified Fresh", "rtAudience": 98, "rtAudienceLabel": "Upright", "imdb": 8.1, "imdbVotes": 241134, "tmdb": 8.309, "tmdbVotes": 6819}}, {"id": "movie-the-wind-rises-2013", "title": "The Wind Rises", "year": 2013, "type": "movie", "runtimeSeconds": 8031, "seasonCount": null, "communityRating": 7.784, "criticRating": 88, "genres": ["Animation", "Drama", "History"], "overview": "A lifelong love of flight inspires Japanese aviation engineer Jiro Horikoshi, whose storied career includes the creation of the A-6M World War II fighter plane.", "poster": "assets/movie-the-wind-rises-2013-poster.jpg", "backdrop": "assets/movie-the-wind-rises-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T10:24:38.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 149870, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 85, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 115756, "tmdb": 7.783, "tmdbVotes": 3536}}, {"id": "movie-the-witcher-nightmare-of-the-wolf-2021", "title": "The Witcher: Nightmare of the Wolf", "year": 2021, "type": "movie", "runtimeSeconds": 5015, "seasonCount": null, "communityRating": 7.422, "criticRating": 100, "genres": ["Animation", "Action", "Fantasy"], "overview": "Escaping from poverty to become a witcher, Vesemir slays monsters for coin and glory, but when a new menace rises, he must face the demons of his past.", "poster": "assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg", "backdrop": "assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-26T04:42:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 666243, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": 83, "rtAudienceLabel": "Upright", "imdb": 7.2, "imdbVotes": 55759, "tmdb": 7.407, "tmdbVotes": 1159}}, {"id": "movie-the-witcher-sirens-of-the-deep-2025", "title": "The Witcher: Sirens of the Deep", "year": 2025, "type": "movie", "runtimeSeconds": 5517, "seasonCount": null, "communityRating": 6.8, "criticRating": 60, "genres": ["Animation", "Fantasy", "Action"], "overview": "When human sailors are attacked by mysterious creatures of the deep, only one person can stop the war between land and sea: the Witcher, Geralt of Rivia.", "poster": "assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg", "backdrop": "assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-26T05:03:26.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1203329, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 60, "rtCriticsLabel": "Fresh", "rtAudience": 37, "rtAudienceLabel": "Spilled", "imdb": 6.1, "imdbVotes": 11799, "tmdb": 6.741, "tmdbVotes": 303}}], "shows": [{"id": "tv-adults-2025", "title": "Adults", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 6.495, "criticRating": null, "genres": ["Comedy"], "overview": "A group of twenty-somethings in New York trying to be good people, despite being neither \"good\" nor \"people\" yet.", "poster": "assets/tv-adults-2025-poster.jpg", "backdrop": "assets/tv-adults-2025-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-23T21:01:41.1814765Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 260458, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.5, "tmdbVotes": 102}}, {"id": "tv-american-crime-story-2016", "title": "American Crime Story", "year": 2016, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.573, "criticRating": null, "genres": ["Crime", "Drama"], "overview": "An anthology series centered around some of history's most famous criminal investigations.", "poster": "assets/tv-american-crime-story-2016-poster.jpg", "backdrop": "assets/tv-american-crime-story-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0490488Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 64513, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 84, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.585, "tmdbVotes": 1094}}, {"id": "tv-american-primeval-2025", "title": "American Primeval", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.691, "criticRating": null, "genres": ["Western", "Drama", "Action & Adventure"], "overview": "A mother and son fleeing from their past form a found family while confronting a harsh landscape of freedom and cruelty in the American West.", "poster": "assets/tv-american-primeval-2025-poster.jpg", "backdrop": "assets/tv-american-primeval-2025-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0573221Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 215866, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 71, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.741, "tmdbVotes": 538}}, {"id": "tv-arrested-development-2003", "title": "Arrested Development", "year": 2003, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 7.925, "criticRating": null, "genres": ["Comedy"], "overview": "The story of a wealthy family that lost everything, and the one son who had no choice but to keep them all together.", "poster": "assets/tv-arrested-development-2003-poster.jpg", "backdrop": "assets/tv-arrested-development-2003-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0513048Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 4589, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 75, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.906, "tmdbVotes": 1793}}, {"id": "tv-avatar-the-last-airbender-2005", "title": "Avatar: The Last Airbender", "year": 2005, "type": "tv", "runtimeSeconds": 1440, "seasonCount": 3, "communityRating": 8.78, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In a war-torn world of elemental magic, a young boy reawakens to undertake a dangerous mystic quest to fulfill his destiny as the Avatar, and bring peace to the world.", "poster": "assets/tv-avatar-the-last-airbender-2005-poster.jpg", "backdrop": "assets/tv-avatar-the-last-airbender-2005-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0504536Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 246, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.787, "tmdbVotes": 5215}}, {"id": "tv-b-b-vol-liefde-2021", "title": "B&B Vol Liefde", "year": 2021, "type": "tv", "runtimeSeconds": 3720, "seasonCount": 1, "communityRating": 7.2, "criticRating": null, "genres": ["Reality"], "overview": "B&B Vol Liefde is a heartwarming adventure, full of romance, excitement, and unforgettable moments. Adventurous Dutch B&B owners abroad throw their doors wide open and let their hearts speak, determined to find that\u2026", "poster": "assets/tv-b-b-vol-liefde-2021-poster.jpg", "backdrop": "assets/tv-b-b-vol-liefde-2021-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-31T22:30:11.129657Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 124428, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.2, "tmdbVotes": 10}}, {"id": "tv-the-bear-2022", "title": "The Bear", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 8.139, "criticRating": null, "genres": ["Drama", "Comedy"], "overview": "Carmy, a young fine-dining chef, comes home to Chicago to run his family sandwich shop. As he fights to transform the shop and himself, he works alongside a rough-around-the-edges crew that ultimately reveal\u2026", "poster": "assets/tv-the-bear-2022-poster.jpg", "backdrop": "assets/tv-the-bear-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0467371Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 136315, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.158, "tmdbVotes": 2025}}, {"id": "tv-blue-planet-ii-2017", "title": "Blue Planet II", "year": 2017, "type": "tv", "runtimeSeconds": 3600, "seasonCount": 1, "communityRating": 8.64, "criticRating": null, "genres": ["Documentary", "Family"], "overview": "There is nowhere more powerful and unforgiving yet more beautiful and compelling than the ocean. Join us and explore the greatest yet least known parts of our planet.", "poster": "assets/tv-blue-planet-ii-2017-poster.jpg", "backdrop": "assets/tv-blue-planet-ii-2017-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0634849Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 74313, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.6, "tmdbVotes": 380}}, {"id": "tv-breeders-2020", "title": "Breeders", "year": 2020, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 4, "communityRating": 6.8, "criticRating": null, "genres": ["Comedy"], "overview": "Exposing the parental-paradox that it is possible, in the very same moment, to love your child to the horizon of the universe, while being apoplectically angry enough to want to send them there.", "poster": "assets/tv-breeders-2020-poster.jpg", "backdrop": "assets/tv-breeders-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-28T18:54:21.8885435Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 93290, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.8, "tmdbVotes": 104}}, {"id": "tv-chernobyl-2019", "title": "Chernobyl", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.718, "criticRating": null, "genres": ["Drama"], "overview": "The true story of one of the worst man-made catastrophes in history: the catastrophic nuclear accident at Chernobyl. A tale of the brave men and women who sacrificed to save Europe from unimaginable disaster.", "poster": "assets/tv-chernobyl-2019-poster.jpg", "backdrop": "assets/tv-chernobyl-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0469903Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 87108, "artwork": {"posterKind": "poster", "backdropKind": "center_crop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.731, "tmdbVotes": 8496}}, {"id": "tv-community-2009", "title": "Community", "year": 2009, "type": "tv", "runtimeSeconds": null, "seasonCount": 6, "communityRating": 7.997, "criticRating": null, "genres": ["Comedy"], "overview": "Follow the lives of a group of students at what is possibly the world\u2019s worst community college in the fictional locale of Greendale, Colorado.", "poster": "assets/tv-community-2009-poster.jpg", "backdrop": "assets/tv-community-2009-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0531416Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 18347, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.994, "tmdbVotes": 2226}}, {"id": "tv-dead-end-job-2026", "title": "Dead-End Job", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.7, "criticRating": null, "genres": ["Mystery"], "overview": "Drowning in debt, a young man takes lucrative part-time jobs from a mysterious agency \u2014 only to find his hustle for more money turning nightmarish.", "poster": "assets/tv-dead-end-job-2026-poster.jpg", "backdrop": "assets/tv-dead-end-job-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T18:58:01.8352857Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 289517, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.7, "tmdbVotes": 38}}, {"id": "tv-dopesick-2021", "title": "Dopesick", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.015, "criticRating": null, "genres": ["Crime", "Drama"], "overview": "The story of how one company triggered the worst drug epidemic in American history. Look into the epicenter of America's struggle with opioid addiction, from a distressed Virginia mining community, to the hallways of\u2026", "poster": "assets/tv-dopesick-2021-poster.jpg", "backdrop": "assets/tv-dopesick-2021-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0580168Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 110695, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.035, "tmdbVotes": 565}}, {"id": "tv-dune-prophecy-2024", "title": "Dune: Prophecy", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.213, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Drama", "Action & Adventure"], "overview": "Ten thousand years before the ascension of Paul Atreides, sisters Valya and Tula Harkonnen establish the fabled sect and female order that would become known as the Bene Gesserit to control the future of humankind.", "poster": "assets/tv-dune-prophecy-2024-poster.jpg", "backdrop": "assets/tv-dune-prophecy-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T18:23:01.2908568Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 90228, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 64, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.213, "tmdbVotes": 677}}, {"id": "tv-east-of-eden-2026", "title": "East of Eden", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.931, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "In the early 1900s, a fiercely independent woman drives a rift between two brothers, igniting a saga that unfolds across two generations.", "poster": "assets/tv-east-of-eden-2026-poster.jpg", "backdrop": "assets/tv-east-of-eden-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-23T18:07:11.3965096Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 258165, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.982, "tmdbVotes": 139}}, {"id": "tv-fallout-2024", "title": "Fallout", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.124, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy", "Drama"], "overview": "The story of haves and have-nots in a world in which there's almost nothing left to have. 200 years after the apocalypse, the gentle denizens of luxury fallout shelters are forced to return to the irradiated\u2026", "poster": "assets/tv-fallout-2024-poster.jpg", "backdrop": "assets/tv-fallout-2024-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0669262Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 106379, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.106, "tmdbVotes": 3233}}, {"id": "tv-fawlty-towers-1975", "title": "Fawlty Towers", "year": 1975, "type": "tv", "runtimeSeconds": 1800, "seasonCount": 2, "communityRating": 8.295, "criticRating": null, "genres": ["Comedy"], "overview": "Owner Basil Fawlty, his wife Sybil, a chambermaid Polly, and Spanish waiter Manuel attempt to run their hotel amidst farcical situations and an array of demanding guests.", "poster": "assets/tv-fawlty-towers-1975-poster.jpg", "backdrop": "assets/tv-fawlty-towers-1975-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-19T23:26:39.7658344Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 2207, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.296, "tmdbVotes": 867}}, {"id": "tv-the-final-problem-2026", "title": "The Final Problem", "year": 2026, "type": "tv", "runtimeSeconds": 3420, "seasonCount": 1, "communityRating": 7.041, "criticRating": null, "genres": ["Crime", "Mystery"], "overview": "In the spring of 1959, a retired actor, who previously played Sherlock Holmes, must investigate a murder at a small hotel on a tiny island near Mallorca.", "poster": "assets/tv-the-final-problem-2026-poster.jpg", "backdrop": "assets/tv-the-final-problem-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T19:16:12.4822791Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 290720, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.041, "tmdbVotes": 49}}, {"id": "tv-the-four-seasons-2025", "title": "The Four Seasons", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 6.879, "criticRating": null, "genres": ["Comedy"], "overview": "The decades-long friendship between three married couples is tested when one divorces, complicating their tradition of quarterly weekend getaways.", "poster": "assets/tv-the-four-seasons-2025-poster.jpg", "backdrop": "assets/tv-the-four-seasons-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-28T18:36:42.5326384Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 243316, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.892, "tmdbVotes": 204}}, {"id": "tv-the-get-down-2016", "title": "The Get Down", "year": 2016, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.3, "criticRating": null, "genres": ["Drama"], "overview": "In 1977 New York City, the talented and soulful youth of the South Bronx chase dreams and breakneck beats to transform music history.", "poster": "assets/tv-the-get-down-2016-poster.jpg", "backdrop": "assets/tv-the-get-down-2016-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0497737Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 65345, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.3, "tmdbVotes": 196}}, {"id": "tv-how-to-change-your-mind-2022", "title": "How to Change Your Mind", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.76, "criticRating": null, "genres": ["Documentary", "Mystery"], "overview": "Author Michael Pollan leads the way in this docuseries exploring the history and uses of psychedelics, including LSD, psilocybin, MDMA and mescaline.", "poster": "assets/tv-how-to-change-your-mind-2022-poster.jpg", "backdrop": "assets/tv-how-to-change-your-mind-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0601399Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 204304, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 53}}, {"id": "tv-interrogation-2020", "title": "Interrogation", "year": 2020, "type": "tv", "runtimeSeconds": 2700, "seasonCount": 1, "communityRating": 6.788, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "Based on a true story that spanned more than 30 years in which a young man was charged and convicted of brutally murdering his mother. Each episode is structured around an interrogation taken directly from the real\u2026", "poster": "assets/tv-interrogation-2020-poster.jpg", "backdrop": "assets/tv-interrogation-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-26T05:52:06.1068207Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 96488, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 57, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.9, "tmdbVotes": 54}}, {"id": "tv-it-s-always-sunny-in-philadelphia-2005", "title": "It's Always Sunny in Philadelphia", "year": 2005, "type": "tv", "runtimeSeconds": null, "seasonCount": 18, "communityRating": 8.2, "criticRating": null, "genres": ["Comedy"], "overview": "Four egocentric friends run a neighborhood Irish pub in Philadelphia and try to find their way through the adult world of work and relationships. Unfortunately, their warped views and precarious judgments often lead\u2026", "poster": "assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg", "backdrop": "assets/tv-it-s-always-sunny-in-philadelphia-2005-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0485774Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 2710, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.253, "tmdbVotes": 1457}}, {"id": "tv-a-knight-of-the-seven-kingdoms-2026", "title": "A Knight of the Seven Kingdoms", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.431, "criticRating": null, "genres": ["Drama", "Sci-Fi & Fantasy", "Action & Adventure"], "overview": "A century before the events of Game of Thrones, two unlikely heroes wandered Westeros: a young, naive but courageous knight, Ser Duncan the Tall, and his diminutive squire, Egg. Set in an age when the Targaryen line\u2026", "poster": "assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg", "backdrop": "assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0493344Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 224372, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.4, "tmdbVotes": 1266}}, {"id": "tv-landman-2024", "title": "Landman", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.194, "criticRating": null, "genres": ["Drama"], "overview": "Set in the proverbial boomtowns of West-Texas and a modern-day tale of fortune-seeking in the world of oil rigs, the series is an upstairs/downstairs story of roughnecks and wildcat billionaires that are fueling a\u2026", "poster": "assets/tv-landman-2024-poster.jpg", "backdrop": "assets/tv-landman-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T21:38:18.0973875Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 157741, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.209, "tmdbVotes": 668}}, {"id": "tv-the-last-of-us-2023", "title": "The Last of Us", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.419, "criticRating": null, "genres": ["Drama"], "overview": "Twenty years after modern civilization has been destroyed, Joel, a hardened survivor, is hired to smuggle Ellie, a 14-year-old girl, out of an oppressive quarantine zone. What starts as a small job soon becomes a\u2026", "poster": "assets/tv-the-last-of-us-2023-poster.jpg", "backdrop": "assets/tv-the-last-of-us-2023-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0640955Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 100088, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.4, "tmdbVotes": 7485}}, {"id": "tv-last-samurai-standing-2025", "title": "Last Samurai Standing", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.805, "criticRating": null, "genres": ["Action & Adventure", "Drama"], "overview": "In the early Meiji era, Shujiro, once known as an undefeated samurai, decides to participate in a deadly survival game to save his family and villagers.", "poster": "assets/tv-last-samurai-standing-2025-poster.jpg", "backdrop": "assets/tv-last-samurai-standing-2025-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0622526Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 252193, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.812, "tmdbVotes": 271}}, {"id": "tv-lord-of-the-flies-2026", "title": "Lord of the Flies", "year": 2026, "type": "tv", "runtimeSeconds": 3480, "seasonCount": 1, "communityRating": 7.571, "criticRating": null, "genres": ["Drama", "Action & Adventure"], "overview": "After a plane crash, a group of schoolboys find themselves stranded on a tropical island without adults. Ralph is elected leader and, with the help of the intelligent Piggy, strives to maintain order and\u2026", "poster": "assets/tv-lord-of-the-flies-2026-poster.jpg", "backdrop": "assets/tv-lord-of-the-flies-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0645648Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 270572, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.58, "tmdbVotes": 138}}, {"id": "tv-the-lord-of-the-rings-the-rings-of-power-2022", "title": "The Lord of the Rings: The Rings of Power", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 7.233, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy", "Drama"], "overview": "Beginning in a time of relative peace, we follow an ensemble cast of characters as they confront the re-emergence of evil to Middle-earth. From the darkest depths of the Misty Mountains, to the majestic forests of\u2026", "poster": "assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg", "backdrop": "assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0618904Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 84773, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 84, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.211, "tmdbVotes": 4003}}, {"id": "tv-love-death-robots-2019", "title": "Love, Death & Robots", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 8.21, "criticRating": null, "genres": ["Animation", "Sci-Fi & Fantasy"], "overview": "Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.", "poster": "assets/tv-love-death-robots-2019-poster.jpg", "backdrop": "assets/tv-love-death-robots-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0460964Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 86831, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.203, "tmdbVotes": 4238}}, {"id": "tv-maniac-2018", "title": "Maniac", "year": 2018, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.338, "criticRating": null, "genres": ["Comedy", "Drama", "Sci-Fi & Fantasy"], "overview": "Two strangers are drawn to a mysterious pharmaceutical trial that will, they're assured, with no complications or side-effects whatsoever, solve all of their problems, permanently. Things do not go as planned.", "poster": "assets/tv-maniac-2018-poster.jpg", "backdrop": "assets/tv-maniac-2018-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-20T19:42:30.955548Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 73411, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 84, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.323, "tmdbVotes": 1073}}, {"id": "tv-mussolini-son-of-the-century-2024", "title": "Mussolini: Son of the Century", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.135, "criticRating": null, "genres": ["Drama", "War & Politics"], "overview": "The rise of Benito Mussolini and the birth of fascism in Italy, chronicling a country\u2019s surrender to dictatorship and the relentless ascent of a man who rose from his ashes time and again.", "poster": "assets/tv-mussolini-son-of-the-century-2024-poster.jpg", "backdrop": "assets/tv-mussolini-son-of-the-century-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0586588Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 212333, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.1, "tmdbVotes": 179}}, {"id": "tv-the-night-manager-2016", "title": "The Night Manager", "year": 2016, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 7.74, "criticRating": null, "genres": ["Drama", "Mystery", "Crime"], "overview": "Former British soldier Jonathan Pine navigates the shadowy recesses of Whitehall and Washington where an unholy alliance operates between the intelligence community and the secret arms trade. To infiltrate the inner\u2026", "poster": "assets/tv-the-night-manager-2016-poster.jpg", "backdrop": "assets/tv-the-night-manager-2016-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0663672Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 61859, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.7, "tmdbVotes": 1422}}, {"id": "tv-one-piece-2023", "title": "ONE PIECE", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.118, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy"], "overview": "With his straw hat and ragtag crew, young pirate Monkey D. Luffy goes on an epic voyage for treasure.", "poster": "assets/tv-one-piece-2023-poster.jpg", "backdrop": "assets/tv-one-piece-2023-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0439378Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 111110, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.104, "tmdbVotes": 1988}}, {"id": "tv-only-murders-in-the-building-2021", "title": "Only Murders in the Building", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 8.438, "criticRating": null, "genres": ["Comedy", "Mystery", "Crime"], "overview": "Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.", "poster": "assets/tv-only-murders-in-the-building-2021-poster.jpg", "backdrop": "assets/tv-only-murders-in-the-building-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0594614Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 107113, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.398, "tmdbVotes": 2333}}, {"id": "tv-over-the-garden-wall-2014", "title": "Over the Garden Wall", "year": 2014, "type": "tv", "runtimeSeconds": 720, "seasonCount": 1, "communityRating": 8.6, "criticRating": null, "genres": ["Mystery", "Sci-Fi & Fantasy", "Animation"], "overview": "Two brothers, Wirt and Greg, find themselves lost in the Unknown; a strange forest adrift in time. With the help of a wise old Woodsman and a foul-tempered bluebird named Beatrice, Wirt and Greg must travel across\u2026", "poster": "assets/tv-over-the-garden-wall-2014-poster.jpg", "backdrop": "assets/tv-over-the-garden-wall-2014-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0455757Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 61617, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.6, "tmdbVotes": 1854}}, {"id": "tv-peaky-blinders-2013", "title": "Peaky Blinders", "year": 2013, "type": "tv", "runtimeSeconds": null, "seasonCount": 6, "communityRating": 8.5, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "A gangster family epic set in 1919 Birmingham, England and centered on a gang who sew razor blades in the peaks of their caps, and their fierce boss Tommy Shelby, who means to move up in the world.", "poster": "assets/tv-peaky-blinders-2013-poster.jpg", "backdrop": "assets/tv-peaky-blinders-2013-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0539185Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 60574, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.5, "tmdbVotes": 11506}}, {"id": "tv-the-pitt-2025", "title": "The Pitt", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.729, "criticRating": null, "genres": ["Drama"], "overview": "The staff of Pittsburgh's Trauma Medical Center work around the clock to save lives in an overcrowded and underfunded emergency department.", "poster": "assets/tv-the-pitt-2025-poster.jpg", "backdrop": "assets/tv-the-pitt-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0582942Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 250307, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.731, "tmdbVotes": 1046}}, {"id": "tv-pluribus-2025", "title": "Pluribus", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.936, "criticRating": null, "genres": ["Drama", "Sci-Fi & Fantasy"], "overview": "The most miserable person on Earth must save the world from happiness.", "poster": "assets/tv-pluribus-2025-poster.jpg", "backdrop": "assets/tv-pluribus-2025-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0674462Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 225171, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 99, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.921, "tmdbVotes": 1191}}, {"id": "tv-ripley-2024", "title": "RIPLEY", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.911, "criticRating": null, "genres": ["Drama"], "overview": "A grifter in 1960s New York is hired to convince a wealthy man's son to return home from Italy and begins a life of deceit, fraud and murder.", "poster": "assets/tv-ripley-2024-poster.jpg", "backdrop": "assets/tv-ripley-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0658942Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 94028, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 86, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.909, "tmdbVotes": 453}}, {"id": "tv-say-nothing-2024", "title": "Say Nothing", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.462, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "Through the eyes of various Irish Republican Army (IRA) members, explore the extremes some people will go to in the name of their beliefs, the way a deeply divided society can suddenly tip over into armed conflict,\u2026", "poster": "assets/tv-say-nothing-2024-poster.jpg", "backdrop": "assets/tv-say-nothing-2024-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0499752Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 245256, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 94, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.475, "tmdbVotes": 122}}, {"id": "tv-severance-2022", "title": "Severance", "year": 2022, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.396, "criticRating": null, "genres": ["Drama", "Mystery", "Sci-Fi & Fantasy"], "overview": "Mark leads a team of office workers whose memories have been surgically divided between their work and personal lives. When a mysterious colleague appears outside of work, it begins a journey to discover the truth\u2026", "poster": "assets/tv-severance-2022-poster.jpg", "backdrop": "assets/tv-severance-2022-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0598478Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 95396, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.389, "tmdbVotes": 3095}}, {"id": "tv-six-feet-under-2001", "title": "Six Feet Under", "year": 2001, "type": "tv", "runtimeSeconds": null, "seasonCount": 5, "communityRating": 8.115, "criticRating": null, "genres": ["Drama"], "overview": "When death is your business, what is your life? For the Fisher family, the world outside of their family-owned funeral home continues to be at least as challenging as\u2014and far less predictable than\u2014the one inside.", "poster": "assets/tv-six-feet-under-2001-poster.jpg", "backdrop": "assets/tv-six-feet-under-2001-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-09-26T04:04:05.108787Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 1274, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.1, "tmdbVotes": 1192}}, {"id": "tv-undercover-2019", "title": "Undercover", "year": 2019, "type": "tv", "runtimeSeconds": 3000, "seasonCount": 3, "communityRating": 7.5, "criticRating": null, "genres": ["Crime", "Drama"], "overview": "Undercover agents infiltrate a drug kingpin's operation by posing as a couple at the campground where he spends his weekends. Inspired by real events.", "poster": "assets/tv-undercover-2019-poster.jpg", "backdrop": "assets/tv-undercover-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0570153Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 86942, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 186}}, {"id": "tv-upload-2020", "title": "Upload", "year": 2020, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 7.781, "criticRating": null, "genres": ["Comedy", "Sci-Fi & Fantasy", "Drama"], "overview": "In 2033, people who are near death can be \u201cuploaded\u201d into virtual reality hotels run by 6 tech firms. Cash-strapped Nora lives in Brooklyn and works customer service for the luxurious \u201cLakeview\u201d digital afterlife.\u2026", "poster": "assets/tv-upload-2020-poster.jpg", "backdrop": "assets/tv-upload-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0566967Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 86248, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 87, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.757, "tmdbVotes": 1511}}, {"id": "tv-the-walk-in-2022", "title": "The Walk-In", "year": 2022, "type": "tv", "runtimeSeconds": 2760, "seasonCount": 1, "communityRating": 6.9, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "Forced to flee the UK and go into hiding in the 1990s, when he became a mole from within the BNP, Matthew Collins returned to Britain to make a new life for himself, working with young white men in the fight against\u2026", "poster": "assets/tv-the-walk-in-2022-poster.jpg", "backdrop": "assets/tv-the-walk-in-2022-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0654569Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 210664, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7, "tmdbVotes": 50}}, {"id": "tv-the-white-lotus-2021", "title": "The White Lotus", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.596, "criticRating": null, "genres": ["Comedy", "Drama", "Mystery"], "overview": "Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026", "poster": "assets/tv-the-white-lotus-2021-poster.jpg", "backdrop": "assets/tv-the-white-lotus-2021-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0546438Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 111803, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.599, "tmdbVotes": 1604}}, {"id": "tv-widow-s-bay-2026", "title": "Widow's Bay", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 8.172, "criticRating": null, "genres": ["Drama", "Comedy", "Mystery"], "overview": "The mayor of a New England town resolves to turn it into the next tourist hot spot, despite local warnings that it's cursed.", "poster": "assets/tv-widow-s-bay-2026-poster.jpg", "backdrop": "assets/tv-widow-s-bay-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-20T10:27:12.7003979Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 270476, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 98, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8, "tmdbVotes": 585}}, {"id": "tv-the-witcher-2019", "title": "The Witcher", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": 4, "communityRating": 7.905, "criticRating": null, "genres": ["Drama", "Action & Adventure"], "overview": "Geralt of Rivia, a mutated monster-hunter for hire, journeys toward his destiny in a turbulent world where people often prove more wicked than beasts.", "poster": "assets/tv-the-witcher-2019-poster.jpg", "backdrop": "assets/tv-the-witcher-2019-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0552369Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 71912, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 75, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.88, "tmdbVotes": 6997}}, {"id": "tv-yellowjackets-2021", "title": "Yellowjackets", "year": 2021, "type": "tv", "runtimeSeconds": null, "seasonCount": 3, "communityRating": 7.449, "criticRating": null, "genres": ["Drama", "Mystery"], "overview": "This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026", "poster": "assets/tv-yellowjackets-2021-poster.jpg", "backdrop": "assets/tv-yellowjackets-2021-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T15:45:22.0577612Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 117488, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.431, "tmdbVotes": 1070}}], "anime": [], "resume": [{"id": "episode-episode-1-2025-s1e1", "title": "Episode 1", "year": 2025, "type": "episode", "runtimeSeconds": 3618, "seasonCount": null, "communityRating": 7.357, "criticRating": null, "genres": ["Biography", "Drama", "History"], "overview": "Mussolini had just founded the Fasci di Combattimento when D'Annunzio took Fiume. Mussolini hoped to capitalize on the poet's achievement in the electoral arena.", "poster": "assets/episode-episode-1-2025-s1e1-poster.jpg", "backdrop": "assets/episode-episode-1-2025-s1e1-backdrop.jpg", "played": false, "playCount": 1, "resumeSeconds": 489.55, "progress": 0.1353, "addedAt": "2026-07-17T13:52:48.0000000Z", "lastPlayedAt": "2026-11-04T19:01:49.9731435Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Mussolini: Son of the Century", "seasonNumber": 1, "episodeNumber": 1, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}}], "nextUp": [{"id": "episode-no-honeymoon-2020-s1e8", "title": "No Honeymoon", "year": 2020, "type": "episode", "runtimeSeconds": 1499, "seasonCount": null, "communityRating": 6.7, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Paul attempts to focus on his upcoming wedding but is constantly thwarted by his family; Ally is distracted by work, Luke is misbehaving at school and Leah (Stella Gonet) doesn\u2019t understand why they are even\u2026", "poster": "assets/episode-no-honeymoon-2020-s1e8-poster.jpg", "backdrop": "assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg", "played": false, "playCount": 1, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-28T19:00:50.0000000Z", "lastPlayedAt": "2026-11-03T20:57:58.3314947Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Breeders", "seasonNumber": 1, "episodeNumber": 8, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-notapusy-2005-s3e4", "title": "Notapusy", "year": 2005, "type": "episode", "runtimeSeconds": 1334, "seasonCount": null, "communityRating": 8.125, "criticRating": null, "genres": ["Comedy"], "overview": "To prove he's a man to Rita, Michael and his nephew Steve Holt compete in the Church And State Fair's annual triathlon. George Sr. tries to scare some at-risk youth, and Tobias tries to help George-Michael's\u2026", "poster": "assets/episode-notapusy-2005-s3e4-poster.jpg", "backdrop": "assets/episode-notapusy-2005-s3e4-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T12:17:58.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Arrested Development", "seasonNumber": 3, "episodeNumber": 4, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-the-man-who-would-be-vogue-2018-s2e1", "title": "The Man Who Would Be Vogue", "year": 2018, "type": "episode", "runtimeSeconds": 3203, "seasonCount": null, "communityRating": 7.459, "criticRating": null, "genres": ["Biography", "Crime", "Drama"], "overview": "The murder of Gianni Versace turns the eyes of the world onto Miami Beach.", "poster": "assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg", "backdrop": "assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T13:29:35.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "American Crime Story", "seasonNumber": 2, "episodeNumber": 1, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-tomorrow-2024-s3e1", "title": "Tomorrow", "year": 2024, "type": "episode", "runtimeSeconds": 2207, "seasonCount": null, "communityRating": 7.6, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "The next day and the days that led to it.", "poster": "assets/episode-tomorrow-2024-s3e1-poster.jpg", "backdrop": "assets/episode-tomorrow-2024-s3e1-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-07-17T13:46:39.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "The Bear", "seasonNumber": 3, "episodeNumber": 1, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}}, {"id": "episode-episode-1-2025-s1e1", "title": "Episode 1", "year": 2025, "type": "episode", "runtimeSeconds": 3618, "seasonCount": null, "communityRating": 7.357, "criticRating": null, "genres": ["Biography", "Drama", "History"], "overview": "Mussolini had just founded the Fasci di Combattimento when D'Annunzio took Fiume. Mussolini hoped to capitalize on the poet's achievement in the electoral arena.", "poster": "assets/episode-episode-1-2025-s1e1-poster.jpg", "backdrop": "assets/episode-episode-1-2025-s1e1-backdrop.jpg", "played": false, "playCount": 1, "resumeSeconds": 489.55, "progress": 0.1353, "addedAt": "2026-07-17T13:52:48.0000000Z", "lastPlayedAt": "2026-11-04T19:01:49.9731435Z", "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "seriesTitle": "Mussolini: Son of the Century", "seasonNumber": 1, "episodeNumber": 1, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}}], "recentlyAdded": [{"id": "tv-the-final-problem-2026", "title": "The Final Problem", "year": 2026, "type": "tv", "runtimeSeconds": 3420, "seasonCount": 1, "communityRating": 7.041, "criticRating": null, "genres": ["Crime", "Mystery"], "overview": "In the spring of 1959, a retired actor, who previously played Sherlock Holmes, must investigate a murder at a small hotel on a tiny island near Mallorca.", "poster": "assets/tv-the-final-problem-2026-poster.jpg", "backdrop": "assets/tv-the-final-problem-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T19:16:12.4822791Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 290720, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.041, "tmdbVotes": 49}}, {"id": "tv-dead-end-job-2026", "title": "Dead-End Job", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.7, "criticRating": null, "genres": ["Mystery"], "overview": "Drowning in debt, a young man takes lucrative part-time jobs from a mysterious agency \u2014 only to find his hustle for more money turning nightmarish.", "poster": "assets/tv-dead-end-job-2026-poster.jpg", "backdrop": "assets/tv-dead-end-job-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T18:58:01.8352857Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 289517, "artwork": {"posterKind": "center_crop", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.7, "tmdbVotes": 38}}, {"id": "tv-dune-prophecy-2024", "title": "Dune: Prophecy", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.213, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Drama", "Action & Adventure"], "overview": "Ten thousand years before the ascension of Paul Atreides, sisters Valya and Tula Harkonnen establish the fabled sect and female order that would become known as the Bene Gesserit to control the future of humankind.", "poster": "assets/tv-dune-prophecy-2024-poster.jpg", "backdrop": "assets/tv-dune-prophecy-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-04T18:23:01.2908568Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 90228, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 64, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.213, "tmdbVotes": 677}}, {"id": "movie-the-uprising-2026", "title": "The Uprising", "year": 2026, "type": "movie", "runtimeSeconds": 7684, "seasonCount": null, "communityRating": 7.846, "criticRating": 55, "genres": ["Action", "Drama", "History"], "overview": "As war and plague devastate 14th-century England, a humble peasant ignites a rebellion, uniting an army of commoners to defy the King\u2019s might in a desperate fight for justice and survival.", "poster": "assets/movie-the-uprising-2026-poster.jpg", "backdrop": "assets/movie-the-uprising-2026-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-11-02T16:37:03.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 977942, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 57, "rtCriticsLabel": "Rotten", "rtAudience": 65, "rtAudienceLabel": "Upright", "imdb": 6.2, "imdbVotes": 6622, "tmdb": 7.8, "tmdbVotes": 404}}, {"id": "movie-jennifer-s-body-2009", "title": "Jennifer's Body", "year": 2009, "type": "movie", "runtimeSeconds": 6450, "seasonCount": null, "communityRating": 6.012, "criticRating": 47, "genres": ["Horror", "Comedy"], "overview": "Jennifer, a gorgeous, seductive cheerleader takes evil to a whole new level after she's possessed by a sinister demon. Now it's up to her best friend to stop Jennifer's reign of terror before it's too late.", "poster": "assets/movie-jennifer-s-body-2009-poster.jpg", "backdrop": "assets/movie-jennifer-s-body-2009-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-30T03:37:04.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 19994, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 47, "rtCriticsLabel": "Rotten", "rtAudience": 36, "rtAudienceLabel": "Spilled", "imdb": 5.6, "imdbVotes": 179710, "tmdb": 6.013, "tmdbVotes": 4481}}, {"id": "tv-breeders-2020", "title": "Breeders", "year": 2020, "type": "tv", "runtimeSeconds": 1500, "seasonCount": 4, "communityRating": 6.8, "criticRating": null, "genres": ["Comedy"], "overview": "Exposing the parental-paradox that it is possible, in the very same moment, to love your child to the horizon of the universe, while being apoplectically angry enough to want to send them there.", "poster": "assets/tv-breeders-2020-poster.jpg", "backdrop": "assets/tv-breeders-2020-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-28T18:54:21.8885435Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 93290, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.8, "tmdbVotes": 104}}, {"id": "tv-the-four-seasons-2025", "title": "The Four Seasons", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 6.879, "criticRating": null, "genres": ["Comedy"], "overview": "The decades-long friendship between three married couples is tested when one divorces, complicating their tradition of quarterly weekend getaways.", "poster": "assets/tv-the-four-seasons-2025-poster.jpg", "backdrop": "assets/tv-the-four-seasons-2025-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-28T18:36:42.5326384Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 243316, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 81, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.892, "tmdbVotes": 204}}, {"id": "tv-adults-2025", "title": "Adults", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 6.495, "criticRating": null, "genres": ["Comedy"], "overview": "A group of twenty-somethings in New York trying to be good people, despite being neither \"good\" nor \"people\" yet.", "poster": "assets/tv-adults-2025-poster.jpg", "backdrop": "assets/tv-adults-2025-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-23T21:01:41.1814765Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 260458, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.5, "tmdbVotes": 102}}, {"id": "tv-east-of-eden-2026", "title": "East of Eden", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": 1, "communityRating": 7.931, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "In the early 1900s, a fiercely independent woman drives a rift between two brothers, igniting a saga that unfolds across two generations.", "poster": "assets/tv-east-of-eden-2026-poster.jpg", "backdrop": "assets/tv-east-of-eden-2026-backdrop.jpg", "played": true, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-23T18:07:11.3965096Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 258165, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.982, "tmdbVotes": 139}}, {"id": "movie-fighting-with-my-family-2019", "title": "Fighting with My Family", "year": 2019, "type": "movie", "runtimeSeconds": 6509, "seasonCount": null, "communityRating": 6.84, "criticRating": 93, "genres": ["Comedy"], "overview": "Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training\u2026", "poster": "assets/movie-fighting-with-my-family-2019-poster.jpg", "backdrop": "assets/movie-fighting-with-my-family-2019-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T22:10:59.0000000Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 445629, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Certified Fresh", "rtAudience": 86, "rtAudienceLabel": "Upright", "imdb": 7, "imdbVotes": 94576, "tmdb": 6.837, "tmdbVotes": 1952}}, {"id": "tv-landman-2024", "title": "Landman", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": 2, "communityRating": 8.194, "criticRating": null, "genres": ["Drama"], "overview": "Set in the proverbial boomtowns of West-Texas and a modern-day tale of fortune-seeking in the world of oil rigs, the series is an upstairs/downstairs story of roughnecks and wildcat billionaires that are fueling a\u2026", "poster": "assets/tv-landman-2024-poster.jpg", "backdrop": "assets/tv-landman-2024-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-22T21:38:18.0973875Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 157741, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.209, "tmdbVotes": 668}}, {"id": "tv-fawlty-towers-1975", "title": "Fawlty Towers", "year": 1975, "type": "tv", "runtimeSeconds": 1800, "seasonCount": 2, "communityRating": 8.295, "criticRating": null, "genres": ["Comedy"], "overview": "Owner Basil Fawlty, his wife Sybil, a chambermaid Polly, and Spanish waiter Manuel attempt to run their hotel amidst farcical situations and an array of demanding guests.", "poster": "assets/tv-fawlty-towers-1975-poster.jpg", "backdrop": "assets/tv-fawlty-towers-1975-backdrop.jpg", "played": false, "playCount": 0, "resumeSeconds": 0.0, "progress": 0, "addedAt": "2026-10-19T23:26:39.7658344Z", "lastPlayedAt": null, "availability": "in_library", "availabilityEvidence": "viewer_scoped_jellyfin_library", "source": "jellyfin_library", "tmdbId": 2207, "artwork": {"posterKind": "poster", "backdropKind": "backdrop"}, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.296, "tmdbVotes": 867}}], "discover": [{"id": "movie-12-years-a-slave-2013", "title": "12 Years a Slave", "year": 2013, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "History"], "overview": "In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay\u2026", "poster": "assets/movie-12-years-a-slave-2013-poster.jpg", "backdrop": "assets/movie-12-years-a-slave-2013-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 76203, "ratings": {"rtCritics": 95, "rtCriticsLabel": "Certified Fresh", "rtAudience": 90, "rtAudienceLabel": "Upright", "imdb": 8.1, "imdbVotes": 796313, "tmdb": 7.927, "tmdbVotes": 12335}}, {"id": "movie-101-dalmatians-ii-patch-s-london-adventure-2003", "title": "101 Dalmatians II: Patch's London Adventure", "year": 2003, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Family", "Animation", "Adventure"], "overview": "Being one of 101 takes its toll on Patch, who doesn't feel unique. When he's accidentally left behind on moving day, he meets his idol, Thunderbolt, who enlists him on a publicity campaign.", "poster": "assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg", "backdrop": "assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 13654, "ratings": {"rtCritics": 67, "rtCriticsLabel": "Fresh", "rtAudience": 41, "rtAudienceLabel": "Spilled", "imdb": 5.7, "imdbVotes": 12343, "tmdb": 6.025, "tmdbVotes": 1274}}, {"id": "movie-absolution-2024", "title": "Absolution", "year": 2024, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Crime", "Thriller"], "overview": "An aging ex-boxer gangster working as muscle for a Boston crime boss receives an upsetting diagnosis. Despite a faltering memory, he attempts to rectify the sins of his past and reconnect with his estranged\u2026", "poster": "assets/movie-absolution-2024-poster.jpg", "backdrop": "assets/movie-absolution-2024-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 974453, "ratings": {"rtCritics": 56, "rtCriticsLabel": "Rotten", "rtAudience": 32, "rtAudienceLabel": "Spilled", "imdb": 5.2, "imdbVotes": 11413, "tmdb": 5.906, "tmdbVotes": 564}}, {"id": "movie-addicted-2014", "title": "Addicted", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.", "poster": "assets/movie-addicted-2014-poster.jpg", "backdrop": "assets/movie-addicted-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 235271, "ratings": {"rtCritics": 7, "rtCriticsLabel": "Rotten", "rtAudience": 51, "rtAudienceLabel": "Spilled", "imdb": 5, "imdbVotes": 13131, "tmdb": 5.928, "tmdbVotes": 466}}, {"id": "movie-all-the-money-in-the-world-2017", "title": "All the Money in the World", "year": 2017, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "History", "Thriller"], "overview": "The story of the kidnapping of 16-year-old John Paul Getty III and the desperate attempt by his devoted mother to convince his billionaire grandfather Jean Paul Getty to pay the ransom.", "poster": "assets/movie-all-the-money-in-the-world-2017-poster.jpg", "backdrop": "assets/movie-all-the-money-in-the-world-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 446791, "ratings": {"rtCritics": 79, "rtCriticsLabel": "Certified Fresh", "rtAudience": 66, "rtAudienceLabel": "Upright", "imdb": 6.8, "imdbVotes": 97448, "tmdb": 6.493, "tmdbVotes": 2493}}, {"id": "movie-the-alto-knights-2025", "title": "The Alto Knights", "year": 2025, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama"], "overview": "Two of New York's most notorious organized crime bosses, Frank Costello and Vito Genovese, vie for control of the city's streets. Once the best of friends, petty jealousies and a series of betrayals place them on a\u2026", "poster": "assets/movie-the-alto-knights-2025-poster.jpg", "backdrop": "assets/movie-the-alto-knights-2025-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1013601, "ratings": {"rtCritics": 39, "rtCriticsLabel": "Rotten", "rtAudience": 71, "rtAudienceLabel": "Upright", "imdb": 5.9, "imdbVotes": 23017, "tmdb": 6.223, "tmdbVotes": 485}}, {"id": "movie-the-amazing-spider-man-2-2014", "title": "The Amazing Spider-Man 2", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026", "poster": "assets/movie-the-amazing-spider-man-2-2014-poster.jpg", "backdrop": "assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 102382, "ratings": {"rtCritics": 51, "rtCriticsLabel": "Rotten", "rtAudience": 64, "rtAudienceLabel": "Upright", "imdb": 6.6, "imdbVotes": 611981, "tmdb": 6.567, "tmdbVotes": 14919}}, {"id": "movie-ant-man-and-the-wasp-quantumania-2023", "title": "Ant-Man and the Wasp: Quantumania", "year": 2023, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026", "poster": "assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg", "backdrop": "assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 640146, "ratings": {"rtCritics": 46, "rtCriticsLabel": "Rotten", "rtAudience": 81, "rtAudienceLabel": "Upright", "imdb": 6, "imdbVotes": 281738, "tmdb": 6.197, "tmdbVotes": 6236}}, {"id": "movie-arctic-dogs-2019", "title": "Arctic Dogs", "year": 2019, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Adventure", "Comedy"], "overview": "Animals band together to save the day when the evil Otto Von Walrus hatches a sinister scheme to accelerate global warming and melt the Arctic Circle.", "poster": "assets/movie-arctic-dogs-2019-poster.jpg", "backdrop": "assets/movie-arctic-dogs-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 416153, "ratings": {"rtCritics": 13, "rtCriticsLabel": "Rotten", "rtAudience": 60, "rtAudienceLabel": "Upright", "imdb": 4.9, "imdbVotes": 2580, "tmdb": 5.9, "tmdbVotes": 148}}, {"id": "movie-the-avengers-2012", "title": "The Avengers", "year": 2012, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Science Fiction", "Action", "Adventure"], "overview": "When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026", "poster": "assets/movie-the-avengers-2012-poster.jpg", "backdrop": "assets/movie-the-avengers-2012-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 24428, "ratings": {"rtCritics": 91, "rtCriticsLabel": "Certified Fresh", "rtAudience": 91, "rtAudienceLabel": "Upright", "imdb": 8, "imdbVotes": 1590693, "tmdb": 8.084, "tmdbVotes": 40262}}, {"id": "movie-black-bear-2020", "title": "Black Bear", "year": 2020, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026", "poster": "assets/movie-black-bear-2020-poster.jpg", "backdrop": "assets/movie-black-bear-2020-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 624788, "ratings": {"rtCritics": 89, "rtCriticsLabel": "Certified Fresh", "rtAudience": 64, "rtAudienceLabel": "Upright", "imdb": 6.5, "imdbVotes": 18408, "tmdb": 6.337, "tmdbVotes": 315}}, {"id": "movie-black-panther-2018", "title": "Black Panther", "year": 2018, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026", "poster": "assets/movie-black-panther-2018-poster.jpg", "backdrop": "assets/movie-black-panther-2018-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 284054, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Certified Fresh", "rtAudience": null, "rtAudienceLabel": "Spilled", "imdb": 7.3, "imdbVotes": 931120, "tmdb": 7.36, "tmdbVotes": 24190}}, {"id": "movie-a-bronx-tale-1993", "title": "A Bronx Tale", "year": 1993, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "Set in the Bronx during the tumultuous 1960s, an adolescent boy is torn between his honest, working-class father and a violent yet charismatic crime boss. Complicating matters is the youngster's growing attraction -\u2026", "poster": "assets/movie-a-bronx-tale-1993-poster.jpg", "backdrop": "assets/movie-a-bronx-tale-1993-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1607, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Fresh", "rtAudience": 92, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 191930, "tmdb": 7.913, "tmdbVotes": 3075}}, {"id": "movie-by-any-means-2026", "title": "By Any Means", "year": 2026, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Thriller", "History", "Crime"], "overview": "In 1960s Mississippi, a young Black FBI agent is forced to partner with a notorious mafia hitman to investigate a wave of brutal civil rights murders.", "poster": "assets/movie-by-any-means-2026-poster.jpg", "backdrop": "assets/movie-by-any-means-2026-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1380417, "ratings": {"rtCritics": 49, "rtCriticsLabel": "Rotten", "rtAudience": 88, "rtAudienceLabel": "Upright", "imdb": 6.8, "imdbVotes": 3534, "tmdb": 6.6, "tmdbVotes": 92}}, {"id": "movie-captain-america-civil-war-2016", "title": "Captain America: Civil War", "year": 2016, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Adventure", "Action", "Science Fiction"], "overview": "Following the events of Age of Ultron, the collective governments of the world pass an act designed to regulate all superhuman activity. This polarizes opinion amongst the Avengers, causing two factions to side with\u2026", "poster": "assets/movie-captain-america-civil-war-2016-poster.jpg", "backdrop": "assets/movie-captain-america-civil-war-2016-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 271110, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 89, "rtAudienceLabel": "Upright", "imdb": 7.8, "imdbVotes": 954943, "tmdb": 7.455, "tmdbVotes": 24808}}, {"id": "movie-captain-america-the-first-avenger-2011", "title": "Captain America: The First Avenger", "year": 2011, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "During World War II, Steve Rogers is a sickly man from Brooklyn who's transformed into super-soldier Captain America to aid in the war effort. Rogers must stop the Red Skull \u2013 Adolf Hitler's ruthless head of\u2026", "poster": "assets/movie-captain-america-the-first-avenger-2011-poster.jpg", "backdrop": "assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1771, "ratings": {"rtCritics": 80, "rtCriticsLabel": "Certified Fresh", "rtAudience": 75, "rtAudienceLabel": "Upright", "imdb": 6.9, "imdbVotes": 993453, "tmdb": 7.012, "tmdbVotes": 23473}}, {"id": "movie-captain-america-the-winter-soldier-2014", "title": "Captain America: The Winter Soldier", "year": 2014, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action", "Adventure", "Science Fiction"], "overview": "After the cataclysmic events in New York with The Avengers, Steve Rogers, aka Captain America is living quietly in Washington, D.C. and trying to adjust to the modern world. But when a S.H.I.E.L.D. colleague comes\u2026", "poster": "assets/movie-captain-america-the-winter-soldier-2014-poster.jpg", "backdrop": "assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 100402, "ratings": {"rtCritics": 90, "rtCriticsLabel": "Certified Fresh", "rtAudience": 92, "rtAudienceLabel": "Upright", "imdb": 7.7, "imdbVotes": 995514, "tmdb": 7.654, "tmdbVotes": 20688}}, {"id": "movie-careful-what-you-wish-for-2015", "title": "Careful What You Wish For", "year": 2015, "type": "movie", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Thriller"], "overview": "A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.", "poster": "assets/movie-careful-what-you-wish-for-2015-poster.jpg", "backdrop": "assets/movie-careful-what-you-wish-for-2015-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 317744, "ratings": {"rtCritics": 17, "rtCriticsLabel": "Rotten", "rtAudience": 61, "rtAudienceLabel": "Upright", "imdb": 5.6, "imdbVotes": 9452, "tmdb": 6.129, "tmdbVotes": 446}}, {"id": "tv-12-monkeys-2015", "title": "12 Monkeys", "year": 2015, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Drama"], "overview": "The provocative story of Cole, a time traveler from a decimated future in a high-stakes race against the clock. Utilizing a dangerous and untested method of time travel, he journeys from 2043 to the present day on a\u2026", "poster": "assets/tv-12-monkeys-2015-poster.jpg", "backdrop": "assets/tv-12-monkeys-2015-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 60948, "ratings": {"rtCritics": 88, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.441, "tmdbVotes": 1245}}, {"id": "tv-90-day-fiance-before-the-90-days-2017", "title": "90 Day Fianc\u00e9: Before the 90 Days", "year": 2017, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026", "poster": "assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg", "backdrop": "assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 73319, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.5, "tmdbVotes": 45}}, {"id": "tv-90-day-fiance-the-other-way-2019", "title": "90 Day Fianc\u00e9: The Other Way", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?", "poster": "assets/tv-90-day-fiance-the-other-way-2019-poster.jpg", "backdrop": "assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 90046, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 34}}, {"id": "tv-the-100-2014", "title": "The 100", "year": 2014, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Sci-Fi & Fantasy", "Action & Adventure", "Drama"], "overview": "100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026", "poster": "assets/tv-the-100-2014-poster.jpg", "backdrop": "assets/tv-the-100-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 48866, "ratings": {"rtCritics": 93, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.878, "tmdbVotes": 8855}}, {"id": "tv-accused-2023", "title": "Accused", "year": 2023, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama"], "overview": "In this crime anthology series, viewers discover how an ordinary person got caught up in an extraordinary situation, ultimately revealing how one wrong turn leads to another, until it\u2019s too late to turn back. Told\u2026", "poster": "assets/tv-accused-2023-poster.jpg", "backdrop": "assets/tv-accused-2023-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 202097, "ratings": {"rtCritics": 61, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.851, "tmdbVotes": 87}}, {"id": "tv-after-life-2019", "title": "After Life", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "Tony had a perfect life. But after his wife Lisa suddenly dies, Tony changes. After contemplating taking his own life, he decides instead to live long enough to punish the world by saying and doing whatever he likes\u2026", "poster": "assets/tv-after-life-2019-poster.jpg", "backdrop": "assets/tv-after-life-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 79410, "ratings": {"rtCritics": 72, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.902, "tmdbVotes": 1169}}, {"id": "tv-all-creatures-great-and-small-1978", "title": "All Creatures Great and Small", "year": 1978, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy", "Drama"], "overview": "The trials and misadventures of the staff at a country veterinary office in Yorkshire. James Herriot, a young animal surgeon, moves to a small Yorkshire town to begin his first job.", "poster": "assets/tv-all-creatures-great-and-small-1978-poster.jpg", "backdrop": "assets/tv-all-creatures-great-and-small-1978-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "center_crop", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 7406, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 50}}, {"id": "tv-american-housewife-2016", "title": "American Housewife", "year": 2016, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy"], "overview": "A family comedy narrated by Katie, a strong-willed mother, raising her flawed family in a wealthy town filled with perfect wives and their perfect offspring.", "poster": "assets/tv-american-housewife-2016-poster.jpg", "backdrop": "assets/tv-american-housewife-2016-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 67117, "ratings": {"rtCritics": null, "rtCriticsLabel": "Rotten", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.7, "tmdbVotes": 187}}, {"id": "tv-atlantis-2013", "title": "Atlantis", "year": 2013, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy"], "overview": "A fantasy drama set in a world of legendary heroes and mythical creatures. Far from home and desperate for answers, Jason washes up on the shores of an ancient land. A mysterious place; a world of bull leaping, of\u2026", "poster": "assets/tv-atlantis-2013-poster.jpg", "backdrop": "assets/tv-atlantis-2013-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 47054, "ratings": {"rtCritics": 62, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.4, "tmdbVotes": 155}}, {"id": "tv-avatar-the-last-airbender-2024", "title": "Avatar: The Last Airbender", "year": 2024, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Action & Adventure", "Sci-Fi & Fantasy", "Drama"], "overview": "A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.", "poster": "assets/tv-avatar-the-last-airbender-2024-poster.jpg", "backdrop": "assets/tv-avatar-the-last-airbender-2024-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 82452, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.8, "tmdbVotes": 1406}}, {"id": "tv-avatar-seven-havens-2026", "title": "Avatar: Seven Havens", "year": 2026, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026", "poster": "assets/tv-avatar-seven-havens-2026-poster.jpg", "backdrop": "assets/tv-avatar-seven-havens-2026-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 284833, "ratings": {"rtCritics": 100, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.9, "tmdbVotes": 183}}, {"id": "tv-batman-the-brave-and-the-bold-2008", "title": "Batman: The Brave and the Bold", "year": 2008, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Animation", "Action & Adventure", "Sci-Fi & Fantasy"], "overview": "The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.", "poster": "assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg", "backdrop": "assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 15804, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.6, "tmdbVotes": 295}}, {"id": "tv-beat-bobby-flay-2013", "title": "Beat Bobby Flay", "year": 2013, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Reality"], "overview": "Two talented chefs go head-to-head for the chance to Beat Bobby Flay. To get to Bobby the chefs must first face off against each other, creating a spectacular dish with a secret ingredient of Bobby's choice. Judges\u2026", "poster": "assets/tv-beat-bobby-flay-2013-poster.jpg", "backdrop": "assets/tv-beat-bobby-flay-2013-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 62481, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.5, "tmdbVotes": 31}}, {"id": "tv-benson-1979", "title": "Benson", "year": 1979, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy"], "overview": "A butler deals with life at the governor's mansion.", "poster": "assets/tv-benson-1979-poster.jpg", "backdrop": "assets/tv-benson-1979-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1849, "ratings": {"rtCritics": null, "rtCriticsLabel": null, "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7, "tmdbVotes": 46}}, {"id": "tv-black-rabbit-2025", "title": "Black Rabbit", "year": 2025, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "A rising-star restaurateur is forced into New York's criminal underworld when his chaotic brother returns to town with loan sharks on his trail.", "poster": "assets/tv-black-rabbit-2025-poster.jpg", "backdrop": "assets/tv-black-rabbit-2025-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 249039, "ratings": {"rtCritics": 66, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 6.983, "tmdbVotes": 240}}, {"id": "tv-black-ish-2014", "title": "black-ish", "year": 2014, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Comedy"], "overview": "A family man struggles to gain a sense of cultural identity while raising his kids in a predominantly white, upper-middle-class neighborhood.", "poster": "assets/tv-black-ish-2014-poster.jpg", "backdrop": "assets/tv-black-ish-2014-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 61381, "ratings": {"rtCritics": 92, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.1, "tmdbVotes": 352}}, {"id": "tv-breaking-bad-2008", "title": "Breaking Bad", "year": 2008, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Drama", "Crime"], "overview": "Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to\u2026", "poster": "assets/tv-breaking-bad-2008-poster.jpg", "backdrop": "assets/tv-breaking-bad-2008-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "center_crop", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 1396, "ratings": {"rtCritics": 96, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 8.959, "tmdbVotes": 19030}}, {"id": "tv-the-capture-2019", "title": "The Capture", "year": 2019, "type": "tv", "runtimeSeconds": null, "seasonCount": null, "communityRating": null, "criticRating": null, "genres": ["Crime", "Drama", "Mystery"], "overview": "When soldier Shaun Emery's conviction for a murder in Afghanistan is overturned due to flawed video evidence, he returns to life as a free man with his young daughter. But when damning CCTV footage from a night out\u2026", "poster": "assets/tv-the-capture-2019-poster.jpg", "backdrop": "assets/tv-the-capture-2019-backdrop.jpg", "played": null, "playCount": null, "resumeSeconds": null, "progress": null, "addedAt": null, "lastPlayedAt": null, "availability": "not_here", "availabilityEvidence": "retained_profile_membership_and_native_request_ledger", "source": "materialized_personal_recommendation", "rank": null, "watchStateKnown": false, "artwork": {"posterKind": "poster", "backdropKind": "poster_crop"}, "availabilityIsSnapshot": true, "tmdbId": 93166, "ratings": {"rtCritics": 97, "rtCriticsLabel": "Fresh", "rtAudience": null, "rtAudienceLabel": null, "imdb": null, "imdbVotes": null, "tmdb": 7.634, "tmdbVotes": 361}}], "discoverySource": {"state": "materialized_snapshot", "rankKnown": false, "selection": "First 18 alphabetically indexed movie stubs and first 18 series stubs, not ranked feed order", "observedAt": "2026-11-04T20:42:12.030108+00:00"}}], "notes": {"real": ["Profile keys and library membership derive from deployed Conductarr configuration and viewer-scoped Jellyfin GET responses.", "Names, years, overviews, library ratings, genres, season-container counts and watch/resume data come from actual Jellyfin metadata.", "All local artwork is copied from Jellyfin; discovery posters are the real materialized Conductarr recommendation artwork.", "Discover contains 36 real materialized personal recommendations per profile, with profile-scoped availability from read-only retained membership/request state.", "Justin has no playable resume items. Mindel and Shared each have one. No progress or watched flags were invented."], "approximations": ["Default profile initials and display capitalization are frontend presentation choices; colours reuse the existing PWA palette, not live user appearance configuration.", "Conductarr catalogue and recommendation GET endpoints both returned 401. Authentication was not bypassed. SQLite contains no retained recommendation feed/candidate snapshot tables; the source keeps ranked feeds in memory. Discovery therefore reads existing per-profile Jellyfin recommendation stubs as metadata only.", "Discovery selection is 18 alphabetically indexed movie suggestions plus 18 series suggestions, not personal rank order; rank is null. Materialization age and original feed ordering are unknown.", "Discovery stub runtime is synthetic (two seconds), so runtime, season count, rating and watch state are null rather than presenting stub values as real media facts.", "93 selected identities lack original landscape art in the materialized metadata; their backdrop is a marked crop of the real poster, not an original film backdrop. Existing artwork is reused; new artwork is centre-cropped to the required dimensions.", "Jellyfin server-side resizing requests maxWidth/maxHeight/quality. When the result had the wrong aspect ratio, sequential single-thread ffmpeg center crops produce exact 300x450 and 1280x720 JPEGs.", "Series seasonCount is Jellyfin ChildCount, including season containers for specials when present.", "Discover availability is a retained profile-membership/request snapshot, not a fresh playback or acquisition guarantee. not_here means absent from that profile in retained state, not absent from the entire household."], "missing": ["Justin Movies has seven live viewer-scoped films, not 200. No titles are invented or borrowed across profile boundaries.", "Discovery community/critic ratings, reliable runtime/season counts, personal feed ranking/freshness and 93 original landscape backdrops are unavailable through the permitted read paths."], "unmetRequirements": ["At least 200 real profile-scoped Justin Movies"], "mustNotBeShown": ["Recs Movies", "Recs Shows", "Justin Movies", "Justin Shows", "Mindel Movies", "Mindel Shows", "Shared Movies", "Shared Shows", "DGiC", "Stored names padded with spaces", "Internal bucket and acquisition-instance names"], "privacy": ["Includes actual watched and resume history for Justin, Mindel and Shared independently; Shared is not a union.", "No Jellyfin item/user IDs, source paths, service URLs, credentials, request actor identities, or private hotlinks are included. tmdbId is a public external title identity.", "Only the seven configured playable libraries are selected. Extra observed DGiC views and all recommendation stubs are excluded from playable collections.", "No profile is assumed to be a child or age-restricted. Each playable list and episode shelf is limited to that profile\u2019s configured libraries."], "sourceReadPaths": ["GET /Users (allowlisted mapping only)", "GET /Users/{userId}/Views", "GET /Users/{userId}/Items (parent-scoped playable libraries and metadata-only recommendation libraries)", "GET /Users/{userId}/Items/Resume", "GET /Shows/NextUp?UserId=...", "GET /Items/{itemId}/Images/Primary", "GET /Items/{itemId}/Images/Backdrop/0", "Conductarr GET /catalogue/discover?page=1&media_type=movie (401)", "Conductarr GET /api/v1/recommendations/profiles/justin/feed (401)", "SQLite file URI mode=ro plus PRAGMA query_only=ON: catalogue_metadata, bucket_memberships, native_requests; sqlite_master for schema inspection"], "productionMutations": 0, "artwork": {"files": 632, "bytes": 35201423, "posterDimensions": [300, 450], "backdropDimensions": [1280, 720], "posterQualityRequested": 70, "backdropQualityRequested": 65, "posterDerivedBackdropCount": 93}, "consumerContract": ["profiles is an array in Justin/Mindel/Shared order. Every profile owns movies/shows/anime/resume/nextUp/recentlyAdded/discover item arrays; empty shelves are intentional.", "Item paths are relative to fixture.json. Resolve assets against the fixture directory, not against the outer prototype document.", "Use destinations for visible navigation. No raw library display/stored names belong in UI.", "Deduplicate library/resume/discover by stable local id or (type,tmdbId). Never treat a materialized recommendation as a playable library item.", "played=null and watchStateKnown=false mean unknown, not unwatched. Null ratings are missing, not zero; communityRating source is Jellyfin, not an asserted TMDB/IMDb score.", "availability values are in_library/requested/not_here. No requested examples were observed in the selected discover set; do not fabricate them.", "Render artwork.backdropKind=poster_crop as a poster-derived landscape; prefer the poster over pretending it is original landscape art.", "This static fixture preserves actual current collection sizes, viewer-scoped watch state and unknown discovery ranking; request and playback demonstrations must remain labelled simulations."]}} \ No newline at end of file diff --git a/app/src/debug/assets/wolfskin-library.json b/app/src/debug/assets/wolfskin-library.json deleted file mode 100644 index 76617014..00000000 --- a/app/src/debug/assets/wolfskin-library.json +++ /dev/null @@ -1 +0,0 @@ -{"profiles":[{"key":"justin","movies":[{"id":"movie-the-girl-who-leapt-through-time-2006","title":"The Girl Who Leapt Through Time","year":2006,"type":"movie","runtimeSeconds":5886,"seasonCount":null,"communityRating":7.772,"criticRating":87,"genres":["Fantasy","Animation","Drama"],"overview":"When 17-year-old Makoto Konno gains the ability to 'leap' backwards through time, she immediately sets about improving her grades and preventing personal mishaps. However, she soon realises that changing the past\u2026","poster":"assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg","backdrop":"assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:39:11.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":14069,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":76917,"tmdb":7.775,"tmdbVotes":2345}},{"id":"movie-locke-2014","title":"Locke","year":2014,"type":"movie","runtimeSeconds":5090,"seasonCount":null,"communityRating":6.891,"criticRating":91,"genres":["Drama","Thriller"],"overview":"Ivan Locke has worked hard to craft a good life for himself. Tonight, that life will collapse around him. On the eve of the biggest challenge of his career, Ivan receives a phone call that sets in motion a series of\u2026","poster":"assets/movie-locke-2014-poster.jpg","backdrop":"assets/movie-locke-2014-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:38:48.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":210479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":171265,"tmdb":6.889,"tmdbVotes":3088}},{"id":"movie-look-who-s-back-2015","title":"Look Who's Back","year":2015,"type":"movie","runtimeSeconds":6936,"seasonCount":null,"communityRating":6.793,"criticRating":null,"genres":["Comedy"],"overview":"When Adolf Hitler reawakens at the site of his former bunker in present-day Berlin, he is mistaken for a comedian and quickly becomes a media phenomenon.","poster":"assets/movie-look-who-s-back-2015-poster.jpg","backdrop":"assets/movie-look-who-s-back-2015-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:48.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":318917,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":54672,"tmdb":6.796,"tmdbVotes":1990}},{"id":"movie-memories-1995","title":"Memories","year":1995,"type":"movie","runtimeSeconds":6805,"seasonCount":null,"communityRating":7.465,"criticRating":null,"genres":["Fantasy","Animation","Science Fiction"],"overview":"Three back-to-back anime films by three different directors make up this sci-fi trilogy three years in the making.","poster":"assets/movie-memories-1995-poster.jpg","backdrop":"assets/movie-memories-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-30T21:36:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":42994,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":26638,"tmdb":7.459,"tmdbVotes":622}},{"id":"movie-reincarnated-2012","title":"Reincarnated","year":2012,"type":"movie","runtimeSeconds":5759,"seasonCount":null,"communityRating":6.145,"criticRating":28,"genres":["Music","Documentary"],"overview":"Evolution as an artist is often times what separates legends from the more mundane. After being heavily influenced by his experience in Jamaica \u2013 and his subsequent name change from Snoop Dogg to Snoop Lion \u2013 the LBC\u2026","poster":"assets/movie-reincarnated-2012-poster.jpg","backdrop":"assets/movie-reincarnated-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":123431,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":28,"rtCriticsLabel":"Rotten","rtAudience":41,"rtAudienceLabel":"Spilled","imdb":6.2,"imdbVotes":2563,"tmdb":6.1,"tmdbVotes":69}},{"id":"movie-shipwrecked-nightmare-at-sea-2026","title":"Shipwrecked: Nightmare at Sea","year":2026,"type":"movie","runtimeSeconds":5385,"seasonCount":null,"communityRating":7.181,"criticRating":null,"genres":["Documentary"],"overview":"Never-before-seen footage and survivor accounts trace the 2012 shipwreck of a luxury cruise and the disaster that ensued, in this immersive documentary.","poster":"assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg","backdrop":"assets/movie-shipwrecked-nightmare-at-sea-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T20:11:43.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1715492,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":6.5,"imdbVotes":5080,"tmdb":7.172,"tmdbVotes":87}},{"id":"movie-steins-gate-the-movie-load-region-of-deja-vu-2013","title":"Steins;Gate: The Movie - Load Region of D\u00e9j\u00e0 Vu","year":2013,"type":"movie","runtimeSeconds":5403,"seasonCount":null,"communityRating":7.8,"criticRating":null,"genres":["Animation","Mystery","Sci-Fi"],"overview":"After the events of the anime, Rintarou begins to feel the repercussions of extensive time travel, and eventually completely fades from reality. Kurisu, being the only companion to remember him, now must find a way\u2026","poster":"assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg","backdrop":"assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T14:55:54.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","artwork":{"posterKind":"poster","backdropKind":"backdrop"}}],"shows":[{"id":"tv-andor-2022","title":"Andor","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.293,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"In an era filled with danger, deception and intrigue, Cassian Andor will discover the difference he can make in the struggle against the tyrannical Galactic Empire. He embarks on a path that is destined to turn him\u2026","poster":"assets/tv-andor-2022-poster.jpg","backdrop":"assets/tv-andor-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0432996Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":83867,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.321,"tmdbVotes":2312}},{"id":"tv-the-boys-2019","title":"The Boys","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.443,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure"],"overview":"A group of vigilantes known informally as \u201cThe Boys\u201d set out to take down corrupt superheroes with no more than blue-collar grit and a willingness to fight dirty.","poster":"assets/tv-the-boys-2019-poster.jpg","backdrop":"assets/tv-the-boys-2019-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0410131Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":76479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":13628}},{"id":"tv-californication-2007","title":"Californication","year":2007,"type":"tv","runtimeSeconds":1680,"seasonCount":7,"communityRating":7.632,"criticRating":null,"genres":["Comedy","Drama"],"overview":"A self-loathing, alcoholic writer attempts to repair his damaged relationships with his daughter and her mother while combating sex addiction, a budding drug problem, and the seeming inability to avoid making bad\u2026","poster":"assets/tv-californication-2007-poster.jpg","backdrop":"assets/tv-californication-2007-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0428037Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1215,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.62,"tmdbVotes":1406}},{"id":"tv-firefly-2002","title":"Firefly","year":2002,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.347,"criticRating":null,"genres":["Drama","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In the year 2517, the universe is a vast frontier where lawlessness reigns and adventure awaits! Join Captain Malcolm Reynolds and his ragtag crew aboard the spaceship Serenity as they navigate the fringes of\u2026","poster":"assets/tv-firefly-2002-poster.jpg","backdrop":"assets/tv-firefly-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T11:40:08.4202369Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1437,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":77,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.351,"tmdbVotes":2550}},{"id":"tv-fleabag-2016","title":"Fleabag","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.278,"criticRating":null,"genres":["Comedy","Drama"],"overview":"A portrait into the mind of a dry-witted, sexual, angry, porn-watching, grief-riddled woman, trying to make sense of the world. As she hurls herself headlong at modern living, Fleabag is thrown roughly up against the\u2026","poster":"assets/tv-fleabag-2016-poster.jpg","backdrop":"assets/tv-fleabag-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:42:25.4848214Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":67070,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.3,"tmdbVotes":1993}},{"id":"tv-gravity-falls-2012","title":"Gravity Falls","year":2012,"type":"tv","runtimeSeconds":1320,"seasonCount":2,"communityRating":8.619,"criticRating":null,"genres":["Action & Adventure","Animation","Comedy"],"overview":"Twin brother and sister Dipper and Mabel Pines are in for an unexpected adventure when they spend the summer helping their great uncle Stan run a tourist trap in the mysterious town of Gravity Falls, Oregon.","poster":"assets/tv-gravity-falls-2012-poster.jpg","backdrop":"assets/tv-gravity-falls-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0435827Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":40075,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.624,"tmdbVotes":3647}},{"id":"tv-invincible-2021","title":"INVINCIBLE","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.638,"criticRating":null,"genres":["Animation","Drama","Sci-Fi & Fantasy"],"overview":"Mark Grayson is a normal teenager except for the fact that his father is the most powerful superhero on the planet. Shortly after his seventeenth birthday, Mark begins to develop powers of his own and enters into his\u2026","poster":"assets/tv-invincible-2021-poster.jpg","backdrop":"assets/tv-invincible-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.040159Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":95557,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":6155}},{"id":"tv-the-legend-of-korra-2012","title":"The Legend of Korra","year":2012,"type":"tv","runtimeSeconds":1380,"seasonCount":4,"communityRating":8.2,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Avatar Korra, a headstrong, rebellious, feisty young woman who continually challenges and breaks with tradition, is on her quest to become a fully realized Avatar. In this story, the Avatar struggles to find balance\u2026","poster":"assets/tv-the-legend-of-korra-2012-poster.jpg","backdrop":"assets/tv-the-legend-of-korra-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T00:19:23.2494945Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":33880,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.221,"tmdbVotes":2482}},{"id":"tv-love-death-robots-2019","title":"Love, Death & Robots","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.205,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy"],"overview":"Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.","poster":"assets/tv-love-death-robots-2019-poster.jpg","backdrop":"assets/tv-love-death-robots-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T11:31:20.8818757Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86831,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.203,"tmdbVotes":4238}},{"id":"tv-norsemen-2016","title":"Norsemen","year":2016,"type":"tv","runtimeSeconds":1800,"seasonCount":3,"communityRating":7.177,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Norsemen is an epic and humorous drama series set in the Viking Age. The residents of an 8th-century Viking village experience political rivalry, social change and innovations that upend their culture and way of life.","poster":"assets/tv-norsemen-2016-poster.jpg","backdrop":"assets/tv-norsemen-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.034182Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":68126,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":233}},{"id":"tv-rick-and-morty-2013","title":"Rick and Morty","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":9,"communityRating":8.679,"criticRating":null,"genres":["Animation","Comedy","Sci-Fi & Fantasy"],"overview":"Follows a sociopathic genius scientist who drags his inherently timid grandson on adventures across the universe.","poster":"assets/tv-rick-and-morty-2013-poster.jpg","backdrop":"assets/tv-rick-and-morty-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0419581Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":60625,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":11548}},{"id":"tv-secret-level-2024","title":"Secret Level","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.411,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy","Action & Adventure"],"overview":"Adult animated series of original short stories which are set within the worlds of beloved video games. Each episode serves as a gateway to a new adventure, unlocking exciting worlds from beloved gaming classics and\u2026","poster":"assets/tv-secret-level-2024-poster.jpg","backdrop":"assets/tv-secret-level-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0336417Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":261579,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.392,"tmdbVotes":612}},{"id":"tv-silo-2023","title":"Silo","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":8.178,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama"],"overview":"In a ruined and toxic future, thousands live in a giant silo deep underground. After its sheriff breaks a cardinal rule and residents die mysteriously, engineer Juliette starts to uncover shocking secrets and the\u2026","poster":"assets/tv-silo-2023-poster.jpg","backdrop":"assets/tv-silo-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0346526Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":125988,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.232,"tmdbVotes":2881}},{"id":"tv-south-park-1997","title":"South Park","year":1997,"type":"tv","runtimeSeconds":null,"seasonCount":29,"communityRating":8.336,"criticRating":null,"genres":["Animation","Comedy"],"overview":"Follow the misadventures of four irreverent grade-schoolers in the quiet, dysfunctional town of South Park, Colorado.","poster":"assets/tv-south-park-1997-poster.jpg","backdrop":"assets/tv-south-park-1997-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0456124Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2190,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.332,"tmdbVotes":5250}}],"anime":[{"id":"tv-bleach-2004","title":"Bleach","year":2004,"type":"tv","runtimeSeconds":1440,"seasonCount":17,"communityRating":8.363,"criticRating":null,"genres":["Action & Adventure","Animation","Sci-Fi & Fantasy"],"overview":"For as long as he can remember, Ichigo Kurosaki has been able to see ghosts. But when he meets Rukia, a Soul Reaper who battles evil spirits known as Hollows, he finds his life is changed forever. Now, with a\u2026","poster":"assets/tv-bleach-2004-poster.jpg","backdrop":"assets/tv-bleach-2004-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0359827Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":30984,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":2331}},{"id":"tv-chainsaw-man-2022","title":"Chainsaw Man","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":1,"communityRating":8.515,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Denji has a simple dream\u2014to live a happy and peaceful life, spending time with a girl he likes. This is a far cry from reality, however, as Denji is forced by the yakuza into killing devils in order to pay off his\u2026","poster":"assets/tv-chainsaw-man-2022-poster.jpg","backdrop":"assets/tv-chainsaw-man-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2095501Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":114410,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.508,"tmdbVotes":2364}},{"id":"tv-cyberpunk-edgerunners-2022","title":"Cyberpunk: Edgerunners","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.466,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a dystopia riddled with corruption and cybernetic implants, a talented but reckless street kid strives to become a mercenary outlaw \u2014 an edgerunner.","poster":"assets/tv-cyberpunk-edgerunners-2022-poster.jpg","backdrop":"assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2093107Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":105248,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.507,"tmdbVotes":2132}},{"id":"tv-dan-da-dan-2024","title":"Dan Da Dan","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.538,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"In a bet to prove whether ghosts or aliens exist, two high schoolers face terrifying paranormal threats, gain superpowers and maybe even fall in love?!","poster":"assets/tv-dan-da-dan-2024-poster.jpg","backdrop":"assets/tv-dan-da-dan-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2652252Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":240411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.517,"tmdbVotes":986}},{"id":"tv-dr-stone-2019","title":"Dr. STONE","year":2019,"type":"tv","runtimeSeconds":1440,"seasonCount":4,"communityRating":8.541,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"One fateful day, all of humanity was petrified by a blinding flash of light. After several millennia, high schooler Taiju awakens and finds himself lost in a world of statues. However, he\u2019s not alone! His\u2026","poster":"assets/tv-dr-stone-2019-poster.jpg","backdrop":"assets/tv-dr-stone-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2091579Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86031,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.53,"tmdbVotes":1814}},{"id":"tv-frieren-beyond-journey-s-end-2023","title":"Frieren: Beyond Journey's End","year":2023,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":8.803,"criticRating":null,"genres":["Animation","Action & Adventure","Drama"],"overview":"After the party of heroes defeated the Demon King, they restored peace to the land and returned to lives of solitude. Generations pass, and the elven mage Frieren comes face to face with humanity\u2019s mortality. She\u2026","poster":"assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg","backdrop":"assets/tv-frieren-beyond-journey-s-end-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2094435Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":209867,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.826,"tmdbVotes":1016}},{"id":"tv-fullmetal-alchemist-brotherhood-2009","title":"Fullmetal Alchemist: Brotherhood","year":2009,"type":"tv","runtimeSeconds":1500,"seasonCount":1,"communityRating":8.7,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Disregard for alchemy\u2019s laws ripped half of Edward Elric\u2019s limbs from his body and left his brother Alphonse\u2019s soul clinging to a suit of armor. To restore what was lost, the brothers seek the Philosopher\u2019s Stone.\u2026","poster":"assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg","backdrop":"assets/tv-fullmetal-alchemist-brotherhood-2009-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T10:27:12.3573939Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":31911,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":2633}},{"id":"tv-jujutsu-kaisen-2020","title":"JUJUTSU KAISEN","year":2020,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.583,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Yuji Itadori is a boy with tremendous physical strength, though he lives a completely ordinary high school life. One day, to save a classmate who has been attacked by curses, he eats the finger of Ryomen Sukuna,\u2026","poster":"assets/tv-jujutsu-kaisen-2020-poster.jpg","backdrop":"assets/tv-jujutsu-kaisen-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.207573Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":95479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.574,"tmdbVotes":4735}},{"id":"tv-kaiju-no-8-2024","title":"Kaiju No. 8","year":2024,"type":"tv","runtimeSeconds":1440,"seasonCount":2,"communityRating":8.436,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world plagued by creatures known as Kaiju, Kafka Hibino aspired to enlist in The Defense Force. He makes a promise to enlist with his childhood friend, Mina Ashiro. Soon, life takes them in separate ways. While\u2026","poster":"assets/tv-kaiju-no-8-2024-poster.jpg","backdrop":"assets/tv-kaiju-no-8-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2646566Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207468,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":836}},{"id":"tv-my-hero-academia-2016","title":"My Hero Academia","year":2016,"type":"tv","runtimeSeconds":1440,"seasonCount":8,"communityRating":8.607,"criticRating":null,"genres":["Action & Adventure","Animation","Sci-Fi & Fantasy"],"overview":"After he saves a bully from a Villain, a normal student is granted a superpower that allows him to attend a high school training academy for Heroes.","poster":"assets/tv-my-hero-academia-2016-poster.jpg","backdrop":"assets/tv-my-hero-academia-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2089315Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":65930,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":5426}},{"id":"tv-neon-genesis-evangelion-1995","title":"Neon Genesis Evangelion","year":1995,"type":"tv","runtimeSeconds":1500,"seasonCount":1,"communityRating":8.568,"criticRating":null,"genres":["Sci-Fi & Fantasy","Animation","Drama"],"overview":"At the turn of the century, the Angels returned to Earth, seeking to wipe out humanity in an apocalyptic fury. Devastated, mankind's last remnants moved underground to wait for the day when the Angels would come back\u2026","poster":"assets/tv-neon-genesis-evangelion-1995-poster.jpg","backdrop":"assets/tv-neon-genesis-evangelion-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0340629Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":890,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.56,"tmdbVotes":2205}},{"id":"tv-record-of-ragnarok-2021","title":"Record of Ragnarok","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":8.402,"criticRating":null,"genres":["Action & Adventure","Animation","Sci-Fi & Fantasy"],"overview":"Before eradicating humankind from the world, the gods give them one last chance to prove themselves worthy of survival. Let the Ragnarok battles begin.","poster":"assets/tv-record-of-ragnarok-2021-poster.jpg","backdrop":"assets/tv-record-of-ragnarok-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2085766Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":114868,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.403,"tmdbVotes":1929}},{"id":"tv-sakamoto-days-2025","title":"SAKAMOTO DAYS","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.071,"criticRating":null,"genres":["Animation","Comedy","Action & Adventure"],"overview":"Once the greatest hitman of all, Taro Sakamoto retired in the name of love. But when his past catches up, he must fight to protect his beloved family.","poster":"assets/tv-sakamoto-days-2025-poster.jpg","backdrop":"assets/tv-sakamoto-days-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.266066Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207332,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.057,"tmdbVotes":375}},{"id":"tv-sentenced-to-be-a-hero-2025","title":"Sentenced to Be a Hero","year":2025,"type":"tv","runtimeSeconds":1440,"seasonCount":1,"communityRating":8.338,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy","Action & Adventure"],"overview":"In a world where heroism is a punishment, Xylo Forbartz, a condemned goddess killer, battles endless hordes of monstrous abominations as part of Penal Hero Unit 9004. Death is no escape, only a cycle of resurrection\u2026","poster":"assets/tv-sentenced-to-be-a-hero-2025-poster.jpg","backdrop":"assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2666791Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":249907,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.244,"tmdbVotes":129}},{"id":"tv-spy-x-family-2022","title":"SPY x FAMILY","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.506,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"A spy, an assassin and a telepath come together to pose as a family, each for their own reasons, while hiding their true identities from each other.","poster":"assets/tv-spy-x-family-2022-poster.jpg","backdrop":"assets/tv-spy-x-family-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2670364Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":120089,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.5,"tmdbVotes":2382}},{"id":"tv-the-tatami-galaxy-2010","title":"The Tatami Galaxy","year":2010,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":8.271,"criticRating":null,"genres":["Animation","Comedy","Drama"],"overview":"When a college dropout stops for a late night bite at a mysterious ramen stand, he crosses paths with a self-proclaimed deity of matrimony. This bizarre meeting sends the young man hurtling through a horrifying\u2026","poster":"assets/tv-the-tatami-galaxy-2010-poster.jpg","backdrop":"assets/tv-the-tatami-galaxy-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-18T05:52:51.0023256Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":36243,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.3,"tmdbVotes":152}},{"id":"tv-vinland-saga-2019","title":"Vinland Saga","year":2019,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":8.527,"criticRating":null,"genres":["Animation","War & Politics","Action & Adventure"],"overview":"For a thousand years, the Vikings have made quite a name and reputation for themselves as the strongest families with a thirst for violence. Thorfinn, the son of one of the Vikings' greatest warriors, spends his\u2026","poster":"assets/tv-vinland-saga-2019-poster.jpg","backdrop":"assets/tv-vinland-saga-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.207389Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":88803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.509,"tmdbVotes":1064}},{"id":"tv-yu-yu-hakusho-1992","title":"Yu Yu Hakusho","year":1992,"type":"tv","runtimeSeconds":1440,"seasonCount":4,"communityRating":8.5,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"After dying to save a boy, delinquent tough guy Yusuke Urameshi is granted another chance at life by redeeming himself as a \"Spirit Detective.\"","poster":"assets/tv-yu-yu-hakusho-1992-poster.jpg","backdrop":"assets/tv-yu-yu-hakusho-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T21:46:01.5733049Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":30669,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.475,"tmdbVotes":519}}],"discover":[{"id":"movie-the-amazing-spider-man-2012","title":"The Amazing Spider-Man","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"A teenage Peter Parker grapples with both high school and amazing super-human crises as his alter-ego Spider-Man.","poster":"assets/movie-the-amazing-spider-man-2012-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1930,"ratings":{"rtCritics":71,"rtCriticsLabel":"Certified Fresh","rtAudience":77,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":791938,"tmdb":6.768,"tmdbVotes":19312}},{"id":"movie-the-amazing-spider-man-2-2014","title":"The Amazing Spider-Man 2","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026","poster":"assets/movie-the-amazing-spider-man-2-2014-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":102382,"ratings":{"rtCritics":51,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":611981,"tmdb":6.567,"tmdbVotes":14919}},{"id":"movie-the-animatrix-2003","title":"The Animatrix","year":2003,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction"],"overview":"Straight from the creators of the groundbreaking Matrix trilogy, this collection of short animated films from the world's leading anime directors fuses computer graphics and Japanese anime to provide the background\u2026","poster":"assets/movie-the-animatrix-2003-poster.jpg","backdrop":"assets/movie-the-animatrix-2003-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":55931,"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":78,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":91929,"tmdb":7.221,"tmdbVotes":1872}},{"id":"movie-ant-man-and-the-wasp-quantumania-2023","title":"Ant-Man and the Wasp: Quantumania","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026","poster":"assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg","backdrop":"assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":640146,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6,"imdbVotes":281738,"tmdb":6.197,"tmdbVotes":6236}},{"id":"movie-appleseed-alpha-2014","title":"Appleseed Alpha","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction","Action"],"overview":"The Third World War (2099\u20132126) devastated part of the Earth. Former nations are reforming while new powers emerge, and groups of humans struggle to survive in ruined cities. The story follows Deunan Knute and her\u2026","poster":"assets/movie-appleseed-alpha-2014-poster.jpg","backdrop":"assets/movie-appleseed-alpha-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":269650,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":59,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":9689,"tmdb":6.808,"tmdbVotes":436}},{"id":"movie-aquaman-and-the-lost-kingdom-2023","title":"Aquaman and the Lost Kingdom","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Fantasy"],"overview":"Black Manta seeks revenge on Aquaman for his father's death. Wielding the Black Trident's power, he becomes a formidable foe. To defend Atlantis, Arthur (Aquaman) forges an alliance with his imprisoned brother. They\u2026","poster":"assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg","backdrop":"assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":572802,"ratings":{"rtCritics":33,"rtCriticsLabel":"Rotten","rtAudience":79,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":122422,"tmdb":6.48,"tmdbVotes":3748}},{"id":"movie-the-avengers-2012","title":"The Avengers","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Adventure"],"overview":"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026","poster":"assets/movie-the-avengers-2012-poster.jpg","backdrop":"assets/movie-the-avengers-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":24428,"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":1590693,"tmdb":8.084,"tmdbVotes":40262}},{"id":"movie-avengers-confidential-black-widow-punisher-2014","title":"Avengers Confidential: Black Widow & Punisher","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction","Action"],"overview":"When the Punisher takes out a black-market weapons dealer, he stumbles upon a far-reaching terrorist plot devised by a group known as Leviathan.","poster":"assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg","backdrop":"assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":257346,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":55,"rtAudienceLabel":"Spilled","imdb":5.7,"imdbVotes":5166,"tmdb":6.368,"tmdbVotes":303}},{"id":"movie-avengers-age-of-ultron-2015","title":"Avengers: Age of Ultron","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth\u2019s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron\u2026","poster":"assets/movie-avengers-age-of-ultron-2015-poster.jpg","backdrop":"assets/movie-avengers-age-of-ultron-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":99861,"ratings":{"rtCritics":75,"rtCriticsLabel":"Certified Fresh","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":1025876,"tmdb":7.278,"tmdbVotes":25289}},{"id":"movie-avengers-infinity-war-2018","title":"Avengers: Infinity War","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Adventure","Action","Science Fiction"],"overview":"As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his\u2026","poster":"assets/movie-avengers-infinity-war-2018-poster.jpg","backdrop":"assets/movie-avengers-infinity-war-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":299536,"ratings":{"rtCritics":85,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":8.4,"imdbVotes":1421954,"tmdb":8.246,"tmdbVotes":33331}},{"id":"movie-batman-ninja-2018","title":"Batman Ninja","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action","Science Fiction"],"overview":"Batman, along with many of his allies and adversaries, finds himself transported to feudal Japan by Gorilla Grodd's time displacement machine.","poster":"assets/movie-batman-ninja-2018-poster.jpg","backdrop":"assets/movie-batman-ninja-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":485942,"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":60,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":24579,"tmdb":5.9,"tmdbVotes":1106}},{"id":"movie-batman-v-superman-dawn-of-justice-2016","title":"Batman v Superman: Dawn of Justice","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Fantasy"],"overview":"Fearing the actions of a god-like Super Hero left unchecked, Gotham City\u2019s own formidable, forceful vigilante takes on Metropolis\u2019s most revered, modern-day savior, while the world wrestles with what sort of hero it\u2026","poster":"assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg","backdrop":"assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":209112,"ratings":{"rtCritics":28,"rtCriticsLabel":"Rotten","rtAudience":63,"rtAudienceLabel":"Upright","imdb":6.4,"imdbVotes":801382,"tmdb":6.014,"tmdbVotes":19527}},{"id":"movie-batman-the-dark-knight-returns-part-1-2012","title":"Batman: The Dark Knight Returns, Part 1","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Animation"],"overview":"Batman has not been seen for ten years. A new breed of criminal ravages Gotham City, forcing 55-year-old Bruce Wayne back into the cape and cowl. But, does he still have what it takes to fight crime in a new era?","poster":"assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg","backdrop":"assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":123025,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":68943,"tmdb":7.742,"tmdbVotes":1766}},{"id":"movie-black-adam-2022","title":"Black Adam","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods\u2014and imprisoned just as quickly\u2014Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the\u2026","poster":"assets/movie-black-adam-2022-poster.jpg","backdrop":"assets/movie-black-adam-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":436270,"ratings":{"rtCritics":39,"rtCriticsLabel":"Rotten","rtAudience":87,"rtAudienceLabel":"Upright","imdb":6.1,"imdbVotes":309624,"tmdb":6.747,"tmdbVotes":7370}},{"id":"movie-black-panther-2018","title":"Black Panther","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026","poster":"assets/movie-black-panther-2018-poster.jpg","backdrop":"assets/movie-black-panther-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284054,"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":null,"rtAudienceLabel":"Spilled","imdb":7.3,"imdbVotes":931120,"tmdb":7.36,"tmdbVotes":24190}},{"id":"movie-black-panther-wakanda-forever-2022","title":"Black Panther: Wakanda Forever","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Queen Ramonda, Shuri, M\u2019Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T\u2019Challa\u2019s death. As the Wakandans strive to embrace their next chapter, the\u2026","poster":"assets/movie-black-panther-wakanda-forever-2022-poster.jpg","backdrop":"assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":505642,"ratings":{"rtCritics":84,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":405088,"tmdb":6.984,"tmdbVotes":7920}},{"id":"movie-blame-2017","title":"BLAME!","year":2017,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Drama","Science Fiction"],"overview":"In the distant technological future, civilization has reached its ultimate Net-based form. An \"infection\" in the past caused the automated systems to spiral out of order, resulting in a multi-leveled city structure\u2026","poster":"assets/movie-blame-2017-poster.jpg","backdrop":"assets/movie-blame-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":409421,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":10258,"tmdb":6.523,"tmdbVotes":370}},{"id":"movie-bleach-the-movie-fade-to-black-2008","title":"Bleach the Movie: Fade to Black","year":2008,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Animation","Fantasy"],"overview":"A strange disturbance in the World of the Living sends Ichigo and Kon to the Soul Society, where the Seireitei has been devastated by an explosion of reishi and Rukia is missing. Stranger still, none of the Soul\u2026","poster":"assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg","backdrop":"assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":17165,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":67,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":3693,"tmdb":7.1,"tmdbVotes":206}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-the-100-2014","title":"The 100","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026","poster":"assets/tv-the-100-2014-poster.jpg","backdrop":"assets/tv-the-100-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":48866,"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.878,"tmdbVotes":8855}},{"id":"tv-the-abbott-and-costello-show-1952","title":"The Abbott and Costello Show","year":1952,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"Bud and Lou are unemployed actors living in Mr. Fields\u2019 boarding house. Lou\u2019s girlfriend Hillary lives across the hall. Many situations arise leading to slapstick and puns.","poster":"assets/tv-the-abbott-and-costello-show-1952-poster.jpg","backdrop":"assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":11821,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":22}},{"id":"tv-accel-world-2012","title":"Accel World","year":2012,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Drama"],"overview":"A short, overweight student uses the Neuro Link, an evolution of the Nerve Gear, to escape himself from the reality. But his skills in the virtual world brings him to the attention of Kuroyukihime, a popular student\u2026","poster":"assets/tv-accel-world-2012-poster.jpg","backdrop":"assets/tv-accel-world-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":43167,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.429,"tmdbVotes":163}},{"id":"tv-ahsoka-2023","title":"Ahsoka","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"Former Jedi Knight Ahsoka Tano investigates an emerging threat to a vulnerable galaxy.","poster":"assets/tv-ahsoka-2023-poster.jpg","backdrop":"assets/tv-ahsoka-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":114461,"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":1351}},{"id":"tv-american-dad-2005","title":"American Dad!","year":2005,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Comedy"],"overview":"The series focuses on an eccentric motley crew that is the Smith family and their three housemates: Father, husband, and breadwinner Stan Smith; his better half housewife, Francine Smith; their college-aged daughter,\u2026","poster":"assets/tv-american-dad-2005-poster.jpg","backdrop":"assets/tv-american-dad-2005-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1433,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.022,"tmdbVotes":2481}},{"id":"tv-american-gods-2017","title":"American Gods","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Sci-Fi & Fantasy"],"overview":"An ex-con becomes the traveling partner of a conman who turns out to be one of the older gods trying to recruit troops to battle the upstart deities. Based on Neil Gaiman's fantasy novel.","poster":"assets/tv-american-gods-2017-poster.jpg","backdrop":"assets/tv-american-gods-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":46639,"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":1852}},{"id":"tv-andromeda-2000","title":"Andromeda","year":2000,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Drama","Sci-Fi & Fantasy"],"overview":"Captain Dylan Hunt and his crew quest to restore a government that once presided over an extended peace and prosperity.","poster":"assets/tv-andromeda-2000-poster.jpg","backdrop":"assets/tv-andromeda-2000-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":2985,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.068,"tmdbVotes":315}},{"id":"tv-aqua-teen-hunger-force-2000","title":"Aqua Teen Hunger Force","year":2000,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Comedy"],"overview":"The surreal adventures of three anthropomorphic fast food items: Master Shake, Frylock and Meatwad, and their human nextdoor neighbor, Carl Brutananadilewski.","poster":"assets/tv-aqua-teen-hunger-force-2000-poster.jpg","backdrop":"assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":251,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":272}},{"id":"tv-arifureta-from-commonplace-to-world-s-strongest-2019","title":"Arifureta: From Commonplace to World's Strongest","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Hajime Nagumo and his high school class are suddenly summoned to a fantastical land as heroes. But while most of his classmates have powerful stats and abilities, Hajime does not. Underappreciated and unprepared, he\u2026","poster":"assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg","backdrop":"assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":86034,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.382,"tmdbVotes":662}},{"id":"tv-arrow-2012","title":"Arrow","year":2012,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Action & Adventure"],"overview":"Spoiled billionaire playboy Oliver Queen is missing and presumed dead when his yacht is lost at sea. He returns five years later a changed man, determined to clean up the city as a hooded vigilante armed with a bow.","poster":"assets/tv-arrow-2012-poster.jpg","backdrop":"assets/tv-arrow-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1412,"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.834,"tmdbVotes":6537}},{"id":"tv-avatar-the-last-airbender-2024","title":"Avatar: The Last Airbender","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.","poster":"assets/tv-avatar-the-last-airbender-2024-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":82452,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":1406}},{"id":"tv-avatar-seven-havens-2026","title":"Avatar: Seven Havens","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026","poster":"assets/tv-avatar-seven-havens-2026-poster.jpg","backdrop":"assets/tv-avatar-seven-havens-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284833,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.9,"tmdbVotes":183}},{"id":"tv-batman-the-brave-and-the-bold-2008","title":"Batman: The Brave and the Bold","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.","poster":"assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","backdrop":"assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":15804,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":295}},{"id":"tv-batwoman-2019","title":"Batwoman","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure"],"overview":"Kate Kane, armed with a passion for social justice and a flair for speaking her mind, soars onto the streets of Gotham as Batwoman, an out lesbian and highly trained street fighter primed to snuff out the failing\u2026","poster":"assets/tv-batwoman-2019-poster.jpg","backdrop":"assets/tv-batwoman-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":89247,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":1619}},{"id":"tv-big-mouth-2017","title":"Big Mouth","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Animation"],"overview":"Teenage friends find their lives upended by the wonders and horrors of puberty in this edgy comedy from real-life pals Nick Kroll and Andrew Goldberg.","poster":"assets/tv-big-mouth-2017-poster.jpg","backdrop":"assets/tv-big-mouth-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":74204,"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.591,"tmdbVotes":1274}},{"id":"tv-black-lightning-2018","title":"Black Lightning","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"Jefferson Pierce is a man wrestling with a secret. As the father of two daughters and principal of a charter high school that also serves as a safe haven for young people in a New Orleans neighborhood overrun by gang\u2026","poster":"assets/tv-black-lightning-2018-poster.jpg","backdrop":"assets/tv-black-lightning-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":71663,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.907,"tmdbVotes":991}}]},{"key":"mindel","movies":[{"id":"movie-chicago-2002","title":"Chicago","year":2002,"type":"movie","runtimeSeconds":6808,"seasonCount":null,"communityRating":7.131,"criticRating":87,"genres":["Comedy","Crime","Drama"],"overview":"Murderesses Velma Kelly and Roxie Hart find themselves on death row together and fight for the fame that will keep them from the gallows in 1920s Chicago.","poster":"assets/movie-chicago-2002-poster.jpg","backdrop":"assets/movie-chicago-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:53.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1574,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":259272,"tmdb":7.135,"tmdbVotes":3025}},{"id":"movie-coherence-2014","title":"Coherence","year":2014,"type":"movie","runtimeSeconds":5286,"seasonCount":null,"communityRating":7.21,"criticRating":89,"genres":["Thriller","Science Fiction"],"overview":"Four couples gather for dinner the night a mysterious and powerful comet passes overhead.","poster":"assets/movie-coherence-2014-poster.jpg","backdrop":"assets/movie-coherence-2014-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2017-08-12T23:49:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":220289,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":180354,"tmdb":7.208,"tmdbVotes":3702}},{"id":"movie-the-killing-of-a-sacred-deer-2017","title":"The Killing of a Sacred Deer","year":2017,"type":"movie","runtimeSeconds":7262,"seasonCount":null,"communityRating":7.003,"criticRating":79,"genres":["Drama","Thriller","Mystery"],"overview":"Dr. Steven Murphy is a renowned cardiovascular surgeon who presides over a spotless household with his wife and two children. Lurking at the margins of his idyllic suburban existence is Martin, a fatherless teen who\u2026","poster":"assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg","backdrop":"assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T21:49:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":399057,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":219488,"tmdb":7.001,"tmdbVotes":4640}},{"id":"movie-night-is-short-walk-on-girl-2017","title":"Night Is Short, Walk On Girl","year":2017,"type":"movie","runtimeSeconds":5548,"seasonCount":null,"communityRating":7.421,"criticRating":90,"genres":["Comedy","Romance","Animation"],"overview":"As a group of university students go out for a night on the town, a sophomore known only as \"The Girl with Black Hair\" experiences a series of surreal encounters with the local nightlife \u2013 all the while unaware of\u2026","poster":"assets/movie-night-is-short-walk-on-girl-2017-poster.jpg","backdrop":"assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:39:14.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":430214,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":7640,"tmdb":7.4,"tmdbVotes":267}},{"id":"movie-thelma-louise-1991","title":"Thelma & Louise","year":1991,"type":"movie","runtimeSeconds":7786,"seasonCount":null,"communityRating":7.596,"criticRating":87,"genres":["Drama","Crime","Adventure"],"overview":"Taking a break from their dreary lives, close friends Thelma and Louise embark on a short weekend trip that ends in unforeseen incriminating circumstances. As fugitives, both women rediscover the strength of their\u2026","poster":"assets/movie-thelma-louise-1991-poster.jpg","backdrop":"assets/movie-thelma-louise-1991-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:40.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1541,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":197947,"tmdb":7.594,"tmdbVotes":4063}},{"id":"movie-vicky-cristina-barcelona-2008","title":"Vicky Cristina Barcelona","year":2008,"type":"movie","runtimeSeconds":5804,"seasonCount":null,"communityRating":6.798,"criticRating":80,"genres":["Drama","Romance"],"overview":"Two girlfriends on a summer holiday in Spain become enamored with the same painter, unaware that his ex-wife, with whom he has a tempestuous relationship, is about to re-enter the picture.","poster":"assets/movie-vicky-cristina-barcelona-2008-poster.jpg","backdrop":"assets/movie-vicky-cristina-barcelona-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:34.0000000Z","lastPlayedAt":"2026-08-11T20:12:57.3064089Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":5038,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Certified Fresh","rtAudience":74,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":283286,"tmdb":6.797,"tmdbVotes":4164}}],"shows":[{"id":"tv-all-creatures-great-small-2020","title":"All Creatures Great & Small","year":2020,"type":"tv","runtimeSeconds":3000,"seasonCount":7,"communityRating":8.2,"criticRating":null,"genres":["Drama","Comedy"],"overview":"The heartwarming and humorous adventures of a young country vet in the Yorkshire Dales in the 1930s. A remake of the 1978 series.","poster":"assets/tv-all-creatures-great-small-2020-poster.jpg","backdrop":"assets/tv-all-creatures-great-small-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:13:03.4266156Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":108255,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.2,"tmdbVotes":168}},{"id":"tv-back-to-life-2019","title":"Back to Life","year":2019,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":7.367,"criticRating":null,"genres":["Comedy","Drama"],"overview":"When Miri Matteson returns home after eighteen years, can she integrate back into her old life? With a terrible event from her past hanging over her, it won\u2019t be easy.","poster":"assets/tv-back-to-life-2019-poster.jpg","backdrop":"assets/tv-back-to-life-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0688758Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":88510,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":65}},{"id":"tv-big-boys-2022","title":"Big Boys","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":7.6,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Two boys from very different ends of the 'spectrum of masculinity' become best friends at Brent University freshers' week; in their first year of uni, they explore, experiment and try to discover themselves, helping\u2026","poster":"assets/tv-big-boys-2022-poster.jpg","backdrop":"assets/tv-big-boys-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:25:58.2156837Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":202851,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":49}},{"id":"tv-call-the-midwife-2012","title":"Call the Midwife","year":2012,"type":"tv","runtimeSeconds":3120,"seasonCount":15,"communityRating":7.5,"criticRating":null,"genres":["Drama"],"overview":"Drama following the lives of a group of midwives working in the poverty-stricken East End of London during the 1950s, based on the best-selling memoirs of Jennifer Worth.","poster":"assets/tv-call-the-midwife-2012-poster.jpg","backdrop":"assets/tv-call-the-midwife-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0377216Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":39793,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.577,"tmdbVotes":267}},{"id":"tv-derry-girls-2018","title":"Derry Girls","year":2018,"type":"tv","runtimeSeconds":1380,"seasonCount":3,"communityRating":8.144,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Amidst the political conflict of Northern Ireland in the 1990s, five secondary school students square off with the universal challenges of being a teenager.","poster":"assets/tv-derry-girls-2018-poster.jpg","backdrop":"assets/tv-derry-girls-2018-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-02T20:01:55.8955537Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":76148,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.138,"tmdbVotes":454}},{"id":"tv-desperate-housewives-2004","title":"Desperate Housewives","year":2004,"type":"tv","runtimeSeconds":2520,"seasonCount":8,"communityRating":7.896,"criticRating":null,"genres":["Mystery","Drama","Comedy"],"overview":"Looking down on her friends and family isn't a way of life for Mary Alice Young... it's a way of death. One day, in her perfect house, in the loveliest of suburbs, Mary Alice ended it all. Now she's taking us into\u2026","poster":"assets/tv-desperate-housewives-2004-poster.jpg","backdrop":"assets/tv-desperate-housewives-2004-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-29T20:56:40.3505596Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":693,"artwork":{"posterKind":"poster","backdropKind":"center_crop"},"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.896,"tmdbVotes":2171}},{"id":"tv-the-diplomat-2023","title":"The Diplomat","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":7.569,"criticRating":null,"genres":["Drama","War & Politics"],"overview":"Amid an international crisis, a US diplomat contends with her high-profile job as ambassador to the UK and her strained marriage to a political star.","poster":"assets/tv-the-diplomat-2023-poster.jpg","backdrop":"assets/tv-the-diplomat-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0640348Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":203857,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.652,"tmdbVotes":521}},{"id":"tv-the-durrells-2016","title":"The Durrells","year":2016,"type":"tv","runtimeSeconds":2760,"seasonCount":4,"communityRating":7.402,"criticRating":null,"genres":["Drama","Comedy","Family"],"overview":"In 1935, financially strapped widow Louisa Durrell, whose life has fallen apart, decides to move from England, with her four children (three sons, one daughter), to the island of Corfu, Greece. Once there, the family\u2026","poster":"assets/tv-the-durrells-2016-poster.jpg","backdrop":"assets/tv-the-durrells-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0619436Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":66134,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":127}},{"id":"tv-etoile-2025","title":"\u00c9toile","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.197,"criticRating":null,"genres":["Comedy","Drama"],"overview":"In an ambitious gambit to save their storied institutions, two world-renowned ballet companies in New York City and Paris swap their most talented stars.","poster":"assets/tv-etoile-2025-poster.jpg","backdrop":"assets/tv-etoile-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0601383Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":225383,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":40}},{"id":"tv-gilmore-girls-2000","title":"Gilmore Girls","year":2000,"type":"tv","runtimeSeconds":2640,"seasonCount":7,"communityRating":7.893,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Set in the charming town of Stars Hollow, Connecticut, the series follows the captivating lives of Lorelai and Rory Gilmore, a mother/daughter pair who have a relationship most people only dream of.","poster":"assets/tv-gilmore-girls-2000-poster.jpg","backdrop":"assets/tv-gilmore-girls-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0631035Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4586,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":1339}},{"id":"tv-ginny-georgia-2021","title":"Ginny & Georgia","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":8.026,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Free-spirited Georgia and her two kids, Ginny and Austin, move north in search of a fresh start but find that the road to new beginnings can be bumpy.","poster":"assets/tv-ginny-georgia-2021-poster.jpg","backdrop":"assets/tv-ginny-georgia-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0694828Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117581,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":68,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8,"tmdbVotes":1840}},{"id":"tv-hellcats-2010","title":"Hellcats","year":2010,"type":"tv","runtimeSeconds":2460,"seasonCount":1,"communityRating":7.5,"criticRating":null,"genres":["Drama"],"overview":"Pre-law student Marti Perkins' world is flipped upside down when she loses her scholarship, and realizes the only way she can stay in school is by reigniting her dormant teen gymnastic skills to win a place on Lancer\u2026","poster":"assets/tv-hellcats-2010-poster.jpg","backdrop":"assets/tv-hellcats-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T20:39:02.7719318Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":32504,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":116}},{"id":"tv-high-potential-2024","title":"High Potential","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.201,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"Morgan is a single mom with an exceptional mind, whose unconventional knack for solving crimes leads to an unusual and unstoppable partnership with a by-the-book seasoned detective.","poster":"assets/tv-high-potential-2024-poster.jpg","backdrop":"assets/tv-high-potential-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0686091Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":226637,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":98,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.3,"tmdbVotes":707}},{"id":"tv-how-to-get-to-heaven-from-belfast-2026","title":"How to Get to Heaven from Belfast","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.027,"criticRating":null,"genres":["Mystery","Comedy","Drama"],"overview":"Three lifelong pals embark on a chaotic quest to solve the mystery of their old friend's suspicious death and keep their own dark secret under wraps.","poster":"assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg","backdrop":"assets/tv-how-to-get-to-heaven-from-belfast-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0683192Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":233079,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.063,"tmdbVotes":95}},{"id":"tv-the-hunting-wives-2025","title":"The Hunting Wives","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":6.932,"criticRating":null,"genres":["Drama","Mystery"],"overview":"Sophie trades New England for East Texas and falls into a wealthy socialite's magnetic orbit \u2014 where a clique of housewives hide deadly secrets.","poster":"assets/tv-the-hunting-wives-2025-poster.jpg","backdrop":"assets/tv-the-hunting-wives-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T19:42:46.3227892Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":244447,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.964,"tmdbVotes":112}},{"id":"tv-the-lost-flowers-of-alice-hart-2023","title":"The Lost Flowers of Alice Hart","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.634,"criticRating":null,"genres":["Drama","Mystery"],"overview":"After losing her parents in a mysterious fire, nine-year-old Alice Hart is raised by her grandmother June on a flower farm where she learns there are secrets within secrets. But years on, an unearthed betrayal sees\u2026","poster":"assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg","backdrop":"assets/tv-the-lost-flowers-of-alice-hart-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0607236Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":125816,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":82,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.64,"tmdbVotes":161}},{"id":"tv-love-is-blind-2020","title":"Love Is Blind","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":11,"communityRating":6.261,"criticRating":null,"genres":["Reality"],"overview":"Nick and Vanessa Lachey host this social experiment where single men and women look for love and get engaged, all before meeting in person.","poster":"assets/tv-love-is-blind-2020-poster.jpg","backdrop":"assets/tv-love-is-blind-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T17:11:08.2447226Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":99353,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.233,"tmdbVotes":118}},{"id":"tv-maximum-pleasure-guaranteed-2026","title":"Maximum Pleasure Guaranteed","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":6.676,"criticRating":null,"genres":["Comedy"],"overview":"A newly divorced mom falls down a dangerous rabbit hole of blackmail, murder, and youth soccer.","poster":"assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg","backdrop":"assets/tv-maximum-pleasure-guaranteed-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-07T21:04:02.8277793Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":285404,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.799,"tmdbVotes":92}},{"id":"tv-minx-2022","title":"Minx","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.833,"criticRating":null,"genres":["Comedy","Drama"],"overview":"An earnest young feminist joins forces with a low-rent publisher to make the world's first erotic magazine for women.","poster":"assets/tv-minx-2022-poster.jpg","backdrop":"assets/tv-minx-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0653613Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":118303,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.9,"tmdbVotes":123}},{"id":"tv-offspring-2010","title":"Offspring","year":2010,"type":"tv","runtimeSeconds":3600,"seasonCount":7,"communityRating":6.9,"criticRating":null,"genres":["Drama"],"overview":"An exuberant drama set in Melbourne's Fitzroy, centering on Nina Proudman and her struggle to deal with her fabulously messy family, her hunt for a decent love life and her tendency to overthink and fly off into fantasy.","poster":"assets/tv-offspring-2010-poster.jpg","backdrop":"assets/tv-offspring-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0664581Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":33155,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.9,"tmdbVotes":27}},{"id":"tv-only-murders-in-the-building-2021","title":"Only Murders in the Building","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.398,"criticRating":null,"genres":["Comedy","Mystery","Crime"],"overview":"Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.","poster":"assets/tv-only-murders-in-the-building-2021-poster.jpg","backdrop":"assets/tv-only-murders-in-the-building-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:25:58.2163301Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":107113,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.398,"tmdbVotes":2333}},{"id":"tv-outlander-blood-of-my-blood-2025","title":"Outlander: Blood of My Blood","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.542,"criticRating":null,"genres":["Drama"],"overview":"From the battlefields of World War I to the rugged Highlands of 18th century Scotland, two fated couples must defy the forces that seek to tear them apart, unfolding in surprising and unforeseen ways in this romantic\u2026","poster":"assets/tv-outlander-blood-of-my-blood-2025-poster.jpg","backdrop":"assets/tv-outlander-blood-of-my-blood-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0691373Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207484,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.738,"tmdbVotes":124}},{"id":"tv-rivals-2024","title":"Rivals","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":7.44,"criticRating":null,"genres":["Drama"],"overview":"It is 1986 and Britain is booming. \"Rivals\" dives headfirst into the cutthroat world of TV where hair-dos are big and ambitions are even bigger. Deals are brokered in boardrooms, as well as bedrooms. Nobody can be\u2026","poster":"assets/tv-rivals-2024-poster.jpg","backdrop":"assets/tv-rivals-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0645815Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":208921,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":93}},{"id":"tv-rooster-2026","title":"Rooster","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.431,"criticRating":null,"genres":["Comedy"],"overview":"On a college campus, an author navigates a complicated relationship with his daughter.","poster":"assets/tv-rooster-2026-poster.jpg","backdrop":"assets/tv-rooster-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.063389Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":254953,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.423,"tmdbVotes":144}},{"id":"tv-sex-and-the-city-1998","title":"Sex and the City","year":1998,"type":"tv","runtimeSeconds":null,"seasonCount":6,"communityRating":7.365,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Based on the bestselling book by Candace Bushnell, Sex and the City tells the story of four best friends, all single and in their late thirties, as they pursue their careers and talk about their sex lives, all while\u2026","poster":"assets/tv-sex-and-the-city-1998-poster.jpg","backdrop":"assets/tv-sex-and-the-city-1998-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0679324Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":105,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":70,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.351,"tmdbVotes":2220}},{"id":"tv-skymed-2022","title":"SkyMed","year":2022,"type":"tv","runtimeSeconds":2640,"seasonCount":4,"communityRating":6.665,"criticRating":null,"genres":["Drama"],"overview":"Life, death and drama at 20,000 feet. The series weaves together intense character journeys and high-stakes medical rescues, as we follow the triumphs, heartbreaks and tribulations of budding nurses and pilots flying\u2026","poster":"assets/tv-skymed-2022-poster.jpg","backdrop":"assets/tv-skymed-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T19:44:29.2431354Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":133908,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.699,"tmdbVotes":98}},{"id":"tv-so-you-think-you-can-dance-2008","title":"So You Think You Can Dance","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":9.8,"criticRating":null,"genres":["Family","Reality"],"overview":"So You Think You Can Dance is a joint Belgian-Dutch dance competition show broadcast on RTL 5 and vtm television stations. The first season was broadcast only in the Netherlands, whereas starting with season 2, the\u2026","poster":"assets/tv-so-you-think-you-can-dance-2008-poster.jpg","backdrop":"assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-23T11:11:35.4124418Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":40231,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":9.8,"tmdbVotes":4}},{"id":"tv-the-white-lotus-2021","title":"The White Lotus","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.597,"criticRating":null,"genres":["Comedy","Drama","Mystery"],"overview":"Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026","poster":"assets/tv-the-white-lotus-2021-poster.jpg","backdrop":"assets/tv-the-white-lotus-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T02:45:52.8742674Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.599,"tmdbVotes":1604}},{"id":"tv-yellowjackets-2021","title":"Yellowjackets","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.431,"criticRating":null,"genres":["Drama","Mystery"],"overview":"This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026","poster":"assets/tv-yellowjackets-2021-poster.jpg","backdrop":"assets/tv-yellowjackets-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:31:47.4592756Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.431,"tmdbVotes":1070}}],"anime":[],"discover":[{"id":"movie-across-the-hall-2009","title":"Across the Hall","year":2009,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Mystery"],"overview":"Convinced that his fiancee is cheating on him, a man follows her to a hotel and calls his best friend to help him avoid a calamity.","poster":"assets/movie-across-the-hall-2009-poster.jpg","backdrop":"assets/movie-across-the-hall-2009-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":21469,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":39,"rtAudienceLabel":"Spilled","imdb":5.8,"imdbVotes":5130,"tmdb":6,"tmdbVotes":129}},{"id":"movie-addicted-2014","title":"Addicted","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.","poster":"assets/movie-addicted-2014-poster.jpg","backdrop":"assets/movie-addicted-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":235271,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":51,"rtAudienceLabel":"Spilled","imdb":5,"imdbVotes":13131,"tmdb":5.928,"tmdbVotes":466}},{"id":"movie-adulterers-2016","title":"Adulterers","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Drama"],"overview":"On a steamy New Orleans afternoon, Sam Deuprey decides to surprise his beautiful wife, Ashley, for their anniversary and comes home early. But Ashley is not alone in the bedroom. She and her lover, Damien are about\u2026","poster":"assets/movie-adulterers-2016-poster.jpg","backdrop":"assets/movie-adulterers-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":376004,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":37,"rtAudienceLabel":"Spilled","imdb":5.1,"imdbVotes":2144,"tmdb":6.2,"tmdbVotes":116}},{"id":"movie-alone-with-her-2006","title":"Alone With Her","year":2006,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Thriller"],"overview":"The harrowing story of a disturbed young man's attempts to win the affections of an unsuspecting young woman. When Doug first sees Amy, he instantly falls for her and begins to watch her every move, going so far as\u2026","poster":"assets/movie-alone-with-her-2006-poster.jpg","backdrop":"assets/movie-alone-with-her-2006-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":18206,"ratings":{"rtCritics":70,"rtCriticsLabel":"Fresh","rtAudience":46,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":4359,"tmdb":5.9,"tmdbVotes":94}},{"id":"movie-awake-2007","title":"Awake","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Crime","Mystery"],"overview":"While undergoing heart surgery, a man experiences a phenomenon called \u2018anesthetic awareness\u2019, which leaves him awake but paralyzed throughout the operation. As various obstacles present themselves, his wife must make\u2026","poster":"assets/movie-awake-2007-poster.jpg","backdrop":"assets/movie-awake-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13483,"ratings":{"rtCritics":23,"rtCriticsLabel":"Rotten","rtAudience":55,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":89389,"tmdb":6.586,"tmdbVotes":1649}},{"id":"movie-black-bear-2020","title":"Black Bear","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026","poster":"assets/movie-black-bear-2020-poster.jpg","backdrop":"assets/movie-black-bear-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":624788,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":18408,"tmdb":6.337,"tmdbVotes":315}},{"id":"movie-butterfly-on-a-wheel-2007","title":"Butterfly on a Wheel","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Thriller"],"overview":"A sociopathic kidnapper methodically pushes a desperate pair of parents to their absolute breaking point.","poster":"assets/movie-butterfly-on-a-wheel-2007-poster.jpg","backdrop":"assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13250,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":35409,"tmdb":6.457,"tmdbVotes":595}},{"id":"movie-careful-what-you-wish-for-2015","title":"Careful What You Wish For","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.","poster":"assets/movie-careful-what-you-wish-for-2015-poster.jpg","backdrop":"assets/movie-careful-what-you-wish-for-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":317744,"ratings":{"rtCritics":17,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":9452,"tmdb":6.129,"tmdbVotes":446}},{"id":"movie-crime-of-passion-1956","title":"Crime of Passion","year":1956,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Thriller"],"overview":"Kathy leaves the newspaper business to marry homicide detective Bill, but is frustrated by his lack of ambition and the banality of life in the suburbs. Her drive to advance Bill's career soon takes her down a\u2026","poster":"assets/movie-crime-of-passion-1956-poster.jpg","backdrop":"assets/movie-crime-of-passion-1956-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1920,"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":34,"rtAudienceLabel":"Spilled","imdb":6.4,"imdbVotes":3750,"tmdb":6.1,"tmdbVotes":57}},{"id":"movie-deep-water-2022","title":"Deep Water","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Thriller"],"overview":"A well-to-do husband who allows his wife to have affairs in order to avoid a divorce becomes a prime suspect in the disappearance of her lovers.","poster":"assets/movie-deep-water-2022-poster.jpg","backdrop":"assets/movie-deep-water-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":619979,"ratings":{"rtCritics":35,"rtCriticsLabel":"Rotten","rtAudience":25,"rtAudienceLabel":"Spilled","imdb":5.5,"imdbVotes":74098,"tmdb":5.834,"tmdbVotes":1755}},{"id":"movie-derailed-2005","title":"Derailed","year":2005,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"When two married business executives having an affair are blackmailed by a violent criminal, they are forced to turn the tables on him to save their families.","poster":"assets/movie-derailed-2005-poster.jpg","backdrop":"assets/movie-derailed-2005-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":8999,"ratings":{"rtCritics":20,"rtCriticsLabel":"Rotten","rtAudience":58,"rtAudienceLabel":"Spilled","imdb":6.6,"imdbVotes":79563,"tmdb":6.405,"tmdbVotes":1193}},{"id":"movie-die-my-love-2025","title":"Die My Love","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"After inheriting a remote Montana house, Jackson moves there from New York with his partner Grace, and the couple soon welcome a child. As Jackson becomes increasingly absent and rural isolation sets in, Grace\u2026","poster":"assets/movie-die-my-love-2025-poster.jpg","backdrop":"assets/movie-die-my-love-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1033148,"ratings":{"rtCritics":74,"rtCriticsLabel":"Certified Fresh","rtAudience":45,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":50664,"tmdb":6.1,"tmdbVotes":683}},{"id":"movie-dream-house-2011","title":"Dream House","year":2011,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Drama","Thriller"],"overview":"Publisher Will Atenton quits a lucrative job in New York to relocate his wife, Libby, and their daughters to a quaint town in New England. However, as they settle into their home the Atentons discover that a woman\u2026","poster":"assets/movie-dream-house-2011-poster.jpg","backdrop":"assets/movie-dream-house-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":69668,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":36,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":71386,"tmdb":6.212,"tmdbVotes":1480}},{"id":"movie-dream-lover-1994","title":"Dream Lover","year":1994,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Thriller"],"overview":"Not long after they cross paths at an art gallery, architect Ray Reardon and hypnotically sensual Lena are married with children. But as strange incidents occur, Ray begins to realize he may not really know the woman\u2026","poster":"assets/movie-dream-lover-1994-poster.jpg","backdrop":"assets/movie-dream-lover-1994-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":18190,"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":38,"rtAudienceLabel":"Spilled","imdb":6.2,"imdbVotes":7865,"tmdb":6.844,"tmdbVotes":167}},{"id":"movie-drop-2025","title":"Drop","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Thriller"],"overview":"Violet, a widowed mother on her first date in years, arrives at an upscale restaurant where she is relieved that her date, Henry, is more charming and handsome than she expected. But their chemistry begins to curdle\u2026","poster":"assets/movie-drop-2025-poster.jpg","backdrop":"assets/movie-drop-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1249213,"ratings":{"rtCritics":83,"rtCriticsLabel":"Certified Fresh","rtAudience":79,"rtAudienceLabel":"Upright","imdb":6.1,"imdbVotes":61018,"tmdb":6.275,"tmdbVotes":1023}},{"id":"movie-eastern-promises-2007","title":"Eastern Promises","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Crime","Mystery"],"overview":"A Russian teenager living in London dies during childbirth but leaves clues in her diary that could tie her child to a rape involving a violent Russian mob family.","poster":"assets/movie-eastern-promises-2007-poster.jpg","backdrop":"assets/movie-eastern-promises-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":2252,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":281015,"tmdb":7.349,"tmdbVotes":3900}},{"id":"movie-faces-1968","title":"Faces","year":1968,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"Middle-aged suburban husband Richard abruptly tells his wife, Maria, that he wants a divorce. As Richard takes up with a younger woman, Maria enjoys a night on the town with her friends and meets a younger man. As\u2026","poster":"assets/movie-faces-1968-poster.jpg","backdrop":"assets/movie-faces-1968-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":753,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":12950,"tmdb":7.093,"tmdbVotes":227}},{"id":"movie-fatale-2020","title":"Fatale","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"After a wild one-night stand, successful sports agent Derrick Tyler watches his perfect life slowly disappear when he discovers the sexy and mysterious woman he risked everything for is determined police detective\u2026","poster":"assets/movie-fatale-2020-poster.jpg","backdrop":"assets/movie-fatale-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":549514,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":87,"rtAudienceLabel":"Upright","imdb":5.5,"imdbVotes":16392,"tmdb":6,"tmdbVotes":401}},{"id":"tv-56-days-2026","title":"56 Days","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"When an unidentified body is found in a luxury apartment linked to Oliver Kennedy and his girlfriend Ciara Wyse, Detectives Lee Reardon and Karl Connolly reconstruct the couple's deadly romance across the past 56 days.","poster":"assets/tv-56-days-2026-poster.jpg","backdrop":"assets/tv-56-days-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":241372,"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.388,"tmdbVotes":245}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-absolutely-fabulous-1992","title":"Absolutely Fabulous","year":1992,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"Set in the world of fashion and PR, immature fun-loving mother Edina Monsoon and her best friend Patsy drive Eddie's sensible daughter, Saffron, up the wall with their constant drug abuse and outrageous selfishness.\u2026","poster":"assets/tv-absolutely-fabulous-1992-poster.jpg","backdrop":"assets/tv-absolutely-fabulous-1992-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":4493,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":198}},{"id":"tv-the-afterparty-2022","title":"The Afterparty","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Comedy","Crime"],"overview":"Nothing is as it seems in this hilarious murder mystery where each suspect's story is told through a different film genre.","poster":"assets/tv-the-afterparty-2022-poster.jpg","backdrop":"assets/tv-the-afterparty-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":106454,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.071,"tmdbVotes":245}},{"id":"tv-agatha-christie-s-marple-2004","title":"Agatha Christie's Marple","year":2004,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"The adventures of Miss Jane Marple, an elderly spinster living in the quiet little village of St Mary Mead. During her many visits to friends and relatives in other villages, Miss Marple often stumbles upon\u2026","poster":"assets/tv-agatha-christie-s-marple-2004-poster.jpg","backdrop":"assets/tv-agatha-christie-s-marple-2004-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":6127,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":152}},{"id":"tv-all-american-homecoming-2022","title":"All American: Homecoming","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"Follow the journeys of Simone Hicks, a tennis hopeful from Beverly Hills, and elite baseball player Damon Sims from Chicago, as they navigate life at the prestigious HBCU Bringston University.","poster":"assets/tv-all-american-homecoming-2022-poster.jpg","backdrop":"assets/tv-all-american-homecoming-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":153723,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":70}},{"id":"tv-and-just-like-that-2021","title":"And Just Like That\u2026","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"This new chapter of \u201cSex and the City\u201d follows Carrie, Miranda and Charlotte as they navigate the journey from the complicated reality of life and friendship in their 30s to the even more complicated reality of life\u2026","poster":"assets/tv-and-just-like-that-2021-poster.jpg","backdrop":"assets/tv-and-just-like-that-2021-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":116450,"ratings":{"rtCritics":52,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.211,"tmdbVotes":556}},{"id":"tv-anne-with-an-e-2017","title":"Anne with an E","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Family"],"overview":"A coming-of-age story about an outsider who, against all odds and numerous challenges, fights for love and acceptance and for her place in the world. The series centers on a young orphaned girl in the late 1890s,\u2026","poster":"assets/tv-anne-with-an-e-2017-poster.jpg","backdrop":"assets/tv-anne-with-an-e-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":70785,"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":5019}},{"id":"tv-awkward-2011","title":"Awkward.","year":2011,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"An irreverent look at the conflict, chaos and humor that defines teenage life through the eyes of 15-year-old Jenna Hamilton whose life begins to change when a simple accident becomes an epic misunderstanding and is\u2026","poster":"assets/tv-awkward-2011-poster.jpg","backdrop":"assets/tv-awkward-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":39859,"ratings":{"rtCritics":78,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":427}},{"id":"tv-the-baby-sitters-club-2020","title":"The Baby-Sitters Club","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Kids","Drama"],"overview":"In this contemporary take on the beloved book series, five best friends launch a baby-sitting business that's big on fun and adventure.","poster":"assets/tv-the-baby-sitters-club-2020-poster.jpg","backdrop":"assets/tv-the-baby-sitters-club-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":100412,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":506}},{"id":"tv-ballers-2015","title":"Ballers","year":2015,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Looking at the lives of former and current football players, the show follows former superstar Spencer Strasmore as he gets his life on track in retirement while mentoring other current and former players through the\u2026","poster":"assets/tv-ballers-2015-poster.jpg","backdrop":"assets/tv-ballers-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":62704,"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.809,"tmdbVotes":820}},{"id":"tv-baretta-1975","title":"Baretta","year":1975,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Mystery"],"overview":"Baretta is an American detective television series which ran on ABC from 1975 to 1978. The show was a milder version of a successful 1973\u201374 ABC series, Toma, starring Tony Musante as chameleon-like, real-life New\u2026","poster":"assets/tv-baretta-1975-poster.jpg","backdrop":"assets/tv-baretta-1975-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":3216,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":43}},{"id":"tv-based-on-a-true-story-2023","title":"Based on a True Story","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The lives of a realtor, a plumber and a former tennis star unexpectedly collide, exposing America\u2019s obsession with true crime, murder and the slow-close toilet seat.","poster":"assets/tv-based-on-a-true-story-2023-poster.jpg","backdrop":"assets/tv-based-on-a-true-story-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":197373,"ratings":{"rtCritics":79,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":160}},{"id":"tv-the-beast-in-me-2025","title":"The Beast in Me","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery"],"overview":"A famous author is pulled into a twisted mind game with her rich, powerful new neighbor \u2014 who might be a murderer.","poster":"assets/tv-the-beast-in-me-2025-poster.jpg","backdrop":"assets/tv-the-beast-in-me-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":250504,"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.072,"tmdbVotes":334}},{"id":"tv-big-city-greens-2018","title":"Big City Greens","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Family","Animation","Comedy"],"overview":"The offbeat adventures of 10-year-old Cricket Green, a mischievous and optimistic country boy who moves to the big city with his wildly out of place family \u2013 older sister Tilly, father Bill and Gramma Alice.","poster":"assets/tv-big-city-greens-2018-poster.jpg","backdrop":"assets/tv-big-city-greens-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":80587,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.555,"tmdbVotes":82}},{"id":"tv-the-bold-type-2017","title":"The Bold Type","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Comedy"],"overview":"A glimpse into the outrageous lives of Jane, Kat and Sutton, who are working at the nation's top women's magazine, Scarlet, while navigating their careers, identities and individual voices.","poster":"assets/tv-the-bold-type-2017-poster.jpg","backdrop":"assets/tv-the-bold-type-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":69630,"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":204}},{"id":"tv-brassic-2019","title":"Brassic","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama","Crime"],"overview":"A group of working-class friends finding unconventional ways to win at life in suburban northern England. These lads have dealt, scammed, bribed and conned their way through adolescence, but now, their dealing and\u2026","poster":"assets/tv-brassic-2019-poster.jpg","backdrop":"assets/tv-brassic-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":92621,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.336,"tmdbVotes":263}}]},{"key":"shared","movies":[{"id":"movie-american-gangster-2007","title":"American Gangster","year":2007,"type":"movie","runtimeSeconds":10546,"seasonCount":null,"communityRating":7.595,"criticRating":81,"genres":["Drama","Crime"],"overview":"Loosely based on the criminal career of Frank Lucas, a gangster from La Grange, North Carolina, who smuggled heroin into the United States on American service planes returning from the Vietnam War, before being\u2026","poster":"assets/movie-american-gangster-2007-poster.jpg","backdrop":"assets/movie-american-gangster-2007-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-08-23T10:22:36.0000000Z","lastPlayedAt":"2026-08-30T21:05:39.7952451Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4982,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":495656,"tmdb":7.599,"tmdbVotes":6350}},{"id":"movie-annie-hall-1977","title":"Annie Hall","year":1977,"type":"movie","runtimeSeconds":5604,"seasonCount":null,"communityRating":7.712,"criticRating":97,"genres":["Comedy","Drama","Romance"],"overview":"New York comedian Alvy Singer falls in love with the ditsy Annie Hall.","poster":"assets/movie-annie-hall-1977-poster.jpg","backdrop":"assets/movie-annie-hall-1977-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:23.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":703,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":293349,"tmdb":7.71,"tmdbVotes":4391}},{"id":"movie-apocalypse-now-1979","title":"Apocalypse Now","year":1979,"type":"movie","runtimeSeconds":8835,"seasonCount":null,"communityRating":8.261,"criticRating":91,"genres":["Drama","War"],"overview":"At the height of the Vietnam war, Captain Benjamin Willard is sent on a dangerous mission that, officially, \"does not exist, nor will it ever exist.\" His goal is to locate - and eliminate - a mysterious Green Beret\u2026","poster":"assets/movie-apocalypse-now-1979-poster.jpg","backdrop":"assets/movie-apocalypse-now-1979-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:25.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":28,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":94,"rtAudienceLabel":"Upright","imdb":8.4,"imdbVotes":781424,"tmdb":8.3,"tmdbVotes":9526}},{"id":"movie-arrival-2016","title":"Arrival","year":2016,"type":"movie","runtimeSeconds":6967,"seasonCount":null,"communityRating":7.636,"criticRating":94,"genres":["Drama","Science Fiction","Mystery"],"overview":"Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.","poster":"assets/movie-arrival-2016-poster.jpg","backdrop":"assets/movie-arrival-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-14T19:54:12.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":329865,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":883621,"tmdb":7.638,"tmdbVotes":20125}},{"id":"movie-avatar-aang-the-last-airbender-2026","title":"Avatar Aang: The Last Airbender","year":2026,"type":"movie","runtimeSeconds":5930,"seasonCount":null,"communityRating":9.147,"criticRating":90,"genres":["Animation","Adventure","Fantasy"],"overview":"Avatar Aang, the world's last Airbender, learns of an ancient power that could save his culture from extinction. With the help of his friends, he embarks on a global quest to find it before it falls into the wrong\u2026","poster":"assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg","backdrop":"assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-25T09:53:09.0000000Z","lastPlayedAt":"2026-08-02T19:22:33.6155383Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":980431,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":99,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":30792,"tmdb":9,"tmdbVotes":1417}},{"id":"movie-bohemian-rhapsody-2018","title":"Bohemian Rhapsody","year":2018,"type":"movie","runtimeSeconds":8077,"seasonCount":null,"communityRating":7.95,"criticRating":60,"genres":["Music","Drama"],"overview":"Singer Freddie Mercury, guitarist Brian May, drummer Roger Taylor and bass guitarist John Deacon take the music world by storm when they form the rock 'n' roll band Queen in 1970. Hit songs become instant classics.\u2026","poster":"assets/movie-bohemian-rhapsody-2018-poster.jpg","backdrop":"assets/movie-bohemian-rhapsody-2018-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":424694,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":655894,"tmdb":7.947,"tmdbVotes":18196}},{"id":"movie-the-boy-and-the-heron-2023","title":"The Boy and the Heron","year":2023,"type":"movie","runtimeSeconds":7437,"seasonCount":null,"communityRating":7.4,"criticRating":96,"genres":["Animation","Fantasy","Drama"],"overview":"While the Second World War rages, the teenage Mahito, haunted by his mother's tragic death, is relocated from Tokyo to the serene rural home of his new stepmother Natsuko, a woman who bears a striking resemblance to\u2026","poster":"assets/movie-the-boy-and-the-heron-2023-poster.jpg","backdrop":"assets/movie-the-boy-and-the-heron-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":508883,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":88,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":152169,"tmdb":7.387,"tmdbVotes":2893}},{"id":"movie-braveheart-1995","title":"Braveheart","year":1995,"type":"movie","runtimeSeconds":10665,"seasonCount":null,"communityRating":7.945,"criticRating":74,"genres":["Drama","History","War"],"overview":"Enraged at the slaughter of Murron, his new bride and childhood love, Scottish warrior William Wallace slays a platoon of the local English lord's soldiers. This leads the village to revolt and, eventually, the\u2026","poster":"assets/movie-braveheart-1995-poster.jpg","backdrop":"assets/movie-braveheart-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T21:24:28.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":197,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":74,"rtCriticsLabel":"Certified Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":8.3,"imdbVotes":1174990,"tmdb":7.947,"tmdbVotes":11584}},{"id":"movie-the-breakfast-club-1985","title":"The Breakfast Club","year":1985,"type":"movie","runtimeSeconds":5833,"seasonCount":null,"communityRating":7.7,"criticRating":87,"genres":["Comedy","Drama"],"overview":"Five high school students from different walks of life endure a Saturday detention under a power-hungry principal. The disparate group includes rebel John, princess Claire, outcast Allison, brainy Brian and Andrew,\u2026","poster":"assets/movie-the-breakfast-club-1985-poster.jpg","backdrop":"assets/movie-the-breakfast-club-1985-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2108,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":478743,"tmdb":7.713,"tmdbVotes":8815}},{"id":"movie-castle-in-the-sky-1986","title":"Castle in the Sky","year":1986,"type":"movie","runtimeSeconds":7474,"seasonCount":null,"communityRating":8,"criticRating":97,"genres":["Adventure","Fantasy","Animation"],"overview":"A young boy and a girl with a magic crystal must race against pirates and foreign agents in a search for a legendary floating castle.","poster":"assets/movie-castle-in-the-sky-1986-poster.jpg","backdrop":"assets/movie-castle-in-the-sky-1986-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:09.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":10515,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":201545,"tmdb":7.972,"tmdbVotes":4868}},{"id":"movie-citizen-kane-1941","title":"Citizen Kane","year":1941,"type":"movie","runtimeSeconds":7173,"seasonCount":null,"communityRating":7.966,"criticRating":99,"genres":["Mystery","Drama"],"overview":"Newspaper magnate Charles Foster Kane is taken from his mother as a boy and made the ward of a rich industrialist. As a result, every well-meaning, tyrannical or self-destructive move he makes for the rest of his\u2026","poster":"assets/movie-citizen-kane-1941-poster.jpg","backdrop":"assets/movie-citizen-kane-1941-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:16.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":15,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Certified Fresh","rtAudience":98,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":498321,"tmdb":7.962,"tmdbVotes":6324}},{"id":"movie-conclave-2024","title":"Conclave","year":2024,"type":"movie","runtimeSeconds":7230,"seasonCount":null,"communityRating":7.19,"criticRating":93,"genres":["Drama","Thriller"],"overview":"After the unexpected death of the Pope, Cardinal Lawrence is tasked with managing the covert and ancient ritual of electing a new one. Sequestered in the Vatican with the Catholic Church\u2019s most powerful leaders until\u2026","poster":"assets/movie-conclave-2024-poster.jpg","backdrop":"assets/movie-conclave-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:41.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":974576,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":290498,"tmdb":7.19,"tmdbVotes":3744}},{"id":"movie-the-dry-2021","title":"The Dry","year":2021,"type":"movie","runtimeSeconds":7037,"seasonCount":null,"communityRating":6.779,"criticRating":null,"genres":["Crime","Thriller","Mystery"],"overview":"Aaron Falk returns to his drought-stricken hometown to attend a tragic funeral. But his return opens a decades-old wound - the unsolved death of a teenage girl.","poster":"assets/movie-the-dry-2021-poster.jpg","backdrop":"assets/movie-the-dry-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":567797,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":41935,"tmdb":6.8,"tmdbVotes":892}},{"id":"movie-easy-a-2010","title":"Easy A","year":2010,"type":"movie","runtimeSeconds":5545,"seasonCount":null,"communityRating":6.8,"criticRating":85,"genres":["Comedy"],"overview":"Olive, an average high school student, sees her below-the-radar existence turn around overnight once she decides to use the school's gossip grapevine to advance her social standing. Now her classmates are turning\u2026","poster":"assets/movie-easy-a-2010-poster.jpg","backdrop":"assets/movie-easy-a-2010-backdrop.jpg","played":true,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-17T19:59:04.0000000Z","lastPlayedAt":"2026-10-17T20:02:51.1083337Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":37735,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":85,"rtCriticsLabel":"Certified Fresh","rtAudience":77,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":448780,"tmdb":6.8,"tmdbVotes":7619}},{"id":"movie-el-camino-a-breaking-bad-movie-2019","title":"El Camino: A Breaking Bad Movie","year":2019,"type":"movie","runtimeSeconds":7362,"seasonCount":null,"communityRating":7.055,"criticRating":92,"genres":["Crime","Drama","Thriller"],"overview":"In the wake of his dramatic escape from captivity, Jesse Pinkman must come to terms with his past in order to forge some kind of future.","poster":"assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg","backdrop":"assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-21T23:31:11.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":559969,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Certified Fresh","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":354488,"tmdb":7.056,"tmdbVotes":5950}},{"id":"movie-emancipation-2022","title":"Emancipation","year":2022,"type":"movie","runtimeSeconds":7949,"seasonCount":null,"communityRating":7.7,"criticRating":45,"genres":["Drama","War","History"],"overview":"Inspired by the gripping true story of a man who would do anything for his family\u2014and for freedom. When Peter, an enslaved man, risks his life to escape and return to his family, he embarks on a perilous journey of\u2026","poster":"assets/movie-emancipation-2022-poster.jpg","backdrop":"assets/movie-emancipation-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T20:37:34.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":715931,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":45,"rtCriticsLabel":"Rotten","rtAudience":58,"rtAudienceLabel":"Spilled","imdb":6.3,"imdbVotes":32079,"tmdb":7.744,"tmdbVotes":1319}},{"id":"movie-fighting-with-my-family-2019","title":"Fighting with My Family","year":2019,"type":"movie","runtimeSeconds":6509,"seasonCount":null,"communityRating":6.84,"criticRating":93,"genres":["Comedy"],"overview":"Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training\u2026","poster":"assets/movie-fighting-with-my-family-2019-poster.jpg","backdrop":"assets/movie-fighting-with-my-family-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T22:10:59.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":445629,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":94576,"tmdb":6.837,"tmdbVotes":1952}},{"id":"movie-force-of-nature-the-dry-2-2024","title":"Force of Nature: The Dry 2","year":2024,"type":"movie","runtimeSeconds":6753,"seasonCount":null,"communityRating":6.3,"criticRating":66,"genres":["Mystery","Thriller"],"overview":"When five women take part in a corporate hiking retreat and only four come out on the other side, Federal Agents Aaron Falk and Carmen Cooper head deep into the Victorian mountain ranges to investigate in the hopes\u2026","poster":"assets/movie-force-of-nature-the-dry-2-2024-poster.jpg","backdrop":"assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:49.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":832262,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":43,"rtAudienceLabel":"Spilled","imdb":5.9,"imdbVotes":10343,"tmdb":6.269,"tmdbVotes":305}},{"id":"movie-free-state-of-jones-2016","title":"Free State of Jones","year":2016,"type":"movie","runtimeSeconds":8376,"seasonCount":null,"communityRating":6.79,"criticRating":48,"genres":["Action","Drama","History"],"overview":"In 1863, Mississippi farmer Newt Knight serves as a medic for the Confederate Army. Opposed to slavery, Knight would rather help the wounded than fight the Union. After his nephew dies in battle, Newt returns home to\u2026","poster":"assets/movie-free-state-of-jones-2016-poster.jpg","backdrop":"assets/movie-free-state-of-jones-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-11T17:56:53.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":316152,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":48,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":68931,"tmdb":6.788,"tmdbVotes":1730}},{"id":"movie-from-up-on-poppy-hill-2011","title":"From Up on Poppy Hill","year":2011,"type":"movie","runtimeSeconds":5482,"seasonCount":null,"communityRating":7.5,"criticRating":87,"genres":["Animation","Drama"],"overview":"Yokohama, 1963. Japan is picking itself up from the devastation of World War II and preparing to host the 1964 Olympics\u2014and the mood is one of both optimism and conflict as the young generation struggles to throw off\u2026","poster":"assets/movie-from-up-on-poppy-hill-2011-poster.jpg","backdrop":"assets/movie-from-up-on-poppy-hill-2011-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-07T14:41:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":83389,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":80,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":60769,"tmdb":7.509,"tmdbVotes":2026}},{"id":"movie-ghost-in-the-shell-1995","title":"Ghost in the Shell","year":1995,"type":"movie","runtimeSeconds":4959,"seasonCount":null,"communityRating":7.929,"criticRating":95,"genres":["Action","Animation","Science Fiction"],"overview":"In the year 2029, the barriers of our world have been broken down by the net and by cybernetics, but this brings new vulnerability to humans in the form of brain-hacking. When a highly-wanted hacker known as 'The\u2026","poster":"assets/movie-ghost-in-the-shell-1995-poster.jpg","backdrop":"assets/movie-ghost-in-the-shell-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:57.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":9323,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":178061,"tmdb":7.928,"tmdbVotes":4130}},{"id":"movie-ghost-in-the-shell-2-innocence-2004","title":"Ghost in the Shell 2: Innocence","year":2004,"type":"movie","runtimeSeconds":5950,"seasonCount":null,"communityRating":7.352,"criticRating":65,"genres":["Animation","Drama","Science Fiction"],"overview":"Cyborg detective Batou is assigned to investigate a series of murders committed by gynoids\u2014doll-like cyborgs, which all malfunctioned, killed, then self-destructed afterwards. The brains of the gynoids initialize in\u2026","poster":"assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg","backdrop":"assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:38.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":12140,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":80,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":43280,"tmdb":7.356,"tmdbVotes":1049}},{"id":"movie-the-godfather-1972","title":"The Godfather","year":1972,"type":"movie","runtimeSeconds":10603,"seasonCount":null,"communityRating":8.687,"criticRating":97,"genres":["Drama","Crime"],"overview":"Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son,\u2026","poster":"assets/movie-the-godfather-1972-poster.jpg","backdrop":"assets/movie-the-godfather-1972-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:12.0000000Z","lastPlayedAt":"2026-09-18T19:24:44.9855867Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":238,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":98,"rtAudienceLabel":"Upright","imdb":9.2,"imdbVotes":2262024,"tmdb":8.688,"tmdbVotes":23907}},{"id":"movie-the-godfather-part-ii-1974","title":"The Godfather Part II","year":1974,"type":"movie","runtimeSeconds":12031,"seasonCount":null,"communityRating":8.573,"criticRating":96,"genres":["Drama","Crime"],"overview":"In the continuing saga of the Corleone crime family, a young Vito Corleone grows up in Sicily and in 1910s New York. In the 1950s, Michael Corleone attempts to expand the family business into Las Vegas, Hollywood and\u2026","poster":"assets/movie-the-godfather-part-ii-1974-poster.jpg","backdrop":"assets/movie-the-godfather-part-ii-1974-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:18.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":240,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":97,"rtAudienceLabel":"Upright","imdb":9,"imdbVotes":1521308,"tmdb":8.571,"tmdbVotes":14559}},{"id":"movie-the-godfather-part-iii-1990","title":"The Godfather Part III","year":1990,"type":"movie","runtimeSeconds":9476,"seasonCount":null,"communityRating":7.416,"criticRating":67,"genres":["Crime","Drama","Thriller"],"overview":"In the midst of trying to legitimize his business dealings in 1979 New York and Italy, aging mafia don, Michael Corleone seeks forgiveness for his sins while taking a young protege under his wing.","poster":"assets/movie-the-godfather-part-iii-1990-poster.jpg","backdrop":"assets/movie-the-godfather-part-iii-1990-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:29.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":242,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":68,"rtCriticsLabel":"Fresh","rtAudience":68,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":462448,"tmdb":7.413,"tmdbVotes":7159}},{"id":"movie-gosford-park-2001","title":"Gosford Park","year":2001,"type":"movie","runtimeSeconds":8247,"seasonCount":null,"communityRating":6.815,"criticRating":87,"genres":["Drama","Mystery","Thriller"],"overview":"In 1930s England, a group of pretentious rich and famous gather together for a weekend of relaxation at a hunting resort. But when a murder occurs, each one of these interesting characters becomes a suspect.","poster":"assets/movie-gosford-park-2001-poster.jpg","backdrop":"assets/movie-gosford-park-2001-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:49.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":5279,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":78,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":103883,"tmdb":6.822,"tmdbVotes":1317}},{"id":"movie-grave-of-the-fireflies-1988","title":"Grave of the Fireflies","year":1988,"type":"movie","runtimeSeconds":5313,"seasonCount":null,"communityRating":8.45,"criticRating":100,"genres":["Animation","Drama","War"],"overview":"In the final months of World War II, 14-year-old Seita and his sister Setsuko are orphaned when their mother is killed during an air raid in Kobe, Japan. After a falling out with their aunt, they move into an\u2026","poster":"assets/movie-grave-of-the-fireflies-1988-poster.jpg","backdrop":"assets/movie-grave-of-the-fireflies-1988-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:14.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":12477,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Certified Fresh","rtAudience":95,"rtAudienceLabel":"Upright","imdb":8.5,"imdbVotes":399802,"tmdb":8.45,"tmdbVotes":6938}},{"id":"movie-the-great-gatsby-2013","title":"The Great Gatsby","year":2013,"type":"movie","runtimeSeconds":8525,"seasonCount":null,"communityRating":7.346,"criticRating":49,"genres":["Drama","Romance"],"overview":"Nick Carraway arrives in New York in 1922, an era of loose morals, glittering jazz and bootleg kings. Chasing his own American Dream, Nick encounters the mysterious millionaire Gatsby and his bewitching cousin Daisy.\u2026","poster":"assets/movie-the-great-gatsby-2013-poster.jpg","backdrop":"assets/movie-the-great-gatsby-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:50.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":64682,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":48,"rtCriticsLabel":"Rotten","rtAudience":67,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":651882,"tmdb":7.346,"tmdbVotes":13290}},{"id":"movie-the-hours-2002","title":"The Hours","year":2002,"type":"movie","runtimeSeconds":6887,"seasonCount":null,"communityRating":7.25,"criticRating":79,"genres":["Drama"],"overview":"The story of three women searching for more potent, meaningful lives. Each is alive at a different time and place, all are linked by their yearnings and their fears. Their stories intertwine, and finally come\u2026","poster":"assets/movie-the-hours-2002-poster.jpg","backdrop":"assets/movie-the-hours-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:27.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":590,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":148367,"tmdb":7.249,"tmdbVotes":1931}},{"id":"movie-howl-s-moving-castle-2004","title":"Howl's Moving Castle","year":2004,"type":"movie","runtimeSeconds":7149,"seasonCount":null,"communityRating":8.4,"criticRating":88,"genres":["Fantasy","Animation","Adventure"],"overview":"Sophie, a young milliner, is turned into an elderly woman by a witch who enters her shop and curses her. She encounters a wizard named Howl and gets caught up in his resistance to fighting for the king.","poster":"assets/movie-howl-s-moving-castle-2004-poster.jpg","backdrop":"assets/movie-howl-s-moving-castle-2004-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:47.0000000Z","lastPlayedAt":"2026-05-21T18:46:08.0000000Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4935,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":524520,"tmdb":8.391,"tmdbVotes":11642}},{"id":"movie-in-the-heights-2021","title":"In the Heights","year":2021,"type":"movie","runtimeSeconds":8579,"seasonCount":null,"communityRating":7.111,"criticRating":null,"genres":["Drama","Romance"],"overview":"The story of Usnavi, a bodega owner who has mixed feelings about closing his store and retiring to the Dominican Republic or staying in Washington Heights.","poster":"assets/movie-in-the-heights-2021-poster.jpg","backdrop":"assets/movie-in-the-heights-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:11.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":467909,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":94,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":56513,"tmdb":7.104,"tmdbVotes":826}},{"id":"movie-jennifer-s-body-2009","title":"Jennifer's Body","year":2009,"type":"movie","runtimeSeconds":6450,"seasonCount":null,"communityRating":6.012,"criticRating":47,"genres":["Horror","Comedy"],"overview":"Jennifer, a gorgeous, seductive cheerleader takes evil to a whole new level after she's possessed by a sinister demon. Now it's up to her best friend to stop Jennifer's reign of terror before it's too late.","poster":"assets/movie-jennifer-s-body-2009-poster.jpg","backdrop":"assets/movie-jennifer-s-body-2009-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-30T03:37:04.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":19994,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":47,"rtCriticsLabel":"Rotten","rtAudience":36,"rtAudienceLabel":"Spilled","imdb":5.6,"imdbVotes":179710,"tmdb":6.013,"tmdbVotes":4481}},{"id":"movie-kiki-s-delivery-service-1989","title":"Kiki's Delivery Service","year":1989,"type":"movie","runtimeSeconds":6183,"seasonCount":null,"communityRating":7.839,"criticRating":null,"genres":["Animation","Family","Fantasy"],"overview":"A young witch, on her mandatory year of independent life, finds fitting into a new community difficult while she supports herself by running an air courier service.","poster":"assets/movie-kiki-s-delivery-service-1989-poster.jpg","backdrop":"assets/movie-kiki-s-delivery-service-1989-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":16859,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":98,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":192477,"tmdb":7.839,"tmdbVotes":4811}},{"id":"movie-the-last-of-the-mohicans-1992","title":"The Last of the Mohicans","year":1992,"type":"movie","runtimeSeconds":6874,"seasonCount":null,"communityRating":7.388,"criticRating":88,"genres":["History","War","Drama"],"overview":"In war-torn colonial America, in the midst of a bloody battle between British, the French and Native American allies, the aristocratic daughter of a British Colonel and her party are captured by a group of Huron\u2026","poster":"assets/movie-the-last-of-the-mohicans-1992-poster.jpg","backdrop":"assets/movie-the-last-of-the-mohicans-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T20:59:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":9361,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Certified Fresh","rtAudience":88,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":210791,"tmdb":7.39,"tmdbVotes":3593}},{"id":"movie-the-last-samurai-2003","title":"The Last Samurai","year":2003,"type":"movie","runtimeSeconds":9251,"seasonCount":null,"communityRating":7.61,"criticRating":65,"genres":["Drama","Action","War"],"overview":"Nathan Algren is an American hired to instruct the Japanese army in the ways of modern warfare, which finds him learning to respect the samurai and the honorable principles that rule them. Pressed to destroy the\u2026","poster":"assets/movie-the-last-samurai-2003-poster.jpg","backdrop":"assets/movie-the-last-samurai-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T05:25:10.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":616,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":512982,"tmdb":7.614,"tmdbVotes":7887}},{"id":"movie-lemony-snicket-s-a-series-of-unfortunate-events-2004","title":"Lemony Snicket's A Series of Unfortunate Events","year":2004,"type":"movie","runtimeSeconds":6479,"seasonCount":null,"communityRating":6.8,"criticRating":72,"genres":["Adventure","Comedy","Family"],"overview":"Three wealthy children's parents are killed in a fire. When they are sent to a distant relative, they find out that he is plotting to kill them and seize their fortune.","poster":"assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg","backdrop":"assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:09.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":11774,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":233636,"tmdb":6.82,"tmdbVotes":5447}},{"id":"movie-the-lighthouse-2019","title":"The Lighthouse","year":2019,"type":"movie","runtimeSeconds":6549,"seasonCount":null,"communityRating":7.463,"criticRating":90,"genres":["Drama","Fantasy","Thriller"],"overview":"Two lighthouse keepers try to maintain their sanity while living on a remote and mysterious New England island in the 1890s.","poster":"assets/movie-the-lighthouse-2019-poster.jpg","backdrop":"assets/movie-the-lighthouse-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:01.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":503919,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":312344,"tmdb":7.46,"tmdbVotes":5895}},{"id":"movie-locke-2014","title":"Locke","year":2014,"type":"movie","runtimeSeconds":5090,"seasonCount":null,"communityRating":6.888,"criticRating":91,"genres":["Drama","Thriller"],"overview":"Ivan Locke has worked hard to craft a good life for himself. Tonight, that life will collapse around him. On the eve of the biggest challenge of his career, Ivan receives a phone call that sets in motion a series of\u2026","poster":"assets/movie-locke-2014-poster.jpg","backdrop":"assets/movie-locke-2014-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:38:48.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":210479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":171265,"tmdb":6.889,"tmdbVotes":3088}},{"id":"movie-the-matrix-1999","title":"The Matrix","year":1999,"type":"movie","runtimeSeconds":8179,"seasonCount":null,"communityRating":8.259,"criticRating":83,"genres":["Action","Science Fiction"],"overview":"Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.","poster":"assets/movie-the-matrix-1999-poster.jpg","backdrop":"assets/movie-the-matrix-1999-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:18:12.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":603,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":83,"rtCriticsLabel":"Certified Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":8.7,"imdbVotes":2289808,"tmdb":8.262,"tmdbVotes":29153}},{"id":"movie-the-matrix-reloaded-2003","title":"The Matrix Reloaded","year":2003,"type":"movie","runtimeSeconds":8298,"seasonCount":null,"communityRating":7.085,"criticRating":74,"genres":["Adventure","Action","Thriller"],"overview":"The Resistance builds in numbers as humans are freed from the Matrix and brought to the city of Zion. Neo discovers his superpowers, including the ability to see the code inside the Matrix. With machine sentinels\u2026","poster":"assets/movie-the-matrix-reloaded-2003-poster.jpg","backdrop":"assets/movie-the-matrix-reloaded-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":604,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":73,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":678013,"tmdb":7.087,"tmdbVotes":12504}},{"id":"movie-the-matrix-resurrections-2021","title":"The Matrix Resurrections","year":2021,"type":"movie","runtimeSeconds":8875,"seasonCount":null,"communityRating":6.303,"criticRating":63,"genres":["Science Fiction","Action","Adventure"],"overview":"Plagued by strange memories, Neo's life takes an unexpected turn when he finds himself back inside the Matrix.","poster":"assets/movie-the-matrix-resurrections-2021-poster.jpg","backdrop":"assets/movie-the-matrix-resurrections-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":624860,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":307780,"tmdb":6.294,"tmdbVotes":6842}},{"id":"movie-the-matrix-revolutions-2003","title":"The Matrix Revolutions","year":2003,"type":"movie","runtimeSeconds":7756,"seasonCount":null,"communityRating":6.758,"criticRating":33,"genres":["Adventure","Action","Thriller"],"overview":"The human city of Zion defends itself against the massive invasion of the machines as Neo fights to end the war at another front while also opposing the rogue Agent Smith.","poster":"assets/movie-the-matrix-revolutions-2003-poster.jpg","backdrop":"assets/movie-the-matrix-revolutions-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:47.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":605,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":34,"rtCriticsLabel":"Rotten","rtAudience":60,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":581553,"tmdb":6.762,"tmdbVotes":11292}},{"id":"movie-mercy-2026","title":"Mercy","year":2026,"type":"movie","runtimeSeconds":5968,"seasonCount":null,"communityRating":7.077,"criticRating":null,"genres":["Science Fiction","Thriller","Crime"],"overview":"In the near future, a detective stands on trial accused of murdering his wife. He has ninety minutes to prove his innocence to the advanced AI Judge he once championed, before it determines his fate.","poster":"assets/movie-mercy-2026-poster.jpg","backdrop":"assets/movie-mercy-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:40.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1236153,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":24,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6.2,"imdbVotes":83878,"tmdb":7.054,"tmdbVotes":1812}},{"id":"movie-normal-2026","title":"Normal","year":2026,"type":"movie","runtimeSeconds":5514,"seasonCount":null,"communityRating":7.267,"criticRating":77,"genres":["Action","Crime","Thriller"],"overview":"An unassuming substitute sheriff with a troubled past responds to a bank robbery in a small, sleepy town and unknowingly uncovers something far more explosive.","poster":"assets/movie-normal-2026-poster.jpg","backdrop":"assets/movie-normal-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-08-21T08:33:18.0000000Z","lastPlayedAt":"2026-09-24T23:07:52.1161581Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1242332,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":78,"rtCriticsLabel":"Certified Fresh","rtAudience":75,"rtAudienceLabel":"Upright","imdb":6.3,"imdbVotes":37501,"tmdb":7.241,"tmdbVotes":786}},{"id":"movie-outlaw-king-2018","title":"Outlaw King","year":2018,"type":"movie","runtimeSeconds":7297,"seasonCount":null,"communityRating":6.829,"criticRating":63,"genres":["Action","History","War"],"overview":"Forced into exile by the English after being crowned King of Scotland, legendary warrior Robert the Bruce fights to reclaim the throne.","poster":"assets/movie-outlaw-king-2018-poster.jpg","backdrop":"assets/movie-outlaw-king-2018-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:32.0000000Z","lastPlayedAt":"2026-07-26T22:35:35.0000000Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":454227,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":69,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":89876,"tmdb":6.844,"tmdbVotes":1780}},{"id":"movie-panic-room-2002","title":"Panic Room","year":2002,"type":"movie","runtimeSeconds":6717,"seasonCount":null,"communityRating":6.794,"criticRating":76,"genres":["Crime","Drama","Thriller"],"overview":"Trapped in their New York brownstone's panic room, a hidden chamber built as a sanctuary in the event of break-ins, newly divorced Meg Altman and her young daughter Sarah play a deadly game of cat-and-mouse with\u2026","poster":"assets/movie-panic-room-2002-poster.jpg","backdrop":"assets/movie-panic-room-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-19T20:50:01.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4547,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":76,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":317468,"tmdb":6.796,"tmdbVotes":5653}},{"id":"movie-the-patriot-2000","title":"The Patriot","year":2000,"type":"movie","runtimeSeconds":9887,"seasonCount":null,"communityRating":7.214,"criticRating":61,"genres":["Drama","History","War"],"overview":"After proving himself on the field of battle in the French and Indian War, Benjamin Martin wants nothing more to do with such things, preferring the simple life of a farmer. But when his son Gabriel enlists in the\u2026","poster":"assets/movie-the-patriot-2000-poster.jpg","backdrop":"assets/movie-the-patriot-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T20:50:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2024,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":61,"rtCriticsLabel":"Fresh","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":317639,"tmdb":7.215,"tmdbVotes":4535}},{"id":"movie-peaky-blinders-the-immortal-man-2026","title":"Peaky Blinders: The Immortal Man","year":2026,"type":"movie","runtimeSeconds":6844,"seasonCount":null,"communityRating":7.187,"criticRating":90,"genres":["Crime","Drama"],"overview":"After his estranged son gets embroiled in a Nazi plot, self-exiled gangster Tommy Shelby must return to Birmingham to save his family \u2014 and his nation.","poster":"assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg","backdrop":"assets/movie-peaky-blinders-the-immortal-man-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:01.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":875828,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":92130,"tmdb":7.189,"tmdbVotes":1304}},{"id":"movie-ponyo-2008","title":"Ponyo","year":2008,"type":"movie","runtimeSeconds":6530,"seasonCount":null,"communityRating":7.763,"criticRating":91,"genres":["Animation","Fantasy","Family"],"overview":"When Sosuke, a young boy who lives on a clifftop overlooking the sea, rescues a stranded goldfish named Ponyo, he discovers more than he bargained for. Ponyo is a curious, energetic young creature who yearns to be\u2026","poster":"assets/movie-ponyo-2008-poster.jpg","backdrop":"assets/movie-ponyo-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:53.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":12429,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":190294,"tmdb":7.759,"tmdbVotes":5050}},{"id":"movie-porco-rosso-1992","title":"Porco Rosso","year":1992,"type":"movie","runtimeSeconds":5600,"seasonCount":null,"communityRating":7.794,"criticRating":96,"genres":["Animation","Adventure","Fantasy"],"overview":"In Italy in the 1930s, sky pirates in biplanes terrorize wealthy cruise ships as they sail the Adriatic Sea. The only pilot brave enough to stop the scourge is the mysterious Porco Rosso, a former World War I flying\u2026","poster":"assets/movie-porco-rosso-1992-poster.jpg","backdrop":"assets/movie-porco-rosso-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":11621,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":119396,"tmdb":7.789,"tmdbVotes":3879}},{"id":"movie-princess-mononoke-1997","title":"Princess Mononoke","year":1997,"type":"movie","runtimeSeconds":8003,"seasonCount":null,"communityRating":8.321,"criticRating":93,"genres":["Adventure","Fantasy","Animation"],"overview":"Ashitaka, a prince of the disappearing Emishi people, is cursed by a demonized boar god and must journey to the west to find a cure. Along the way, he encounters San, a young human woman fighting to protect the\u2026","poster":"assets/movie-princess-mononoke-1997-poster.jpg","backdrop":"assets/movie-princess-mononoke-1997-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:28.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":128,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":99,"rtAudienceLabel":"Upright","imdb":8.3,"imdbVotes":493735,"tmdb":8.321,"tmdbVotes":9438}},{"id":"movie-prisoners-2013","title":"Prisoners","year":2013,"type":"movie","runtimeSeconds":9192,"seasonCount":null,"communityRating":8.106,"criticRating":81,"genres":["Drama","Thriller","Crime"],"overview":"After his six-year-old daughter is abducted, a desperate father takes justice into his own hands when he loses faith in the detective leading the investigation, setting both men on a collision course as the search\u2026","poster":"assets/movie-prisoners-2013-poster.jpg","backdrop":"assets/movie-prisoners-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:33.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":146233,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":82,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":963062,"tmdb":8.105,"tmdbVotes":13722}},{"id":"movie-racing-stripes-2005","title":"Racing Stripes","year":2005,"type":"movie","runtimeSeconds":6117,"seasonCount":null,"communityRating":5.945,"criticRating":35,"genres":["Comedy","Family"],"overview":"Shattered illusions are hard to repair -- especially for a good-hearted zebra named Stripes who's spent his life on a Kentucky farm amidst the sorely mistaken notion that he's a debonair thoroughbred. Once he faces\u2026","poster":"assets/movie-racing-stripes-2005-poster.jpg","backdrop":"assets/movie-racing-stripes-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:02.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":6439,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":35,"rtCriticsLabel":"Rotten","rtAudience":42,"rtAudienceLabel":"Spilled","imdb":5.3,"imdbVotes":19569,"tmdb":5.962,"tmdbVotes":705}},{"id":"movie-requiem-for-a-dream-2000","title":"Requiem for a Dream","year":2000,"type":"movie","runtimeSeconds":6086,"seasonCount":null,"communityRating":8.005,"criticRating":80,"genres":["Crime","Drama"],"overview":"The drug-induced utopias of four Coney Island residents are shattered when their addictions run deep.","poster":"assets/movie-requiem-for-a-dream-2000-poster.jpg","backdrop":"assets/movie-requiem-for-a-dream-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":641,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":8.3,"imdbVotes":989090,"tmdb":8,"tmdbVotes":11454}},{"id":"movie-rogue-one-a-star-wars-story-2016","title":"Rogue One: A Star Wars Story","year":2016,"type":"movie","runtimeSeconds":8038,"seasonCount":null,"communityRating":7.51,"criticRating":84,"genres":["Action","Adventure","Science Fiction"],"overview":"A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.","poster":"assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg","backdrop":"assets/movie-rogue-one-a-star-wars-story-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":330459,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":768365,"tmdb":7.512,"tmdbVotes":16937}},{"id":"movie-secondhand-lions-2003","title":"Secondhand Lions","year":2003,"type":"movie","runtimeSeconds":6547,"seasonCount":null,"communityRating":7.237,"criticRating":60,"genres":["Family","Comedy","Drama"],"overview":"The comedic adventures of an introverted boy left on the doorstep of a pair of reluctant, eccentric great-uncles, whose exotic remembrances stir the boy's spirit and re-ignite the men's lives.","poster":"assets/movie-secondhand-lions-2003-poster.jpg","backdrop":"assets/movie-secondhand-lions-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-12T07:16:13.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":13156,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":59,"rtCriticsLabel":"Rotten","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":66911,"tmdb":7.242,"tmdbVotes":872}},{"id":"movie-the-secret-life-of-pets-2-2019","title":"The Secret Life of Pets 2","year":2019,"type":"movie","runtimeSeconds":5164,"seasonCount":null,"communityRating":6.95,"criticRating":60,"genres":["Animation","Comedy","Family"],"overview":"Max the terrier must cope with some major life changes when his owner gets married and has a baby. When the family takes a trip to the countryside, nervous Max has numerous run-ins with canine-intolerant cows,\u2026","poster":"assets/movie-the-secret-life-of-pets-2-2019-poster.jpg","backdrop":"assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-05T07:00:53.0000000Z","lastPlayedAt":"2026-09-17T18:07:00.7024231Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":412117,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":6.4,"imdbVotes":80464,"tmdb":6.948,"tmdbVotes":3450}},{"id":"movie-self-reliance-2024","title":"Self Reliance","year":2024,"type":"movie","runtimeSeconds":5461,"seasonCount":null,"communityRating":5.888,"criticRating":73,"genres":["Comedy","Thriller"],"overview":"When a man is offered a million dollars to play a game in which hunters try to kill him, he thinks he has found the perfect loophole: they can only attack when he\u2019s alone. His only problem is that none of his friends\u2026","poster":"assets/movie-self-reliance-2024-poster.jpg","backdrop":"assets/movie-self-reliance-2024-backdrop.jpg","played":false,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:00.0000000Z","lastPlayedAt":"2026-10-02T19:59:16.9286868Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1072876,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":73,"rtCriticsLabel":"Certified Fresh","rtAudience":56,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":23771,"tmdb":5.866,"tmdbVotes":346}},{"id":"movie-serpico-1973","title":"Serpico","year":1973,"type":"movie","runtimeSeconds":7805,"seasonCount":null,"communityRating":7.5,"criticRating":93,"genres":["Crime","Drama","History"],"overview":"New York cop Frank Serpico blows the whistle on the rampant corruption in the force only to have his comrades turn against him.","poster":"assets/movie-serpico-1973-poster.jpg","backdrop":"assets/movie-serpico-1973-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T21:45:02.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":9040,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":88,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":149750,"tmdb":7.525,"tmdbVotes":2323}},{"id":"movie-shaft-2000","title":"Shaft","year":2000,"type":"movie","runtimeSeconds":5960,"seasonCount":null,"communityRating":6.026,"criticRating":67,"genres":["Action","Adventure","Crime"],"overview":"New York police detective John Shaft arrests Walter Wade Jr. for a racially motivated slaying. But the only eyewitness disappears, and Wade jumps bail for Switzerland. Two years later Wade returns to face trial,\u2026","poster":"assets/movie-shaft-2000-poster.jpg","backdrop":"assets/movie-shaft-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T21:30:40.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":68,"rtCriticsLabel":"Fresh","rtAudience":40,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":86257,"tmdb":6.024,"tmdbVotes":1531}},{"id":"movie-a-silent-voice-the-movie-2016","title":"A Silent Voice: The Movie","year":2016,"type":"movie","runtimeSeconds":7804,"seasonCount":null,"communityRating":8.398,"criticRating":null,"genres":["Animation","Drama","Romance"],"overview":"Shouya Ishida starts bullying the new girl in class, Shouko Nishimiya, because she is deaf. But as the teasing continues, the rest of the class starts to turn on Shouya for his lack of compassion. When they leave\u2026","poster":"assets/movie-a-silent-voice-the-movie-2016-poster.jpg","backdrop":"assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:39:46.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":378064,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":94,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":151926,"tmdb":8.396,"tmdbVotes":4764}},{"id":"movie-sinbad-legend-of-the-seven-seas-2003","title":"Sinbad: Legend of the Seven Seas","year":2003,"type":"movie","runtimeSeconds":5143,"seasonCount":null,"communityRating":7.067,"criticRating":44,"genres":["Action","Adventure","Animation"],"overview":"The sailor of legend is framed by the goddess Eris for the theft of the Book of Peace, and must travel to her realm at the end of the world to retrieve it and save the life of his childhood friend Prince Proteus.","poster":"assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg","backdrop":"assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:25.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":14411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":57,"rtAudienceLabel":"Spilled","imdb":6.8,"imdbVotes":65298,"tmdb":7.071,"tmdbVotes":2323}},{"id":"movie-sleeping-with-other-people-2015","title":"Sleeping with Other People","year":2015,"type":"movie","runtimeSeconds":6076,"seasonCount":null,"communityRating":6.274,"criticRating":64,"genres":["Comedy","Romance"],"overview":"Can two serial cheaters get a second chance at love? After a one-night stand in college, New Yorkers Lainey and Jake meet by chance twelve years later and discover they each have the same problem: because of their\u2026","poster":"assets/movie-sleeping-with-other-people-2015-poster.jpg","backdrop":"assets/movie-sleeping-with-other-people-2015-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:42.0000000Z","lastPlayedAt":"2026-07-17T17:50:44.0000000Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":288036,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":56,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":52275,"tmdb":6.275,"tmdbVotes":1020}},{"id":"movie-snowpiercer-2013","title":"Snowpiercer","year":2013,"type":"movie","runtimeSeconds":7573,"seasonCount":null,"communityRating":6.909,"criticRating":94,"genres":["Action","Science Fiction","Drama"],"overview":"In a future where a failed global-warming experiment kills off most life on the planet, a class system evolves aboard the Snowpiercer; a train that travels around the globe via a perpetual-motion engine.","poster":"assets/movie-snowpiercer-2013-poster.jpg","backdrop":"assets/movie-snowpiercer-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:07.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":110415,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":426976,"tmdb":6.9,"tmdbVotes":10807}},{"id":"movie-spider-man-brand-new-day-2026","title":"Spider-Man: Brand New Day","year":2026,"type":"movie","runtimeSeconds":8701,"seasonCount":null,"communityRating":8.374,"criticRating":90,"genres":["Science Fiction","Action","Adventure"],"overview":"Fighting crime full-time as Spider-Man in a world that doesn't remember him\u2014and the pressure of seeing his old friends move on without him\u2014sparks a change in Peter Parker he may not have the power to control. But\u2026","poster":"assets/movie-spider-man-brand-new-day-2026-poster.jpg","backdrop":"assets/movie-spider-man-brand-new-day-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:14:27.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":969681,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":97,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":366886,"tmdb":8.5,"tmdbVotes":6537}},{"id":"movie-spider-man-far-from-home-2019","title":"Spider-Man: Far From Home","year":2019,"type":"movie","runtimeSeconds":7762,"seasonCount":null,"communityRating":7.389,"criticRating":91,"genres":["Action","Adventure","Science Fiction"],"overview":"Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and\u2026","poster":"assets/movie-spider-man-far-from-home-2019-poster.jpg","backdrop":"assets/movie-spider-man-far-from-home-2019-backdrop.jpg","played":true,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:25:47.0000000Z","lastPlayedAt":"2026-10-10T17:50:35.6193119Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":429617,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":95,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":682887,"tmdb":7.387,"tmdbVotes":17847}},{"id":"movie-spider-man-homecoming-2017","title":"Spider-Man: Homecoming","year":2017,"type":"movie","runtimeSeconds":8008,"seasonCount":null,"communityRating":7.333,"criticRating":92,"genres":["Action","Adventure","Science Fiction"],"overview":"Following the events of Captain America: Civil War, Peter Parker, with the help of his mentor Tony Stark, tries to balance his life as an ordinary high school student in Queens, New York City, with fighting crime as\u2026","poster":"assets/movie-spider-man-homecoming-2017-poster.jpg","backdrop":"assets/movie-spider-man-homecoming-2017-backdrop.jpg","played":true,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:33:13.0000000Z","lastPlayedAt":"2026-10-10T20:31:23.4444213Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":315635,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":840974,"tmdb":7.332,"tmdbVotes":24183}},{"id":"movie-spider-man-no-way-home-2021","title":"Spider-Man: No Way Home","year":2021,"type":"movie","runtimeSeconds":8890,"seasonCount":null,"communityRating":7.947,"criticRating":93,"genres":["Action","Adventure","Science Fiction"],"overview":"Peter Parker is unmasked and no longer able to separate his normal life from the high-stakes of being a super-hero. When he asks for help from Doctor Strange the stakes become even more dangerous, forcing him to\u2026","poster":"assets/movie-spider-man-no-way-home-2021-poster.jpg","backdrop":"assets/movie-spider-man-no-way-home-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:21:05.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":634649,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":97,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":1082660,"tmdb":7.947,"tmdbVotes":23505}},{"id":"movie-spirited-away-2001","title":"Spirited Away","year":2001,"type":"movie","runtimeSeconds":7472,"seasonCount":null,"communityRating":8.535,"criticRating":96,"genres":["Animation","Family","Fantasy"],"overview":"A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family.","poster":"assets/movie-spirited-away-2001-poster.jpg","backdrop":"assets/movie-spirited-away-2001-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:42.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":129,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":99,"rtAudienceLabel":"Upright","imdb":8.6,"imdbVotes":987658,"tmdb":8.536,"tmdbVotes":19176}},{"id":"movie-the-substance-2024","title":"The Substance","year":2024,"type":"movie","runtimeSeconds":8531,"seasonCount":null,"communityRating":7.135,"criticRating":89,"genres":["Horror","Science Fiction"],"overview":"A fading celebrity decides to use a black market drug, a cell-replicating substance that temporarily creates a younger, better version of herself.","poster":"assets/movie-the-substance-2024-poster.jpg","backdrop":"assets/movie-the-substance-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":933260,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":76,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":466542,"tmdb":7.13,"tmdbVotes":6746}},{"id":"movie-the-tale-of-the-princess-kaguya-2013","title":"The Tale of The Princess Kaguya","year":2013,"type":"movie","runtimeSeconds":8218,"seasonCount":null,"communityRating":8.1,"criticRating":100,"genres":["Animation","Drama","Fantasy"],"overview":"Found inside a shining stalk of bamboo by an old bamboo cutter and his wife, a tiny girl grows rapidly into an exquisite young lady. The mysterious young princess enthrals all who encounter her. But, ultimately, she\u2026","poster":"assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg","backdrop":"assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-07T13:40:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":149871,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":63574,"tmdb":8.083,"tmdbVotes":2079}},{"id":"movie-taxi-driver-1976","title":"Taxi Driver","year":1976,"type":"movie","runtimeSeconds":6831,"seasonCount":null,"communityRating":8.121,"criticRating":89,"genres":["Crime","Drama"],"overview":"Suffering from insomnia, disturbed loner Travis Bickle takes a job as a New York City cabbie, haunting the streets nightly, growing increasingly detached from reality as he dreams of cleaning up the filthy city.","poster":"assets/movie-taxi-driver-1976-poster.jpg","backdrop":"assets/movie-taxi-driver-1976-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:04.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":103,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":1046700,"tmdb":8.116,"tmdbVotes":13973}},{"id":"movie-triangle-of-sadness-2022","title":"Triangle of Sadness","year":2022,"type":"movie","runtimeSeconds":8833,"seasonCount":null,"communityRating":7.005,"criticRating":71,"genres":["Comedy","Drama"],"overview":"A celebrity model couple are invited on a luxury cruise for the uber-rich, helmed by an unhinged, alcoholic captain. What first appears Instagrammable ends catastrophically, leaving the survivors stranded on a desert\u2026","poster":"assets/movie-triangle-of-sadness-2022-poster.jpg","backdrop":"assets/movie-triangle-of-sadness-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":497828,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":80,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":221069,"tmdb":7,"tmdbVotes":3104}},{"id":"movie-tsotsi-2005","title":"Tsotsi","year":2005,"type":"movie","runtimeSeconds":5663,"seasonCount":null,"communityRating":6.937,"criticRating":82,"genres":["Crime","Drama"],"overview":"A young South African boy from the Johannesburg ghetto named Tsotsi, meaning Gangster, leaves home as a child to get away from his helpless parents. Now a teenage thug, Tsotsi finds a baby in the back seat of a car\u2026","poster":"assets/movie-tsotsi-2005-poster.jpg","backdrop":"assets/movie-tsotsi-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:58.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":868,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":82,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":32526,"tmdb":6.941,"tmdbVotes":372}},{"id":"movie-unknown-number-the-high-school-catfish-2025","title":"Unknown Number: The High School Catfish","year":2025,"type":"movie","runtimeSeconds":5691,"seasonCount":null,"communityRating":6.746,"criticRating":85,"genres":["Documentary"],"overview":"Vulgar, taunting texts blow up the phones of a teen and her boyfriend. Who's sending them \u2014 and why? This twisty documentary reveals the shocking answer.","poster":"assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg","backdrop":"assets/movie-unknown-number-the-high-school-catfish-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:32:10.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1517772,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":69,"rtAudienceLabel":"Upright","imdb":6.7,"imdbVotes":23039,"tmdb":6.737,"tmdbVotes":257}},{"id":"movie-the-uprising-2026","title":"The Uprising","year":2026,"type":"movie","runtimeSeconds":7684,"seasonCount":null,"communityRating":7.846,"criticRating":55,"genres":["Action","Drama","History"],"overview":"As war and plague devastate 14th-century England, a humble peasant ignites a rebellion, uniting an army of commoners to defy the King\u2019s might in a desperate fight for justice and survival.","poster":"assets/movie-the-uprising-2026-poster.jpg","backdrop":"assets/movie-the-uprising-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-02T16:37:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":977942,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":65,"rtAudienceLabel":"Upright","imdb":6.2,"imdbVotes":6622,"tmdb":7.8,"tmdbVotes":404}},{"id":"movie-vice-2018","title":"Vice","year":2018,"type":"movie","runtimeSeconds":7948,"seasonCount":null,"communityRating":7.013,"criticRating":64,"genres":["Drama","Comedy","History"],"overview":"George W. Bush picks Dick Cheney, the CEO of Halliburton Co., to be his Republican running mate in the 2000 presidential election. No stranger to politics, Cheney's impressive r\u00e9sum\u00e9 includes stints as White House\u2026","poster":"assets/movie-vice-2018-poster.jpg","backdrop":"assets/movie-vice-2018-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:30.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":429197,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":61,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":177299,"tmdb":7.014,"tmdbVotes":3723}},{"id":"movie-warriors-of-the-wind-1984","title":"Warriors of the Wind","year":1984,"type":"movie","runtimeSeconds":7022,"seasonCount":null,"communityRating":7.941,"criticRating":87,"genres":["Adventure","Animation","Fantasy"],"overview":"After a global war, the seaside kingdom known as the Valley of the Wind remains one of the last strongholds on Earth untouched by a poisonous jungle and the powerful insects that guard it. Led by the courageous\u2026","poster":"assets/movie-warriors-of-the-wind-1984-poster.jpg","backdrop":"assets/movie-warriors-of-the-wind-1984-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":81,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":200154,"tmdb":7.94,"tmdbVotes":4260}},{"id":"movie-we-bury-the-dead-2026","title":"We Bury the Dead","year":2026,"type":"movie","runtimeSeconds":5693,"seasonCount":null,"communityRating":6.153,"criticRating":87,"genres":["Horror","Thriller"],"overview":"After a catastrophic military disaster reanimates the dead, Ava searches for her missing husband amidst an increasingly hostile world.","poster":"assets/movie-we-bury-the-dead-2026-poster.jpg","backdrop":"assets/movie-we-bury-the-dead-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-08-26T05:36:00.4272328Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1198984,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":45,"rtAudienceLabel":"Spilled","imdb":5.6,"imdbVotes":20693,"tmdb":6.137,"tmdbVotes":433}},{"id":"movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022","title":"White Hot: The Rise & Fall of Abercrombie & Fitch","year":2022,"type":"movie","runtimeSeconds":5187,"seasonCount":null,"communityRating":5.663,"criticRating":66,"genres":["Documentary"],"overview":"All the cool kids were wearing it. This documentary explores A&F's pop culture reign in the late '90s and early 2000s and how it thrived on exclusion.","poster":"assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg","backdrop":"assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:34.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":953371,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":29,"rtAudienceLabel":"Spilled","imdb":5.8,"imdbVotes":5655,"tmdb":5.7,"tmdbVotes":102}},{"id":"movie-the-wild-robot-2024","title":"The Wild Robot","year":2024,"type":"movie","runtimeSeconds":6101,"seasonCount":null,"communityRating":8.308,"criticRating":97,"genres":["Family","Animation","Science Fiction"],"overview":"After a shipwreck, an intelligent robot called Roz is stranded on an uninhabited island. To survive the harsh environment, Roz bonds with the island's animals and cares for an orphaned baby goose.","poster":"assets/movie-the-wild-robot-2024-poster.jpg","backdrop":"assets/movie-the-wild-robot-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:49.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1184918,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":98,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":241134,"tmdb":8.309,"tmdbVotes":6819}},{"id":"movie-the-wind-rises-2013","title":"The Wind Rises","year":2013,"type":"movie","runtimeSeconds":8031,"seasonCount":null,"communityRating":7.784,"criticRating":88,"genres":["Animation","Drama","History"],"overview":"A lifelong love of flight inspires Japanese aviation engineer Jiro Horikoshi, whose storied career includes the creation of the A-6M World War II fighter plane.","poster":"assets/movie-the-wind-rises-2013-poster.jpg","backdrop":"assets/movie-the-wind-rises-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:38.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":149870,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":115756,"tmdb":7.783,"tmdbVotes":3536}},{"id":"movie-the-witcher-nightmare-of-the-wolf-2021","title":"The Witcher: Nightmare of the Wolf","year":2021,"type":"movie","runtimeSeconds":5015,"seasonCount":null,"communityRating":7.422,"criticRating":100,"genres":["Animation","Action","Fantasy"],"overview":"Escaping from poverty to become a witcher, Vesemir slays monsters for coin and glory, but when a new menace rises, he must face the demons of his past.","poster":"assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg","backdrop":"assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T04:42:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":666243,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":55759,"tmdb":7.407,"tmdbVotes":1159}},{"id":"movie-the-witcher-sirens-of-the-deep-2025","title":"The Witcher: Sirens of the Deep","year":2025,"type":"movie","runtimeSeconds":5517,"seasonCount":null,"communityRating":6.8,"criticRating":60,"genres":["Animation","Fantasy","Action"],"overview":"When human sailors are attacked by mysterious creatures of the deep, only one person can stop the war between land and sea: the Witcher, Geralt of Rivia.","poster":"assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg","backdrop":"assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T05:03:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1203329,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":37,"rtAudienceLabel":"Spilled","imdb":6.1,"imdbVotes":11799,"tmdb":6.741,"tmdbVotes":303}}],"shows":[{"id":"tv-adults-2025","title":"Adults","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.495,"criticRating":null,"genres":["Comedy"],"overview":"A group of twenty-somethings in New York trying to be good people, despite being neither \"good\" nor \"people\" yet.","poster":"assets/tv-adults-2025-poster.jpg","backdrop":"assets/tv-adults-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-23T21:01:41.1814765Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":260458,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":102}},{"id":"tv-american-crime-story-2016","title":"American Crime Story","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.573,"criticRating":null,"genres":["Crime","Drama"],"overview":"An anthology series centered around some of history's most famous criminal investigations.","poster":"assets/tv-american-crime-story-2016-poster.jpg","backdrop":"assets/tv-american-crime-story-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0490488Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":64513,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.585,"tmdbVotes":1094}},{"id":"tv-american-primeval-2025","title":"American Primeval","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.691,"criticRating":null,"genres":["Western","Drama","Action & Adventure"],"overview":"A mother and son fleeing from their past form a found family while confronting a harsh landscape of freedom and cruelty in the American West.","poster":"assets/tv-american-primeval-2025-poster.jpg","backdrop":"assets/tv-american-primeval-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0573221Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":215866,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":71,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.741,"tmdbVotes":538}},{"id":"tv-arrested-development-2003","title":"Arrested Development","year":2003,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":7.925,"criticRating":null,"genres":["Comedy"],"overview":"The story of a wealthy family that lost everything, and the one son who had no choice but to keep them all together.","poster":"assets/tv-arrested-development-2003-poster.jpg","backdrop":"assets/tv-arrested-development-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0513048Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4589,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.906,"tmdbVotes":1793}},{"id":"tv-avatar-the-last-airbender-2005","title":"Avatar: The Last Airbender","year":2005,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.78,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a war-torn world of elemental magic, a young boy reawakens to undertake a dangerous mystic quest to fulfill his destiny as the Avatar, and bring peace to the world.","poster":"assets/tv-avatar-the-last-airbender-2005-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0504536Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":246,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.787,"tmdbVotes":5215}},{"id":"tv-b-b-vol-liefde-2021","title":"B&B Vol Liefde","year":2021,"type":"tv","runtimeSeconds":3720,"seasonCount":1,"communityRating":7.2,"criticRating":null,"genres":["Reality"],"overview":"B&B Vol Liefde is a heartwarming adventure, full of romance, excitement, and unforgettable moments. Adventurous Dutch B&B owners abroad throw their doors wide open and let their hearts speak, determined to find that\u2026","poster":"assets/tv-b-b-vol-liefde-2021-poster.jpg","backdrop":"assets/tv-b-b-vol-liefde-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-31T22:30:11.129657Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":124428,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":10}},{"id":"tv-the-bear-2022","title":"The Bear","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.139,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Carmy, a young fine-dining chef, comes home to Chicago to run his family sandwich shop. As he fights to transform the shop and himself, he works alongside a rough-around-the-edges crew that ultimately reveal\u2026","poster":"assets/tv-the-bear-2022-poster.jpg","backdrop":"assets/tv-the-bear-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0467371Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":136315,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.158,"tmdbVotes":2025}},{"id":"tv-blue-planet-ii-2017","title":"Blue Planet II","year":2017,"type":"tv","runtimeSeconds":3600,"seasonCount":1,"communityRating":8.64,"criticRating":null,"genres":["Documentary","Family"],"overview":"There is nowhere more powerful and unforgiving yet more beautiful and compelling than the ocean. Join us and explore the greatest yet least known parts of our planet.","poster":"assets/tv-blue-planet-ii-2017-poster.jpg","backdrop":"assets/tv-blue-planet-ii-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0634849Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":74313,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":380}},{"id":"tv-breeders-2020","title":"Breeders","year":2020,"type":"tv","runtimeSeconds":1500,"seasonCount":4,"communityRating":6.8,"criticRating":null,"genres":["Comedy"],"overview":"Exposing the parental-paradox that it is possible, in the very same moment, to love your child to the horizon of the universe, while being apoplectically angry enough to want to send them there.","poster":"assets/tv-breeders-2020-poster.jpg","backdrop":"assets/tv-breeders-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T18:54:21.8885435Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":93290,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":104}},{"id":"tv-chernobyl-2019","title":"Chernobyl","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.718,"criticRating":null,"genres":["Drama"],"overview":"The true story of one of the worst man-made catastrophes in history: the catastrophic nuclear accident at Chernobyl. A tale of the brave men and women who sacrificed to save Europe from unimaginable disaster.","poster":"assets/tv-chernobyl-2019-poster.jpg","backdrop":"assets/tv-chernobyl-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0469903Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":87108,"artwork":{"posterKind":"poster","backdropKind":"center_crop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.731,"tmdbVotes":8496}},{"id":"tv-community-2009","title":"Community","year":2009,"type":"tv","runtimeSeconds":null,"seasonCount":6,"communityRating":7.997,"criticRating":null,"genres":["Comedy"],"overview":"Follow the lives of a group of students at what is possibly the world\u2019s worst community college in the fictional locale of Greendale, Colorado.","poster":"assets/tv-community-2009-poster.jpg","backdrop":"assets/tv-community-2009-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0531416Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":18347,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.994,"tmdbVotes":2226}},{"id":"tv-dead-end-job-2026","title":"Dead-End Job","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.7,"criticRating":null,"genres":["Mystery"],"overview":"Drowning in debt, a young man takes lucrative part-time jobs from a mysterious agency \u2014 only to find his hustle for more money turning nightmarish.","poster":"assets/tv-dead-end-job-2026-poster.jpg","backdrop":"assets/tv-dead-end-job-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T18:58:01.8352857Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":289517,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":38}},{"id":"tv-dopesick-2021","title":"Dopesick","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.015,"criticRating":null,"genres":["Crime","Drama"],"overview":"The story of how one company triggered the worst drug epidemic in American history. Look into the epicenter of America's struggle with opioid addiction, from a distressed Virginia mining community, to the hallways of\u2026","poster":"assets/tv-dopesick-2021-poster.jpg","backdrop":"assets/tv-dopesick-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0580168Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":110695,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.035,"tmdbVotes":565}},{"id":"tv-dune-prophecy-2024","title":"Dune: Prophecy","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.213,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama","Action & Adventure"],"overview":"Ten thousand years before the ascension of Paul Atreides, sisters Valya and Tula Harkonnen establish the fabled sect and female order that would become known as the Bene Gesserit to control the future of humankind.","poster":"assets/tv-dune-prophecy-2024-poster.jpg","backdrop":"assets/tv-dune-prophecy-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T18:23:01.2908568Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":90228,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.213,"tmdbVotes":677}},{"id":"tv-east-of-eden-2026","title":"East of Eden","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.931,"criticRating":null,"genres":["Drama","Crime"],"overview":"In the early 1900s, a fiercely independent woman drives a rift between two brothers, igniting a saga that unfolds across two generations.","poster":"assets/tv-east-of-eden-2026-poster.jpg","backdrop":"assets/tv-east-of-eden-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-23T18:07:11.3965096Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":258165,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.982,"tmdbVotes":139}},{"id":"tv-fallout-2024","title":"Fallout","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.124,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"The story of haves and have-nots in a world in which there's almost nothing left to have. 200 years after the apocalypse, the gentle denizens of luxury fallout shelters are forced to return to the irradiated\u2026","poster":"assets/tv-fallout-2024-poster.jpg","backdrop":"assets/tv-fallout-2024-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0669262Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":106379,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.106,"tmdbVotes":3233}},{"id":"tv-fawlty-towers-1975","title":"Fawlty Towers","year":1975,"type":"tv","runtimeSeconds":1800,"seasonCount":2,"communityRating":8.295,"criticRating":null,"genres":["Comedy"],"overview":"Owner Basil Fawlty, his wife Sybil, a chambermaid Polly, and Spanish waiter Manuel attempt to run their hotel amidst farcical situations and an array of demanding guests.","poster":"assets/tv-fawlty-towers-1975-poster.jpg","backdrop":"assets/tv-fawlty-towers-1975-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T23:26:39.7658344Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2207,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.296,"tmdbVotes":867}},{"id":"tv-the-final-problem-2026","title":"The Final Problem","year":2026,"type":"tv","runtimeSeconds":3420,"seasonCount":1,"communityRating":7.041,"criticRating":null,"genres":["Crime","Mystery"],"overview":"In the spring of 1959, a retired actor, who previously played Sherlock Holmes, must investigate a murder at a small hotel on a tiny island near Mallorca.","poster":"assets/tv-the-final-problem-2026-poster.jpg","backdrop":"assets/tv-the-final-problem-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T19:16:12.4822791Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":290720,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.041,"tmdbVotes":49}},{"id":"tv-the-four-seasons-2025","title":"The Four Seasons","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.879,"criticRating":null,"genres":["Comedy"],"overview":"The decades-long friendship between three married couples is tested when one divorces, complicating their tradition of quarterly weekend getaways.","poster":"assets/tv-the-four-seasons-2025-poster.jpg","backdrop":"assets/tv-the-four-seasons-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T18:36:42.5326384Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":243316,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.892,"tmdbVotes":204}},{"id":"tv-the-get-down-2016","title":"The Get Down","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.3,"criticRating":null,"genres":["Drama"],"overview":"In 1977 New York City, the talented and soulful youth of the South Bronx chase dreams and breakneck beats to transform music history.","poster":"assets/tv-the-get-down-2016-poster.jpg","backdrop":"assets/tv-the-get-down-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0497737Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":65345,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":196}},{"id":"tv-how-to-change-your-mind-2022","title":"How to Change Your Mind","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.76,"criticRating":null,"genres":["Documentary","Mystery"],"overview":"Author Michael Pollan leads the way in this docuseries exploring the history and uses of psychedelics, including LSD, psilocybin, MDMA and mescaline.","poster":"assets/tv-how-to-change-your-mind-2022-poster.jpg","backdrop":"assets/tv-how-to-change-your-mind-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0601399Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":204304,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":53}},{"id":"tv-interrogation-2020","title":"Interrogation","year":2020,"type":"tv","runtimeSeconds":2700,"seasonCount":1,"communityRating":6.788,"criticRating":null,"genres":["Drama","Crime"],"overview":"Based on a true story that spanned more than 30 years in which a young man was charged and convicted of brutally murdering his mother. Each episode is structured around an interrogation taken directly from the real\u2026","poster":"assets/tv-interrogation-2020-poster.jpg","backdrop":"assets/tv-interrogation-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T05:52:06.1068207Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":96488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.9,"tmdbVotes":54}},{"id":"tv-it-s-always-sunny-in-philadelphia-2005","title":"It's Always Sunny in Philadelphia","year":2005,"type":"tv","runtimeSeconds":null,"seasonCount":18,"communityRating":8.2,"criticRating":null,"genres":["Comedy"],"overview":"Four egocentric friends run a neighborhood Irish pub in Philadelphia and try to find their way through the adult world of work and relationships. Unfortunately, their warped views and precarious judgments often lead\u2026","poster":"assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg","backdrop":"assets/tv-it-s-always-sunny-in-philadelphia-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0485774Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2710,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.253,"tmdbVotes":1457}},{"id":"tv-a-knight-of-the-seven-kingdoms-2026","title":"A Knight of the Seven Kingdoms","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.431,"criticRating":null,"genres":["Drama","Sci-Fi & Fantasy","Action & Adventure"],"overview":"A century before the events of Game of Thrones, two unlikely heroes wandered Westeros: a young, naive but courageous knight, Ser Duncan the Tall, and his diminutive squire, Egg. Set in an age when the Targaryen line\u2026","poster":"assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg","backdrop":"assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0493344Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":224372,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":1266}},{"id":"tv-landman-2024","title":"Landman","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.194,"criticRating":null,"genres":["Drama"],"overview":"Set in the proverbial boomtowns of West-Texas and a modern-day tale of fortune-seeking in the world of oil rigs, the series is an upstairs/downstairs story of roughnecks and wildcat billionaires that are fueling a\u2026","poster":"assets/tv-landman-2024-poster.jpg","backdrop":"assets/tv-landman-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T21:38:18.0973875Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":157741,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.209,"tmdbVotes":668}},{"id":"tv-the-last-of-us-2023","title":"The Last of Us","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.419,"criticRating":null,"genres":["Drama"],"overview":"Twenty years after modern civilization has been destroyed, Joel, a hardened survivor, is hired to smuggle Ellie, a 14-year-old girl, out of an oppressive quarantine zone. What starts as a small job soon becomes a\u2026","poster":"assets/tv-the-last-of-us-2023-poster.jpg","backdrop":"assets/tv-the-last-of-us-2023-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0640955Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":100088,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":7485}},{"id":"tv-last-samurai-standing-2025","title":"Last Samurai Standing","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.805,"criticRating":null,"genres":["Action & Adventure","Drama"],"overview":"In the early Meiji era, Shujiro, once known as an undefeated samurai, decides to participate in a deadly survival game to save his family and villagers.","poster":"assets/tv-last-samurai-standing-2025-poster.jpg","backdrop":"assets/tv-last-samurai-standing-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0622526Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":252193,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.812,"tmdbVotes":271}},{"id":"tv-lord-of-the-flies-2026","title":"Lord of the Flies","year":2026,"type":"tv","runtimeSeconds":3480,"seasonCount":1,"communityRating":7.571,"criticRating":null,"genres":["Drama","Action & Adventure"],"overview":"After a plane crash, a group of schoolboys find themselves stranded on a tropical island without adults. Ralph is elected leader and, with the help of the intelligent Piggy, strives to maintain order and\u2026","poster":"assets/tv-lord-of-the-flies-2026-poster.jpg","backdrop":"assets/tv-lord-of-the-flies-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0645648Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":270572,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.58,"tmdbVotes":138}},{"id":"tv-the-lord-of-the-rings-the-rings-of-power-2022","title":"The Lord of the Rings: The Rings of Power","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":7.233,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"Beginning in a time of relative peace, we follow an ensemble cast of characters as they confront the re-emergence of evil to Middle-earth. From the darkest depths of the Misty Mountains, to the majestic forests of\u2026","poster":"assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg","backdrop":"assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0618904Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":84773,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.211,"tmdbVotes":4003}},{"id":"tv-love-death-robots-2019","title":"Love, Death & Robots","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.21,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy"],"overview":"Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.","poster":"assets/tv-love-death-robots-2019-poster.jpg","backdrop":"assets/tv-love-death-robots-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0460964Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86831,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.203,"tmdbVotes":4238}},{"id":"tv-maniac-2018","title":"Maniac","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.338,"criticRating":null,"genres":["Comedy","Drama","Sci-Fi & Fantasy"],"overview":"Two strangers are drawn to a mysterious pharmaceutical trial that will, they're assured, with no complications or side-effects whatsoever, solve all of their problems, permanently. Things do not go as planned.","poster":"assets/tv-maniac-2018-poster.jpg","backdrop":"assets/tv-maniac-2018-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T19:42:30.955548Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":73411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.323,"tmdbVotes":1073}},{"id":"tv-mussolini-son-of-the-century-2024","title":"Mussolini: Son of the Century","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.135,"criticRating":null,"genres":["Drama","War & Politics"],"overview":"The rise of Benito Mussolini and the birth of fascism in Italy, chronicling a country\u2019s surrender to dictatorship and the relentless ascent of a man who rose from his ashes time and again.","poster":"assets/tv-mussolini-son-of-the-century-2024-poster.jpg","backdrop":"assets/tv-mussolini-son-of-the-century-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0586588Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":212333,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.1,"tmdbVotes":179}},{"id":"tv-the-night-manager-2016","title":"The Night Manager","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":7.74,"criticRating":null,"genres":["Drama","Mystery","Crime"],"overview":"Former British soldier Jonathan Pine navigates the shadowy recesses of Whitehall and Washington where an unholy alliance operates between the intelligence community and the secret arms trade. To infiltrate the inner\u2026","poster":"assets/tv-the-night-manager-2016-poster.jpg","backdrop":"assets/tv-the-night-manager-2016-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0663672Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":61859,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":1422}},{"id":"tv-one-piece-2023","title":"ONE PIECE","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.118,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy"],"overview":"With his straw hat and ragtag crew, young pirate Monkey D. Luffy goes on an epic voyage for treasure.","poster":"assets/tv-one-piece-2023-poster.jpg","backdrop":"assets/tv-one-piece-2023-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0439378Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111110,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.104,"tmdbVotes":1988}},{"id":"tv-only-murders-in-the-building-2021","title":"Only Murders in the Building","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.438,"criticRating":null,"genres":["Comedy","Mystery","Crime"],"overview":"Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.","poster":"assets/tv-only-murders-in-the-building-2021-poster.jpg","backdrop":"assets/tv-only-murders-in-the-building-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0594614Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":107113,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.398,"tmdbVotes":2333}},{"id":"tv-over-the-garden-wall-2014","title":"Over the Garden Wall","year":2014,"type":"tv","runtimeSeconds":720,"seasonCount":1,"communityRating":8.6,"criticRating":null,"genres":["Mystery","Sci-Fi & Fantasy","Animation"],"overview":"Two brothers, Wirt and Greg, find themselves lost in the Unknown; a strange forest adrift in time. With the help of a wise old Woodsman and a foul-tempered bluebird named Beatrice, Wirt and Greg must travel across\u2026","poster":"assets/tv-over-the-garden-wall-2014-poster.jpg","backdrop":"assets/tv-over-the-garden-wall-2014-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0455757Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":61617,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":1854}},{"id":"tv-peaky-blinders-2013","title":"Peaky Blinders","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":6,"communityRating":8.5,"criticRating":null,"genres":["Drama","Crime"],"overview":"A gangster family epic set in 1919 Birmingham, England and centered on a gang who sew razor blades in the peaks of their caps, and their fierce boss Tommy Shelby, who means to move up in the world.","poster":"assets/tv-peaky-blinders-2013-poster.jpg","backdrop":"assets/tv-peaky-blinders-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0539185Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":60574,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.5,"tmdbVotes":11506}},{"id":"tv-the-pitt-2025","title":"The Pitt","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.729,"criticRating":null,"genres":["Drama"],"overview":"The staff of Pittsburgh's Trauma Medical Center work around the clock to save lives in an overcrowded and underfunded emergency department.","poster":"assets/tv-the-pitt-2025-poster.jpg","backdrop":"assets/tv-the-pitt-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0582942Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":250307,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.731,"tmdbVotes":1046}},{"id":"tv-pluribus-2025","title":"Pluribus","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.936,"criticRating":null,"genres":["Drama","Sci-Fi & Fantasy"],"overview":"The most miserable person on Earth must save the world from happiness.","poster":"assets/tv-pluribus-2025-poster.jpg","backdrop":"assets/tv-pluribus-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0674462Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":225171,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.921,"tmdbVotes":1191}},{"id":"tv-ripley-2024","title":"RIPLEY","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.911,"criticRating":null,"genres":["Drama"],"overview":"A grifter in 1960s New York is hired to convince a wealthy man's son to return home from Italy and begins a life of deceit, fraud and murder.","poster":"assets/tv-ripley-2024-poster.jpg","backdrop":"assets/tv-ripley-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0658942Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":94028,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.909,"tmdbVotes":453}},{"id":"tv-say-nothing-2024","title":"Say Nothing","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.462,"criticRating":null,"genres":["Drama","Crime"],"overview":"Through the eyes of various Irish Republican Army (IRA) members, explore the extremes some people will go to in the name of their beliefs, the way a deeply divided society can suddenly tip over into armed conflict,\u2026","poster":"assets/tv-say-nothing-2024-poster.jpg","backdrop":"assets/tv-say-nothing-2024-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0499752Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":245256,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.475,"tmdbVotes":122}},{"id":"tv-severance-2022","title":"Severance","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.396,"criticRating":null,"genres":["Drama","Mystery","Sci-Fi & Fantasy"],"overview":"Mark leads a team of office workers whose memories have been surgically divided between their work and personal lives. When a mysterious colleague appears outside of work, it begins a journey to discover the truth\u2026","poster":"assets/tv-severance-2022-poster.jpg","backdrop":"assets/tv-severance-2022-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0598478Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":95396,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.389,"tmdbVotes":3095}},{"id":"tv-six-feet-under-2001","title":"Six Feet Under","year":2001,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.115,"criticRating":null,"genres":["Drama"],"overview":"When death is your business, what is your life? For the Fisher family, the world outside of their family-owned funeral home continues to be at least as challenging as\u2014and far less predictable than\u2014the one inside.","poster":"assets/tv-six-feet-under-2001-poster.jpg","backdrop":"assets/tv-six-feet-under-2001-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T04:04:05.108787Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1274,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.1,"tmdbVotes":1192}},{"id":"tv-undercover-2019","title":"Undercover","year":2019,"type":"tv","runtimeSeconds":3000,"seasonCount":3,"communityRating":7.5,"criticRating":null,"genres":["Crime","Drama"],"overview":"Undercover agents infiltrate a drug kingpin's operation by posing as a couple at the campground where he spends his weekends. Inspired by real events.","poster":"assets/tv-undercover-2019-poster.jpg","backdrop":"assets/tv-undercover-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0570153Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86942,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":186}},{"id":"tv-upload-2020","title":"Upload","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":7.781,"criticRating":null,"genres":["Comedy","Sci-Fi & Fantasy","Drama"],"overview":"In 2033, people who are near death can be \u201cuploaded\u201d into virtual reality hotels run by 6 tech firms. Cash-strapped Nora lives in Brooklyn and works customer service for the luxurious \u201cLakeview\u201d digital afterlife.\u2026","poster":"assets/tv-upload-2020-poster.jpg","backdrop":"assets/tv-upload-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0566967Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86248,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.757,"tmdbVotes":1511}},{"id":"tv-the-walk-in-2022","title":"The Walk-In","year":2022,"type":"tv","runtimeSeconds":2760,"seasonCount":1,"communityRating":6.9,"criticRating":null,"genres":["Drama","Crime"],"overview":"Forced to flee the UK and go into hiding in the 1990s, when he became a mole from within the BNP, Matthew Collins returned to Britain to make a new life for himself, working with young white men in the fight against\u2026","poster":"assets/tv-the-walk-in-2022-poster.jpg","backdrop":"assets/tv-the-walk-in-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0654569Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":210664,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":50}},{"id":"tv-the-white-lotus-2021","title":"The White Lotus","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.596,"criticRating":null,"genres":["Comedy","Drama","Mystery"],"overview":"Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026","poster":"assets/tv-the-white-lotus-2021-poster.jpg","backdrop":"assets/tv-the-white-lotus-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0546438Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.599,"tmdbVotes":1604}},{"id":"tv-widow-s-bay-2026","title":"Widow's Bay","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.172,"criticRating":null,"genres":["Drama","Comedy","Mystery"],"overview":"The mayor of a New England town resolves to turn it into the next tourist hot spot, despite local warnings that it's cursed.","poster":"assets/tv-widow-s-bay-2026-poster.jpg","backdrop":"assets/tv-widow-s-bay-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T10:27:12.7003979Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":270476,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":98,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8,"tmdbVotes":585}},{"id":"tv-the-witcher-2019","title":"The Witcher","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":7.905,"criticRating":null,"genres":["Drama","Action & Adventure"],"overview":"Geralt of Rivia, a mutated monster-hunter for hire, journeys toward his destiny in a turbulent world where people often prove more wicked than beasts.","poster":"assets/tv-the-witcher-2019-poster.jpg","backdrop":"assets/tv-the-witcher-2019-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0552369Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":71912,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.88,"tmdbVotes":6997}},{"id":"tv-yellowjackets-2021","title":"Yellowjackets","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.449,"criticRating":null,"genres":["Drama","Mystery"],"overview":"This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026","poster":"assets/tv-yellowjackets-2021-poster.jpg","backdrop":"assets/tv-yellowjackets-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0577612Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.431,"tmdbVotes":1070}}],"anime":[],"discover":[{"id":"movie-12-years-a-slave-2013","title":"12 Years a Slave","year":2013,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","History"],"overview":"In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay\u2026","poster":"assets/movie-12-years-a-slave-2013-poster.jpg","backdrop":"assets/movie-12-years-a-slave-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":76203,"ratings":{"rtCritics":95,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":796313,"tmdb":7.927,"tmdbVotes":12335}},{"id":"movie-101-dalmatians-ii-patch-s-london-adventure-2003","title":"101 Dalmatians II: Patch's London Adventure","year":2003,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Family","Animation","Adventure"],"overview":"Being one of 101 takes its toll on Patch, who doesn't feel unique. When he's accidentally left behind on moving day, he meets his idol, Thunderbolt, who enlists him on a publicity campaign.","poster":"assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg","backdrop":"assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13654,"ratings":{"rtCritics":67,"rtCriticsLabel":"Fresh","rtAudience":41,"rtAudienceLabel":"Spilled","imdb":5.7,"imdbVotes":12343,"tmdb":6.025,"tmdbVotes":1274}},{"id":"movie-absolution-2024","title":"Absolution","year":2024,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Crime","Thriller"],"overview":"An aging ex-boxer gangster working as muscle for a Boston crime boss receives an upsetting diagnosis. Despite a faltering memory, he attempts to rectify the sins of his past and reconnect with his estranged\u2026","poster":"assets/movie-absolution-2024-poster.jpg","backdrop":"assets/movie-absolution-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":974453,"ratings":{"rtCritics":56,"rtCriticsLabel":"Rotten","rtAudience":32,"rtAudienceLabel":"Spilled","imdb":5.2,"imdbVotes":11413,"tmdb":5.906,"tmdbVotes":564}},{"id":"movie-addicted-2014","title":"Addicted","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.","poster":"assets/movie-addicted-2014-poster.jpg","backdrop":"assets/movie-addicted-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":235271,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":51,"rtAudienceLabel":"Spilled","imdb":5,"imdbVotes":13131,"tmdb":5.928,"tmdbVotes":466}},{"id":"movie-all-the-money-in-the-world-2017","title":"All the Money in the World","year":2017,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","History","Thriller"],"overview":"The story of the kidnapping of 16-year-old John Paul Getty III and the desperate attempt by his devoted mother to convince his billionaire grandfather Jean Paul Getty to pay the ransom.","poster":"assets/movie-all-the-money-in-the-world-2017-poster.jpg","backdrop":"assets/movie-all-the-money-in-the-world-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":446791,"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":66,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":97448,"tmdb":6.493,"tmdbVotes":2493}},{"id":"movie-the-alto-knights-2025","title":"The Alto Knights","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama"],"overview":"Two of New York's most notorious organized crime bosses, Frank Costello and Vito Genovese, vie for control of the city's streets. Once the best of friends, petty jealousies and a series of betrayals place them on a\u2026","poster":"assets/movie-the-alto-knights-2025-poster.jpg","backdrop":"assets/movie-the-alto-knights-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1013601,"ratings":{"rtCritics":39,"rtCriticsLabel":"Rotten","rtAudience":71,"rtAudienceLabel":"Upright","imdb":5.9,"imdbVotes":23017,"tmdb":6.223,"tmdbVotes":485}},{"id":"movie-the-amazing-spider-man-2-2014","title":"The Amazing Spider-Man 2","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026","poster":"assets/movie-the-amazing-spider-man-2-2014-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":102382,"ratings":{"rtCritics":51,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":611981,"tmdb":6.567,"tmdbVotes":14919}},{"id":"movie-ant-man-and-the-wasp-quantumania-2023","title":"Ant-Man and the Wasp: Quantumania","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026","poster":"assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg","backdrop":"assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":640146,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6,"imdbVotes":281738,"tmdb":6.197,"tmdbVotes":6236}},{"id":"movie-arctic-dogs-2019","title":"Arctic Dogs","year":2019,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Adventure","Comedy"],"overview":"Animals band together to save the day when the evil Otto Von Walrus hatches a sinister scheme to accelerate global warming and melt the Arctic Circle.","poster":"assets/movie-arctic-dogs-2019-poster.jpg","backdrop":"assets/movie-arctic-dogs-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":416153,"ratings":{"rtCritics":13,"rtCriticsLabel":"Rotten","rtAudience":60,"rtAudienceLabel":"Upright","imdb":4.9,"imdbVotes":2580,"tmdb":5.9,"tmdbVotes":148}},{"id":"movie-the-avengers-2012","title":"The Avengers","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Adventure"],"overview":"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026","poster":"assets/movie-the-avengers-2012-poster.jpg","backdrop":"assets/movie-the-avengers-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":24428,"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":1590693,"tmdb":8.084,"tmdbVotes":40262}},{"id":"movie-black-bear-2020","title":"Black Bear","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026","poster":"assets/movie-black-bear-2020-poster.jpg","backdrop":"assets/movie-black-bear-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":624788,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":18408,"tmdb":6.337,"tmdbVotes":315}},{"id":"movie-black-panther-2018","title":"Black Panther","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026","poster":"assets/movie-black-panther-2018-poster.jpg","backdrop":"assets/movie-black-panther-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284054,"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":null,"rtAudienceLabel":"Spilled","imdb":7.3,"imdbVotes":931120,"tmdb":7.36,"tmdbVotes":24190}},{"id":"movie-a-bronx-tale-1993","title":"A Bronx Tale","year":1993,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"Set in the Bronx during the tumultuous 1960s, an adolescent boy is torn between his honest, working-class father and a violent yet charismatic crime boss. Complicating matters is the youngster's growing attraction -\u2026","poster":"assets/movie-a-bronx-tale-1993-poster.jpg","backdrop":"assets/movie-a-bronx-tale-1993-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1607,"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":191930,"tmdb":7.913,"tmdbVotes":3075}},{"id":"movie-by-any-means-2026","title":"By Any Means","year":2026,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","History","Crime"],"overview":"In 1960s Mississippi, a young Black FBI agent is forced to partner with a notorious mafia hitman to investigate a wave of brutal civil rights murders.","poster":"assets/movie-by-any-means-2026-poster.jpg","backdrop":"assets/movie-by-any-means-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1380417,"ratings":{"rtCritics":49,"rtCriticsLabel":"Rotten","rtAudience":88,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":3534,"tmdb":6.6,"tmdbVotes":92}},{"id":"movie-captain-america-civil-war-2016","title":"Captain America: Civil War","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Adventure","Action","Science Fiction"],"overview":"Following the events of Age of Ultron, the collective governments of the world pass an act designed to regulate all superhuman activity. This polarizes opinion amongst the Avengers, causing two factions to side with\u2026","poster":"assets/movie-captain-america-civil-war-2016-poster.jpg","backdrop":"assets/movie-captain-america-civil-war-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":271110,"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":954943,"tmdb":7.455,"tmdbVotes":24808}},{"id":"movie-captain-america-the-first-avenger-2011","title":"Captain America: The First Avenger","year":2011,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"During World War II, Steve Rogers is a sickly man from Brooklyn who's transformed into super-soldier Captain America to aid in the war effort. Rogers must stop the Red Skull \u2013 Adolf Hitler's ruthless head of\u2026","poster":"assets/movie-captain-america-the-first-avenger-2011-poster.jpg","backdrop":"assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1771,"ratings":{"rtCritics":80,"rtCriticsLabel":"Certified Fresh","rtAudience":75,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":993453,"tmdb":7.012,"tmdbVotes":23473}},{"id":"movie-captain-america-the-winter-soldier-2014","title":"Captain America: The Winter Soldier","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"After the cataclysmic events in New York with The Avengers, Steve Rogers, aka Captain America is living quietly in Washington, D.C. and trying to adjust to the modern world. But when a S.H.I.E.L.D. colleague comes\u2026","poster":"assets/movie-captain-america-the-winter-soldier-2014-poster.jpg","backdrop":"assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":100402,"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":995514,"tmdb":7.654,"tmdbVotes":20688}},{"id":"movie-careful-what-you-wish-for-2015","title":"Careful What You Wish For","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.","poster":"assets/movie-careful-what-you-wish-for-2015-poster.jpg","backdrop":"assets/movie-careful-what-you-wish-for-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":317744,"ratings":{"rtCritics":17,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":9452,"tmdb":6.129,"tmdbVotes":446}},{"id":"tv-12-monkeys-2015","title":"12 Monkeys","year":2015,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama"],"overview":"The provocative story of Cole, a time traveler from a decimated future in a high-stakes race against the clock. Utilizing a dangerous and untested method of time travel, he journeys from 2043 to the present day on a\u2026","poster":"assets/tv-12-monkeys-2015-poster.jpg","backdrop":"assets/tv-12-monkeys-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":60948,"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.441,"tmdbVotes":1245}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-the-100-2014","title":"The 100","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026","poster":"assets/tv-the-100-2014-poster.jpg","backdrop":"assets/tv-the-100-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":48866,"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.878,"tmdbVotes":8855}},{"id":"tv-accused-2023","title":"Accused","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama"],"overview":"In this crime anthology series, viewers discover how an ordinary person got caught up in an extraordinary situation, ultimately revealing how one wrong turn leads to another, until it\u2019s too late to turn back. Told\u2026","poster":"assets/tv-accused-2023-poster.jpg","backdrop":"assets/tv-accused-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":202097,"ratings":{"rtCritics":61,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.851,"tmdbVotes":87}},{"id":"tv-after-life-2019","title":"After Life","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Tony had a perfect life. But after his wife Lisa suddenly dies, Tony changes. After contemplating taking his own life, he decides instead to live long enough to punish the world by saying and doing whatever he likes\u2026","poster":"assets/tv-after-life-2019-poster.jpg","backdrop":"assets/tv-after-life-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":79410,"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.902,"tmdbVotes":1169}},{"id":"tv-all-creatures-great-and-small-1978","title":"All Creatures Great and Small","year":1978,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The trials and misadventures of the staff at a country veterinary office in Yorkshire. James Herriot, a young animal surgeon, moves to a small Yorkshire town to begin his first job.","poster":"assets/tv-all-creatures-great-and-small-1978-poster.jpg","backdrop":"assets/tv-all-creatures-great-and-small-1978-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":7406,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":50}},{"id":"tv-american-housewife-2016","title":"American Housewife","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A family comedy narrated by Katie, a strong-willed mother, raising her flawed family in a wealthy town filled with perfect wives and their perfect offspring.","poster":"assets/tv-american-housewife-2016-poster.jpg","backdrop":"assets/tv-american-housewife-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":67117,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":187}},{"id":"tv-atlantis-2013","title":"Atlantis","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy"],"overview":"A fantasy drama set in a world of legendary heroes and mythical creatures. Far from home and desperate for answers, Jason washes up on the shores of an ancient land. A mysterious place; a world of bull leaping, of\u2026","poster":"assets/tv-atlantis-2013-poster.jpg","backdrop":"assets/tv-atlantis-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":47054,"ratings":{"rtCritics":62,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.4,"tmdbVotes":155}},{"id":"tv-avatar-the-last-airbender-2024","title":"Avatar: The Last Airbender","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.","poster":"assets/tv-avatar-the-last-airbender-2024-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":82452,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":1406}},{"id":"tv-avatar-seven-havens-2026","title":"Avatar: Seven Havens","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026","poster":"assets/tv-avatar-seven-havens-2026-poster.jpg","backdrop":"assets/tv-avatar-seven-havens-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284833,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.9,"tmdbVotes":183}},{"id":"tv-batman-the-brave-and-the-bold-2008","title":"Batman: The Brave and the Bold","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.","poster":"assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","backdrop":"assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":15804,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":295}},{"id":"tv-beat-bobby-flay-2013","title":"Beat Bobby Flay","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Two talented chefs go head-to-head for the chance to Beat Bobby Flay. To get to Bobby the chefs must first face off against each other, creating a spectacular dish with a secret ingredient of Bobby's choice. Judges\u2026","poster":"assets/tv-beat-bobby-flay-2013-poster.jpg","backdrop":"assets/tv-beat-bobby-flay-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":62481,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":31}},{"id":"tv-benson-1979","title":"Benson","year":1979,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A butler deals with life at the governor's mansion.","poster":"assets/tv-benson-1979-poster.jpg","backdrop":"assets/tv-benson-1979-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1849,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":46}},{"id":"tv-black-rabbit-2025","title":"Black Rabbit","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"A rising-star restaurateur is forced into New York's criminal underworld when his chaotic brother returns to town with loan sharks on his trail.","poster":"assets/tv-black-rabbit-2025-poster.jpg","backdrop":"assets/tv-black-rabbit-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":249039,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.983,"tmdbVotes":240}},{"id":"tv-black-ish-2014","title":"black-ish","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A family man struggles to gain a sense of cultural identity while raising his kids in a predominantly white, upper-middle-class neighborhood.","poster":"assets/tv-black-ish-2014-poster.jpg","backdrop":"assets/tv-black-ish-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":61381,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":352}},{"id":"tv-breaking-bad-2008","title":"Breaking Bad","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to\u2026","poster":"assets/tv-breaking-bad-2008-poster.jpg","backdrop":"assets/tv-breaking-bad-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1396,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.959,"tmdbVotes":19030}},{"id":"tv-the-capture-2019","title":"The Capture","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Mystery"],"overview":"When soldier Shaun Emery's conviction for a murder in Afghanistan is overturned due to flawed video evidence, he returns to life as a free man with his young daughter. But when damning CCTV footage from a night out\u2026","poster":"assets/tv-the-capture-2019-poster.jpg","backdrop":"assets/tv-the-capture-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":93166,"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.634,"tmdbVotes":361}}]}],"seeAll":{"trending":[{"id":"tv-american-gods-2017","title":"American Gods","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Sci-Fi & Fantasy"],"overview":"An ex-con becomes the traveling partner of a conman who turns out to be one of the older gods trying to recruit troops to battle the upstart deities. Based on Neil Gaiman's fantasy novel.","poster":"assets/tv-american-gods-2017-poster.jpg","backdrop":"assets/tv-american-gods-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":46639,"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":1852}},{"id":"tv-avatar-seven-havens-2026","title":"Avatar: Seven Havens","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026","poster":"assets/tv-avatar-seven-havens-2026-poster.jpg","backdrop":"assets/tv-avatar-seven-havens-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284833,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.9,"tmdbVotes":183}},{"id":"tv-batman-the-brave-and-the-bold-2008","title":"Batman: The Brave and the Bold","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.","poster":"assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","backdrop":"assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":15804,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":295}},{"id":"tv-batwoman-2019","title":"Batwoman","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure"],"overview":"Kate Kane, armed with a passion for social justice and a flair for speaking her mind, soars onto the streets of Gotham as Batwoman, an out lesbian and highly trained street fighter primed to snuff out the failing\u2026","poster":"assets/tv-batwoman-2019-poster.jpg","backdrop":"assets/tv-batwoman-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":89247,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":1619}},{"id":"tv-big-mouth-2017","title":"Big Mouth","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Animation"],"overview":"Teenage friends find their lives upended by the wonders and horrors of puberty in this edgy comedy from real-life pals Nick Kroll and Andrew Goldberg.","poster":"assets/tv-big-mouth-2017-poster.jpg","backdrop":"assets/tv-big-mouth-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":74204,"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.591,"tmdbVotes":1274}},{"id":"tv-black-lightning-2018","title":"Black Lightning","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"Jefferson Pierce is a man wrestling with a secret. As the father of two daughters and principal of a charter high school that also serves as a safe haven for young people in a New Orleans neighborhood overrun by gang\u2026","poster":"assets/tv-black-lightning-2018-poster.jpg","backdrop":"assets/tv-black-lightning-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":71663,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.907,"tmdbVotes":991}},{"id":"movie-across-the-hall-2009","title":"Across the Hall","year":2009,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Mystery"],"overview":"Convinced that his fiancee is cheating on him, a man follows her to a hotel and calls his best friend to help him avoid a calamity.","poster":"assets/movie-across-the-hall-2009-poster.jpg","backdrop":"assets/movie-across-the-hall-2009-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":21469,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":39,"rtAudienceLabel":"Spilled","imdb":5.8,"imdbVotes":5130,"tmdb":6,"tmdbVotes":129}},{"id":"movie-addicted-2014","title":"Addicted","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.","poster":"assets/movie-addicted-2014-poster.jpg","backdrop":"assets/movie-addicted-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":235271,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":51,"rtAudienceLabel":"Spilled","imdb":5,"imdbVotes":13131,"tmdb":5.928,"tmdbVotes":466}},{"id":"movie-adulterers-2016","title":"Adulterers","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Drama"],"overview":"On a steamy New Orleans afternoon, Sam Deuprey decides to surprise his beautiful wife, Ashley, for their anniversary and comes home early. But Ashley is not alone in the bedroom. She and her lover, Damien are about\u2026","poster":"assets/movie-adulterers-2016-poster.jpg","backdrop":"assets/movie-adulterers-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":376004,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":37,"rtAudienceLabel":"Spilled","imdb":5.1,"imdbVotes":2144,"tmdb":6.2,"tmdbVotes":116}},{"id":"movie-alone-with-her-2006","title":"Alone With Her","year":2006,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Thriller"],"overview":"The harrowing story of a disturbed young man's attempts to win the affections of an unsuspecting young woman. When Doug first sees Amy, he instantly falls for her and begins to watch her every move, going so far as\u2026","poster":"assets/movie-alone-with-her-2006-poster.jpg","backdrop":"assets/movie-alone-with-her-2006-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":18206,"ratings":{"rtCritics":70,"rtCriticsLabel":"Fresh","rtAudience":46,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":4359,"tmdb":5.9,"tmdbVotes":94}},{"id":"movie-awake-2007","title":"Awake","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Crime","Mystery"],"overview":"While undergoing heart surgery, a man experiences a phenomenon called \u2018anesthetic awareness\u2019, which leaves him awake but paralyzed throughout the operation. As various obstacles present themselves, his wife must make\u2026","poster":"assets/movie-awake-2007-poster.jpg","backdrop":"assets/movie-awake-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13483,"ratings":{"rtCritics":23,"rtCriticsLabel":"Rotten","rtAudience":55,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":89389,"tmdb":6.586,"tmdbVotes":1649}},{"id":"movie-black-bear-2020","title":"Black Bear","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026","poster":"assets/movie-black-bear-2020-poster.jpg","backdrop":"assets/movie-black-bear-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":624788,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":18408,"tmdb":6.337,"tmdbVotes":315}},{"id":"movie-butterfly-on-a-wheel-2007","title":"Butterfly on a Wheel","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Thriller"],"overview":"A sociopathic kidnapper methodically pushes a desperate pair of parents to their absolute breaking point.","poster":"assets/movie-butterfly-on-a-wheel-2007-poster.jpg","backdrop":"assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13250,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":35409,"tmdb":6.457,"tmdbVotes":595}},{"id":"movie-careful-what-you-wish-for-2015","title":"Careful What You Wish For","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.","poster":"assets/movie-careful-what-you-wish-for-2015-poster.jpg","backdrop":"assets/movie-careful-what-you-wish-for-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":317744,"ratings":{"rtCritics":17,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":9452,"tmdb":6.129,"tmdbVotes":446}},{"id":"movie-crime-of-passion-1956","title":"Crime of Passion","year":1956,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Thriller"],"overview":"Kathy leaves the newspaper business to marry homicide detective Bill, but is frustrated by his lack of ambition and the banality of life in the suburbs. Her drive to advance Bill's career soon takes her down a\u2026","poster":"assets/movie-crime-of-passion-1956-poster.jpg","backdrop":"assets/movie-crime-of-passion-1956-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1920,"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":34,"rtAudienceLabel":"Spilled","imdb":6.4,"imdbVotes":3750,"tmdb":6.1,"tmdbVotes":57}},{"id":"movie-deep-water-2022","title":"Deep Water","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Thriller"],"overview":"A well-to-do husband who allows his wife to have affairs in order to avoid a divorce becomes a prime suspect in the disappearance of her lovers.","poster":"assets/movie-deep-water-2022-poster.jpg","backdrop":"assets/movie-deep-water-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":619979,"ratings":{"rtCritics":35,"rtCriticsLabel":"Rotten","rtAudience":25,"rtAudienceLabel":"Spilled","imdb":5.5,"imdbVotes":74098,"tmdb":5.834,"tmdbVotes":1755}},{"id":"movie-derailed-2005","title":"Derailed","year":2005,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"When two married business executives having an affair are blackmailed by a violent criminal, they are forced to turn the tables on him to save their families.","poster":"assets/movie-derailed-2005-poster.jpg","backdrop":"assets/movie-derailed-2005-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":8999,"ratings":{"rtCritics":20,"rtCriticsLabel":"Rotten","rtAudience":58,"rtAudienceLabel":"Spilled","imdb":6.6,"imdbVotes":79563,"tmdb":6.405,"tmdbVotes":1193}},{"id":"movie-die-my-love-2025","title":"Die My Love","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"After inheriting a remote Montana house, Jackson moves there from New York with his partner Grace, and the couple soon welcome a child. As Jackson becomes increasingly absent and rural isolation sets in, Grace\u2026","poster":"assets/movie-die-my-love-2025-poster.jpg","backdrop":"assets/movie-die-my-love-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1033148,"ratings":{"rtCritics":74,"rtCriticsLabel":"Certified Fresh","rtAudience":45,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":50664,"tmdb":6.1,"tmdbVotes":683}},{"id":"movie-dream-house-2011","title":"Dream House","year":2011,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Drama","Thriller"],"overview":"Publisher Will Atenton quits a lucrative job in New York to relocate his wife, Libby, and their daughters to a quaint town in New England. However, as they settle into their home the Atentons discover that a woman\u2026","poster":"assets/movie-dream-house-2011-poster.jpg","backdrop":"assets/movie-dream-house-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":69668,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":36,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":71386,"tmdb":6.212,"tmdbVotes":1480}},{"id":"movie-dream-lover-1994","title":"Dream Lover","year":1994,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Thriller"],"overview":"Not long after they cross paths at an art gallery, architect Ray Reardon and hypnotically sensual Lena are married with children. But as strange incidents occur, Ray begins to realize he may not really know the woman\u2026","poster":"assets/movie-dream-lover-1994-poster.jpg","backdrop":"assets/movie-dream-lover-1994-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":18190,"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":38,"rtAudienceLabel":"Spilled","imdb":6.2,"imdbVotes":7865,"tmdb":6.844,"tmdbVotes":167}},{"id":"movie-drop-2025","title":"Drop","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Thriller"],"overview":"Violet, a widowed mother on her first date in years, arrives at an upscale restaurant where she is relieved that her date, Henry, is more charming and handsome than she expected. But their chemistry begins to curdle\u2026","poster":"assets/movie-drop-2025-poster.jpg","backdrop":"assets/movie-drop-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1249213,"ratings":{"rtCritics":83,"rtCriticsLabel":"Certified Fresh","rtAudience":79,"rtAudienceLabel":"Upright","imdb":6.1,"imdbVotes":61018,"tmdb":6.275,"tmdbVotes":1023}},{"id":"movie-eastern-promises-2007","title":"Eastern Promises","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Crime","Mystery"],"overview":"A Russian teenager living in London dies during childbirth but leaves clues in her diary that could tie her child to a rape involving a violent Russian mob family.","poster":"assets/movie-eastern-promises-2007-poster.jpg","backdrop":"assets/movie-eastern-promises-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":2252,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":281015,"tmdb":7.349,"tmdbVotes":3900}},{"id":"movie-faces-1968","title":"Faces","year":1968,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"Middle-aged suburban husband Richard abruptly tells his wife, Maria, that he wants a divorce. As Richard takes up with a younger woman, Maria enjoys a night on the town with her friends and meets a younger man. As\u2026","poster":"assets/movie-faces-1968-poster.jpg","backdrop":"assets/movie-faces-1968-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":753,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":12950,"tmdb":7.093,"tmdbVotes":227}},{"id":"movie-fatale-2020","title":"Fatale","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"After a wild one-night stand, successful sports agent Derrick Tyler watches his perfect life slowly disappear when he discovers the sexy and mysterious woman he risked everything for is determined police detective\u2026","poster":"assets/movie-fatale-2020-poster.jpg","backdrop":"assets/movie-fatale-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":549514,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":87,"rtAudienceLabel":"Upright","imdb":5.5,"imdbVotes":16392,"tmdb":6,"tmdbVotes":401}},{"id":"tv-56-days-2026","title":"56 Days","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"When an unidentified body is found in a luxury apartment linked to Oliver Kennedy and his girlfriend Ciara Wyse, Detectives Lee Reardon and Karl Connolly reconstruct the couple's deadly romance across the past 56 days.","poster":"assets/tv-56-days-2026-poster.jpg","backdrop":"assets/tv-56-days-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":241372,"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.388,"tmdbVotes":245}},{"id":"tv-absolutely-fabulous-1992","title":"Absolutely Fabulous","year":1992,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"Set in the world of fashion and PR, immature fun-loving mother Edina Monsoon and her best friend Patsy drive Eddie's sensible daughter, Saffron, up the wall with their constant drug abuse and outrageous selfishness.\u2026","poster":"assets/tv-absolutely-fabulous-1992-poster.jpg","backdrop":"assets/tv-absolutely-fabulous-1992-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":4493,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":198}},{"id":"tv-the-afterparty-2022","title":"The Afterparty","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Comedy","Crime"],"overview":"Nothing is as it seems in this hilarious murder mystery where each suspect's story is told through a different film genre.","poster":"assets/tv-the-afterparty-2022-poster.jpg","backdrop":"assets/tv-the-afterparty-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":106454,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.071,"tmdbVotes":245}},{"id":"tv-agatha-christie-s-marple-2004","title":"Agatha Christie's Marple","year":2004,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"The adventures of Miss Jane Marple, an elderly spinster living in the quiet little village of St Mary Mead. During her many visits to friends and relatives in other villages, Miss Marple often stumbles upon\u2026","poster":"assets/tv-agatha-christie-s-marple-2004-poster.jpg","backdrop":"assets/tv-agatha-christie-s-marple-2004-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":6127,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":152}},{"id":"tv-all-american-homecoming-2022","title":"All American: Homecoming","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"Follow the journeys of Simone Hicks, a tennis hopeful from Beverly Hills, and elite baseball player Damon Sims from Chicago, as they navigate life at the prestigious HBCU Bringston University.","poster":"assets/tv-all-american-homecoming-2022-poster.jpg","backdrop":"assets/tv-all-american-homecoming-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":153723,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":70}},{"id":"tv-and-just-like-that-2021","title":"And Just Like That\u2026","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"This new chapter of \u201cSex and the City\u201d follows Carrie, Miranda and Charlotte as they navigate the journey from the complicated reality of life and friendship in their 30s to the even more complicated reality of life\u2026","poster":"assets/tv-and-just-like-that-2021-poster.jpg","backdrop":"assets/tv-and-just-like-that-2021-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":116450,"ratings":{"rtCritics":52,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.211,"tmdbVotes":556}},{"id":"tv-anne-with-an-e-2017","title":"Anne with an E","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Family"],"overview":"A coming-of-age story about an outsider who, against all odds and numerous challenges, fights for love and acceptance and for her place in the world. The series centers on a young orphaned girl in the late 1890s,\u2026","poster":"assets/tv-anne-with-an-e-2017-poster.jpg","backdrop":"assets/tv-anne-with-an-e-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":70785,"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":5019}},{"id":"tv-awkward-2011","title":"Awkward.","year":2011,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"An irreverent look at the conflict, chaos and humor that defines teenage life through the eyes of 15-year-old Jenna Hamilton whose life begins to change when a simple accident becomes an epic misunderstanding and is\u2026","poster":"assets/tv-awkward-2011-poster.jpg","backdrop":"assets/tv-awkward-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":39859,"ratings":{"rtCritics":78,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":427}},{"id":"tv-the-baby-sitters-club-2020","title":"The Baby-Sitters Club","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Kids","Drama"],"overview":"In this contemporary take on the beloved book series, five best friends launch a baby-sitting business that's big on fun and adventure.","poster":"assets/tv-the-baby-sitters-club-2020-poster.jpg","backdrop":"assets/tv-the-baby-sitters-club-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":100412,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":506}},{"id":"tv-ballers-2015","title":"Ballers","year":2015,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Looking at the lives of former and current football players, the show follows former superstar Spencer Strasmore as he gets his life on track in retirement while mentoring other current and former players through the\u2026","poster":"assets/tv-ballers-2015-poster.jpg","backdrop":"assets/tv-ballers-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":62704,"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.809,"tmdbVotes":820}},{"id":"tv-baretta-1975","title":"Baretta","year":1975,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Mystery"],"overview":"Baretta is an American detective television series which ran on ABC from 1975 to 1978. The show was a milder version of a successful 1973\u201374 ABC series, Toma, starring Tony Musante as chameleon-like, real-life New\u2026","poster":"assets/tv-baretta-1975-poster.jpg","backdrop":"assets/tv-baretta-1975-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":3216,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":43}},{"id":"tv-based-on-a-true-story-2023","title":"Based on a True Story","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The lives of a realtor, a plumber and a former tennis star unexpectedly collide, exposing America\u2019s obsession with true crime, murder and the slow-close toilet seat.","poster":"assets/tv-based-on-a-true-story-2023-poster.jpg","backdrop":"assets/tv-based-on-a-true-story-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":197373,"ratings":{"rtCritics":79,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":160}},{"id":"tv-the-beast-in-me-2025","title":"The Beast in Me","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery"],"overview":"A famous author is pulled into a twisted mind game with her rich, powerful new neighbor \u2014 who might be a murderer.","poster":"assets/tv-the-beast-in-me-2025-poster.jpg","backdrop":"assets/tv-the-beast-in-me-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":250504,"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.072,"tmdbVotes":334}},{"id":"tv-big-city-greens-2018","title":"Big City Greens","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Family","Animation","Comedy"],"overview":"The offbeat adventures of 10-year-old Cricket Green, a mischievous and optimistic country boy who moves to the big city with his wildly out of place family \u2013 older sister Tilly, father Bill and Gramma Alice.","poster":"assets/tv-big-city-greens-2018-poster.jpg","backdrop":"assets/tv-big-city-greens-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":80587,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.555,"tmdbVotes":82}},{"id":"tv-the-bold-type-2017","title":"The Bold Type","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Comedy"],"overview":"A glimpse into the outrageous lives of Jane, Kat and Sutton, who are working at the nation's top women's magazine, Scarlet, while navigating their careers, identities and individual voices.","poster":"assets/tv-the-bold-type-2017-poster.jpg","backdrop":"assets/tv-the-bold-type-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":69630,"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":204}},{"id":"tv-brassic-2019","title":"Brassic","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama","Crime"],"overview":"A group of working-class friends finding unconventional ways to win at life in suburban northern England. These lads have dealt, scammed, bribed and conned their way through adolescence, but now, their dealing and\u2026","poster":"assets/tv-brassic-2019-poster.jpg","backdrop":"assets/tv-brassic-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":92621,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.336,"tmdbVotes":263}},{"id":"movie-12-years-a-slave-2013","title":"12 Years a Slave","year":2013,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","History"],"overview":"In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay\u2026","poster":"assets/movie-12-years-a-slave-2013-poster.jpg","backdrop":"assets/movie-12-years-a-slave-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":76203,"ratings":{"rtCritics":95,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":796313,"tmdb":7.927,"tmdbVotes":12335}},{"id":"movie-101-dalmatians-ii-patch-s-london-adventure-2003","title":"101 Dalmatians II: Patch's London Adventure","year":2003,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Family","Animation","Adventure"],"overview":"Being one of 101 takes its toll on Patch, who doesn't feel unique. When he's accidentally left behind on moving day, he meets his idol, Thunderbolt, who enlists him on a publicity campaign.","poster":"assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg","backdrop":"assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13654,"ratings":{"rtCritics":67,"rtCriticsLabel":"Fresh","rtAudience":41,"rtAudienceLabel":"Spilled","imdb":5.7,"imdbVotes":12343,"tmdb":6.025,"tmdbVotes":1274}},{"id":"movie-absolution-2024","title":"Absolution","year":2024,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Crime","Thriller"],"overview":"An aging ex-boxer gangster working as muscle for a Boston crime boss receives an upsetting diagnosis. Despite a faltering memory, he attempts to rectify the sins of his past and reconnect with his estranged\u2026","poster":"assets/movie-absolution-2024-poster.jpg","backdrop":"assets/movie-absolution-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":974453,"ratings":{"rtCritics":56,"rtCriticsLabel":"Rotten","rtAudience":32,"rtAudienceLabel":"Spilled","imdb":5.2,"imdbVotes":11413,"tmdb":5.906,"tmdbVotes":564}},{"id":"movie-all-the-money-in-the-world-2017","title":"All the Money in the World","year":2017,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","History","Thriller"],"overview":"The story of the kidnapping of 16-year-old John Paul Getty III and the desperate attempt by his devoted mother to convince his billionaire grandfather Jean Paul Getty to pay the ransom.","poster":"assets/movie-all-the-money-in-the-world-2017-poster.jpg","backdrop":"assets/movie-all-the-money-in-the-world-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":446791,"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":66,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":97448,"tmdb":6.493,"tmdbVotes":2493}},{"id":"movie-the-alto-knights-2025","title":"The Alto Knights","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama"],"overview":"Two of New York's most notorious organized crime bosses, Frank Costello and Vito Genovese, vie for control of the city's streets. Once the best of friends, petty jealousies and a series of betrayals place them on a\u2026","poster":"assets/movie-the-alto-knights-2025-poster.jpg","backdrop":"assets/movie-the-alto-knights-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1013601,"ratings":{"rtCritics":39,"rtCriticsLabel":"Rotten","rtAudience":71,"rtAudienceLabel":"Upright","imdb":5.9,"imdbVotes":23017,"tmdb":6.223,"tmdbVotes":485}},{"id":"movie-arctic-dogs-2019","title":"Arctic Dogs","year":2019,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Adventure","Comedy"],"overview":"Animals band together to save the day when the evil Otto Von Walrus hatches a sinister scheme to accelerate global warming and melt the Arctic Circle.","poster":"assets/movie-arctic-dogs-2019-poster.jpg","backdrop":"assets/movie-arctic-dogs-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":416153,"ratings":{"rtCritics":13,"rtCriticsLabel":"Rotten","rtAudience":60,"rtAudienceLabel":"Upright","imdb":4.9,"imdbVotes":2580,"tmdb":5.9,"tmdbVotes":148}},{"id":"movie-a-bronx-tale-1993","title":"A Bronx Tale","year":1993,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"Set in the Bronx during the tumultuous 1960s, an adolescent boy is torn between his honest, working-class father and a violent yet charismatic crime boss. Complicating matters is the youngster's growing attraction -\u2026","poster":"assets/movie-a-bronx-tale-1993-poster.jpg","backdrop":"assets/movie-a-bronx-tale-1993-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1607,"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":191930,"tmdb":7.913,"tmdbVotes":3075}},{"id":"movie-by-any-means-2026","title":"By Any Means","year":2026,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","History","Crime"],"overview":"In 1960s Mississippi, a young Black FBI agent is forced to partner with a notorious mafia hitman to investigate a wave of brutal civil rights murders.","poster":"assets/movie-by-any-means-2026-poster.jpg","backdrop":"assets/movie-by-any-means-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1380417,"ratings":{"rtCritics":49,"rtCriticsLabel":"Rotten","rtAudience":88,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":3534,"tmdb":6.6,"tmdbVotes":92}},{"id":"movie-captain-america-civil-war-2016","title":"Captain America: Civil War","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Adventure","Action","Science Fiction"],"overview":"Following the events of Age of Ultron, the collective governments of the world pass an act designed to regulate all superhuman activity. This polarizes opinion amongst the Avengers, causing two factions to side with\u2026","poster":"assets/movie-captain-america-civil-war-2016-poster.jpg","backdrop":"assets/movie-captain-america-civil-war-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":271110,"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":954943,"tmdb":7.455,"tmdbVotes":24808}},{"id":"movie-captain-america-the-first-avenger-2011","title":"Captain America: The First Avenger","year":2011,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"During World War II, Steve Rogers is a sickly man from Brooklyn who's transformed into super-soldier Captain America to aid in the war effort. Rogers must stop the Red Skull \u2013 Adolf Hitler's ruthless head of\u2026","poster":"assets/movie-captain-america-the-first-avenger-2011-poster.jpg","backdrop":"assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1771,"ratings":{"rtCritics":80,"rtCriticsLabel":"Certified Fresh","rtAudience":75,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":993453,"tmdb":7.012,"tmdbVotes":23473}},{"id":"movie-captain-america-the-winter-soldier-2014","title":"Captain America: The Winter Soldier","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"After the cataclysmic events in New York with The Avengers, Steve Rogers, aka Captain America is living quietly in Washington, D.C. and trying to adjust to the modern world. But when a S.H.I.E.L.D. colleague comes\u2026","poster":"assets/movie-captain-america-the-winter-soldier-2014-poster.jpg","backdrop":"assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":100402,"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":995514,"tmdb":7.654,"tmdbVotes":20688}},{"id":"tv-12-monkeys-2015","title":"12 Monkeys","year":2015,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama"],"overview":"The provocative story of Cole, a time traveler from a decimated future in a high-stakes race against the clock. Utilizing a dangerous and untested method of time travel, he journeys from 2043 to the present day on a\u2026","poster":"assets/tv-12-monkeys-2015-poster.jpg","backdrop":"assets/tv-12-monkeys-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":60948,"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.441,"tmdbVotes":1245}},{"id":"tv-accused-2023","title":"Accused","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama"],"overview":"In this crime anthology series, viewers discover how an ordinary person got caught up in an extraordinary situation, ultimately revealing how one wrong turn leads to another, until it\u2019s too late to turn back. Told\u2026","poster":"assets/tv-accused-2023-poster.jpg","backdrop":"assets/tv-accused-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":202097,"ratings":{"rtCritics":61,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.851,"tmdbVotes":87}},{"id":"tv-after-life-2019","title":"After Life","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Tony had a perfect life. But after his wife Lisa suddenly dies, Tony changes. After contemplating taking his own life, he decides instead to live long enough to punish the world by saying and doing whatever he likes\u2026","poster":"assets/tv-after-life-2019-poster.jpg","backdrop":"assets/tv-after-life-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":79410,"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.902,"tmdbVotes":1169}},{"id":"tv-all-creatures-great-and-small-1978","title":"All Creatures Great and Small","year":1978,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The trials and misadventures of the staff at a country veterinary office in Yorkshire. James Herriot, a young animal surgeon, moves to a small Yorkshire town to begin his first job.","poster":"assets/tv-all-creatures-great-and-small-1978-poster.jpg","backdrop":"assets/tv-all-creatures-great-and-small-1978-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":7406,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":50}},{"id":"tv-american-housewife-2016","title":"American Housewife","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A family comedy narrated by Katie, a strong-willed mother, raising her flawed family in a wealthy town filled with perfect wives and their perfect offspring.","poster":"assets/tv-american-housewife-2016-poster.jpg","backdrop":"assets/tv-american-housewife-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":67117,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":187}},{"id":"tv-atlantis-2013","title":"Atlantis","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy"],"overview":"A fantasy drama set in a world of legendary heroes and mythical creatures. Far from home and desperate for answers, Jason washes up on the shores of an ancient land. A mysterious place; a world of bull leaping, of\u2026","poster":"assets/tv-atlantis-2013-poster.jpg","backdrop":"assets/tv-atlantis-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":47054,"ratings":{"rtCritics":62,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.4,"tmdbVotes":155}},{"id":"tv-beat-bobby-flay-2013","title":"Beat Bobby Flay","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Two talented chefs go head-to-head for the chance to Beat Bobby Flay. To get to Bobby the chefs must first face off against each other, creating a spectacular dish with a secret ingredient of Bobby's choice. Judges\u2026","poster":"assets/tv-beat-bobby-flay-2013-poster.jpg","backdrop":"assets/tv-beat-bobby-flay-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":62481,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":31}},{"id":"tv-benson-1979","title":"Benson","year":1979,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A butler deals with life at the governor's mansion.","poster":"assets/tv-benson-1979-poster.jpg","backdrop":"assets/tv-benson-1979-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1849,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":46}},{"id":"tv-black-rabbit-2025","title":"Black Rabbit","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"A rising-star restaurateur is forced into New York's criminal underworld when his chaotic brother returns to town with loan sharks on his trail.","poster":"assets/tv-black-rabbit-2025-poster.jpg","backdrop":"assets/tv-black-rabbit-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":249039,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.983,"tmdbVotes":240}},{"id":"tv-black-ish-2014","title":"black-ish","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A family man struggles to gain a sense of cultural identity while raising his kids in a predominantly white, upper-middle-class neighborhood.","poster":"assets/tv-black-ish-2014-poster.jpg","backdrop":"assets/tv-black-ish-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":61381,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":352}},{"id":"tv-breaking-bad-2008","title":"Breaking Bad","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to\u2026","poster":"assets/tv-breaking-bad-2008-poster.jpg","backdrop":"assets/tv-breaking-bad-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1396,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.959,"tmdbVotes":19030}},{"id":"tv-the-capture-2019","title":"The Capture","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Mystery"],"overview":"When soldier Shaun Emery's conviction for a murder in Afghanistan is overturned due to flawed video evidence, he returns to life as a free man with his young daughter. But when damning CCTV footage from a night out\u2026","poster":"assets/tv-the-capture-2019-poster.jpg","backdrop":"assets/tv-the-capture-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":93166,"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.634,"tmdbVotes":361}}],"duplicate":[{"id":"movie-the-amazing-spider-man-2012","title":"The Amazing Spider-Man","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"A teenage Peter Parker grapples with both high school and amazing super-human crises as his alter-ego Spider-Man.","poster":"assets/movie-the-amazing-spider-man-2012-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1930,"ratings":{"rtCritics":71,"rtCriticsLabel":"Certified Fresh","rtAudience":77,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":791938,"tmdb":6.768,"tmdbVotes":19312}},{"id":"movie-the-amazing-spider-man-2-2014","title":"The Amazing Spider-Man 2","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026","poster":"assets/movie-the-amazing-spider-man-2-2014-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":102382,"ratings":{"rtCritics":51,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":611981,"tmdb":6.567,"tmdbVotes":14919}},{"id":"movie-the-animatrix-2003","title":"The Animatrix","year":2003,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction"],"overview":"Straight from the creators of the groundbreaking Matrix trilogy, this collection of short animated films from the world's leading anime directors fuses computer graphics and Japanese anime to provide the background\u2026","poster":"assets/movie-the-animatrix-2003-poster.jpg","backdrop":"assets/movie-the-animatrix-2003-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":55931,"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":78,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":91929,"tmdb":7.221,"tmdbVotes":1872}},{"id":"movie-ant-man-and-the-wasp-quantumania-2023","title":"Ant-Man and the Wasp: Quantumania","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026","poster":"assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg","backdrop":"assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":640146,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6,"imdbVotes":281738,"tmdb":6.197,"tmdbVotes":6236}},{"id":"movie-appleseed-alpha-2014","title":"Appleseed Alpha","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction","Action"],"overview":"The Third World War (2099\u20132126) devastated part of the Earth. Former nations are reforming while new powers emerge, and groups of humans struggle to survive in ruined cities. The story follows Deunan Knute and her\u2026","poster":"assets/movie-appleseed-alpha-2014-poster.jpg","backdrop":"assets/movie-appleseed-alpha-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":269650,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":59,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":9689,"tmdb":6.808,"tmdbVotes":436}},{"id":"movie-aquaman-and-the-lost-kingdom-2023","title":"Aquaman and the Lost Kingdom","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Fantasy"],"overview":"Black Manta seeks revenge on Aquaman for his father's death. Wielding the Black Trident's power, he becomes a formidable foe. To defend Atlantis, Arthur (Aquaman) forges an alliance with his imprisoned brother. They\u2026","poster":"assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg","backdrop":"assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":572802,"ratings":{"rtCritics":33,"rtCriticsLabel":"Rotten","rtAudience":79,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":122422,"tmdb":6.48,"tmdbVotes":3748}},{"id":"movie-the-avengers-2012","title":"The Avengers","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Adventure"],"overview":"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026","poster":"assets/movie-the-avengers-2012-poster.jpg","backdrop":"assets/movie-the-avengers-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":24428,"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":1590693,"tmdb":8.084,"tmdbVotes":40262}},{"id":"movie-avengers-confidential-black-widow-punisher-2014","title":"Avengers Confidential: Black Widow & Punisher","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction","Action"],"overview":"When the Punisher takes out a black-market weapons dealer, he stumbles upon a far-reaching terrorist plot devised by a group known as Leviathan.","poster":"assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg","backdrop":"assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":257346,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":55,"rtAudienceLabel":"Spilled","imdb":5.7,"imdbVotes":5166,"tmdb":6.368,"tmdbVotes":303}},{"id":"movie-avengers-age-of-ultron-2015","title":"Avengers: Age of Ultron","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth\u2019s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron\u2026","poster":"assets/movie-avengers-age-of-ultron-2015-poster.jpg","backdrop":"assets/movie-avengers-age-of-ultron-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":99861,"ratings":{"rtCritics":75,"rtCriticsLabel":"Certified Fresh","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":1025876,"tmdb":7.278,"tmdbVotes":25289}},{"id":"movie-avengers-infinity-war-2018","title":"Avengers: Infinity War","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Adventure","Action","Science Fiction"],"overview":"As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his\u2026","poster":"assets/movie-avengers-infinity-war-2018-poster.jpg","backdrop":"assets/movie-avengers-infinity-war-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":299536,"ratings":{"rtCritics":85,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":8.4,"imdbVotes":1421954,"tmdb":8.246,"tmdbVotes":33331}},{"id":"movie-batman-ninja-2018","title":"Batman Ninja","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action","Science Fiction"],"overview":"Batman, along with many of his allies and adversaries, finds himself transported to feudal Japan by Gorilla Grodd's time displacement machine.","poster":"assets/movie-batman-ninja-2018-poster.jpg","backdrop":"assets/movie-batman-ninja-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":485942,"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":60,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":24579,"tmdb":5.9,"tmdbVotes":1106}},{"id":"movie-batman-v-superman-dawn-of-justice-2016","title":"Batman v Superman: Dawn of Justice","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Fantasy"],"overview":"Fearing the actions of a god-like Super Hero left unchecked, Gotham City\u2019s own formidable, forceful vigilante takes on Metropolis\u2019s most revered, modern-day savior, while the world wrestles with what sort of hero it\u2026","poster":"assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg","backdrop":"assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":209112,"ratings":{"rtCritics":28,"rtCriticsLabel":"Rotten","rtAudience":63,"rtAudienceLabel":"Upright","imdb":6.4,"imdbVotes":801382,"tmdb":6.014,"tmdbVotes":19527}},{"id":"movie-batman-the-dark-knight-returns-part-1-2012","title":"Batman: The Dark Knight Returns, Part 1","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Animation"],"overview":"Batman has not been seen for ten years. A new breed of criminal ravages Gotham City, forcing 55-year-old Bruce Wayne back into the cape and cowl. But, does he still have what it takes to fight crime in a new era?","poster":"assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg","backdrop":"assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":123025,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":68943,"tmdb":7.742,"tmdbVotes":1766}},{"id":"movie-black-adam-2022","title":"Black Adam","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods\u2014and imprisoned just as quickly\u2014Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the\u2026","poster":"assets/movie-black-adam-2022-poster.jpg","backdrop":"assets/movie-black-adam-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":436270,"ratings":{"rtCritics":39,"rtCriticsLabel":"Rotten","rtAudience":87,"rtAudienceLabel":"Upright","imdb":6.1,"imdbVotes":309624,"tmdb":6.747,"tmdbVotes":7370}},{"id":"movie-black-panther-2018","title":"Black Panther","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026","poster":"assets/movie-black-panther-2018-poster.jpg","backdrop":"assets/movie-black-panther-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284054,"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":null,"rtAudienceLabel":"Spilled","imdb":7.3,"imdbVotes":931120,"tmdb":7.36,"tmdbVotes":24190}},{"id":"movie-black-panther-wakanda-forever-2022","title":"Black Panther: Wakanda Forever","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Queen Ramonda, Shuri, M\u2019Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T\u2019Challa\u2019s death. As the Wakandans strive to embrace their next chapter, the\u2026","poster":"assets/movie-black-panther-wakanda-forever-2022-poster.jpg","backdrop":"assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":505642,"ratings":{"rtCritics":84,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":405088,"tmdb":6.984,"tmdbVotes":7920}},{"id":"movie-blame-2017","title":"BLAME!","year":2017,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Drama","Science Fiction"],"overview":"In the distant technological future, civilization has reached its ultimate Net-based form. An \"infection\" in the past caused the automated systems to spiral out of order, resulting in a multi-leveled city structure\u2026","poster":"assets/movie-blame-2017-poster.jpg","backdrop":"assets/movie-blame-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":409421,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":10258,"tmdb":6.523,"tmdbVotes":370}},{"id":"movie-bleach-the-movie-fade-to-black-2008","title":"Bleach the Movie: Fade to Black","year":2008,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Animation","Fantasy"],"overview":"A strange disturbance in the World of the Living sends Ichigo and Kon to the Soul Society, where the Seireitei has been devastated by an explosion of reishi and Rukia is missing. Stranger still, none of the Soul\u2026","poster":"assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg","backdrop":"assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":17165,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":67,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":3693,"tmdb":7.1,"tmdbVotes":206}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-the-100-2014","title":"The 100","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026","poster":"assets/tv-the-100-2014-poster.jpg","backdrop":"assets/tv-the-100-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":48866,"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.878,"tmdbVotes":8855}},{"id":"tv-the-abbott-and-costello-show-1952","title":"The Abbott and Costello Show","year":1952,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"Bud and Lou are unemployed actors living in Mr. Fields\u2019 boarding house. Lou\u2019s girlfriend Hillary lives across the hall. Many situations arise leading to slapstick and puns.","poster":"assets/tv-the-abbott-and-costello-show-1952-poster.jpg","backdrop":"assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":11821,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":22}},{"id":"tv-accel-world-2012","title":"Accel World","year":2012,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Drama"],"overview":"A short, overweight student uses the Neuro Link, an evolution of the Nerve Gear, to escape himself from the reality. But his skills in the virtual world brings him to the attention of Kuroyukihime, a popular student\u2026","poster":"assets/tv-accel-world-2012-poster.jpg","backdrop":"assets/tv-accel-world-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":43167,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.429,"tmdbVotes":163}},{"id":"tv-ahsoka-2023","title":"Ahsoka","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"Former Jedi Knight Ahsoka Tano investigates an emerging threat to a vulnerable galaxy.","poster":"assets/tv-ahsoka-2023-poster.jpg","backdrop":"assets/tv-ahsoka-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":114461,"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":1351}},{"id":"tv-american-dad-2005","title":"American Dad!","year":2005,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Comedy"],"overview":"The series focuses on an eccentric motley crew that is the Smith family and their three housemates: Father, husband, and breadwinner Stan Smith; his better half housewife, Francine Smith; their college-aged daughter,\u2026","poster":"assets/tv-american-dad-2005-poster.jpg","backdrop":"assets/tv-american-dad-2005-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1433,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.022,"tmdbVotes":2481}},{"id":"tv-american-gods-2017","title":"American Gods","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Sci-Fi & Fantasy"],"overview":"An ex-con becomes the traveling partner of a conman who turns out to be one of the older gods trying to recruit troops to battle the upstart deities. Based on Neil Gaiman's fantasy novel.","poster":"assets/tv-american-gods-2017-poster.jpg","backdrop":"assets/tv-american-gods-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":46639,"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":1852}},{"id":"tv-andromeda-2000","title":"Andromeda","year":2000,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Drama","Sci-Fi & Fantasy"],"overview":"Captain Dylan Hunt and his crew quest to restore a government that once presided over an extended peace and prosperity.","poster":"assets/tv-andromeda-2000-poster.jpg","backdrop":"assets/tv-andromeda-2000-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":2985,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.068,"tmdbVotes":315}},{"id":"tv-aqua-teen-hunger-force-2000","title":"Aqua Teen Hunger Force","year":2000,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Comedy"],"overview":"The surreal adventures of three anthropomorphic fast food items: Master Shake, Frylock and Meatwad, and their human nextdoor neighbor, Carl Brutananadilewski.","poster":"assets/tv-aqua-teen-hunger-force-2000-poster.jpg","backdrop":"assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":251,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":272}},{"id":"tv-arifureta-from-commonplace-to-world-s-strongest-2019","title":"Arifureta: From Commonplace to World's Strongest","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Hajime Nagumo and his high school class are suddenly summoned to a fantastical land as heroes. But while most of his classmates have powerful stats and abilities, Hajime does not. Underappreciated and unprepared, he\u2026","poster":"assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg","backdrop":"assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":86034,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.382,"tmdbVotes":662}},{"id":"tv-arrow-2012","title":"Arrow","year":2012,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Action & Adventure"],"overview":"Spoiled billionaire playboy Oliver Queen is missing and presumed dead when his yacht is lost at sea. He returns five years later a changed man, determined to clean up the city as a hooded vigilante armed with a bow.","poster":"assets/tv-arrow-2012-poster.jpg","backdrop":"assets/tv-arrow-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1412,"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.834,"tmdbVotes":6537}},{"id":"tv-avatar-the-last-airbender-2024","title":"Avatar: The Last Airbender","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.","poster":"assets/tv-avatar-the-last-airbender-2024-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":82452,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":1406}},{"id":"tv-avatar-seven-havens-2026","title":"Avatar: Seven Havens","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026","poster":"assets/tv-avatar-seven-havens-2026-poster.jpg","backdrop":"assets/tv-avatar-seven-havens-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284833,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.9,"tmdbVotes":183}},{"id":"tv-batman-the-brave-and-the-bold-2008","title":"Batman: The Brave and the Bold","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.","poster":"assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","backdrop":"assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":15804,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":295}},{"id":"tv-batwoman-2019","title":"Batwoman","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure"],"overview":"Kate Kane, armed with a passion for social justice and a flair for speaking her mind, soars onto the streets of Gotham as Batwoman, an out lesbian and highly trained street fighter primed to snuff out the failing\u2026","poster":"assets/tv-batwoman-2019-poster.jpg","backdrop":"assets/tv-batwoman-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":89247,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":1619}},{"id":"tv-big-mouth-2017","title":"Big Mouth","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Animation"],"overview":"Teenage friends find their lives upended by the wonders and horrors of puberty in this edgy comedy from real-life pals Nick Kroll and Andrew Goldberg.","poster":"assets/tv-big-mouth-2017-poster.jpg","backdrop":"assets/tv-big-mouth-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":74204,"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.591,"tmdbVotes":1274}},{"id":"tv-black-lightning-2018","title":"Black Lightning","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"Jefferson Pierce is a man wrestling with a secret. As the father of two daughters and principal of a charter high school that also serves as a safe haven for young people in a New Orleans neighborhood overrun by gang\u2026","poster":"assets/tv-black-lightning-2018-poster.jpg","backdrop":"assets/tv-black-lightning-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":71663,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.907,"tmdbVotes":991}}]}} diff --git a/app/src/debug/assets/wolfskin-requests.json b/app/src/debug/assets/wolfskin-requests.json deleted file mode 100644 index 507ebd65..00000000 --- a/app/src/debug/assets/wolfskin-requests.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "source": "Locked Opus fixture/fixture.json; index.html EXAMPLES() pool order", - "items": [ - { - "id": "x1", - "mediaType": "tv", - "tmdbId": 11821, - "title": "The Abbott and Costello Show", - "requesterKey": "justin", - "requesterName": "Justin", - "actorKey": "justin", - "actorName": "Justin", - "stage": "Downloading", - "poster": "tv-the-abbott-and-costello-show-1952-poster.jpg", - "ratings": [ - { - "mark": "tmdb", - "score": 7.2, - "percentage": false - } - ] - }, - { - "id": "x2", - "mediaType": "movie", - "tmdbId": 13483, - "title": "Awake", - "requesterKey": "mindel", - "requesterName": "Mindel", - "actorKey": "justin", - "actorName": "Justin", - "stage": "AlmostReady", - "poster": "movie-awake-2007-poster.jpg", - "ratings": [ - { - "mark": "rotten", - "score": 23, - "percentage": true - }, - { - "mark": "spilled", - "score": 55, - "percentage": true - }, - { - "mark": "imdb", - "score": 6.5, - "percentage": false - } - ] - }, - { - "id": "x3", - "mediaType": "tv", - "tmdbId": 70785, - "title": "Anne with an E", - "requesterKey": "shared", - "requesterName": "Shared", - "actorKey": "mindel", - "actorName": "Mindel", - "stage": "Searching", - "poster": "tv-anne-with-an-e-2017-poster.jpg", - "ratings": [ - { - "mark": "fresh", - "score": 63, - "percentage": true - } - ] - }, - { - "id": "x4", - "mediaType": "movie", - "tmdbId": 99861, - "title": "Avengers: Age of Ultron", - "requesterKey": "justin", - "requesterName": "Justin", - "actorKey": "justin", - "actorName": "Justin", - "stage": "Waiting", - "poster": "movie-avengers-age-of-ultron-2015-poster.jpg", - "ratings": [ - { - "mark": "fresh", - "score": 75, - "percentage": true - }, - { - "mark": "upright", - "score": 82, - "percentage": true - }, - { - "mark": "imdb", - "score": 7.3, - "percentage": false - } - ] - }, - { - "id": "x5", - "mediaType": "movie", - "tmdbId": 1033148, - "title": "Die My Love", - "requesterKey": "mindel", - "requesterName": "Mindel", - "actorKey": "mindel", - "actorName": "Mindel", - "stage": "Failed", - "poster": "movie-die-my-love-2025-poster.jpg", - "ratings": [ - { - "mark": "fresh", - "score": 74, - "percentage": true - }, - { - "mark": "spilled", - "score": 45, - "percentage": true - }, - { - "mark": "imdb", - "score": 6, - "percentage": false - } - ] - }, - { - "id": "x6", - "mediaType": "movie", - "tmdbId": 416153, - "title": "Arctic Dogs", - "requesterKey": "shared", - "requesterName": "Shared", - "actorKey": "justin", - "actorName": "Justin", - "stage": "Ready", - "poster": "movie-arctic-dogs-2019-poster.jpg", - "ratings": [ - { - "mark": "rotten", - "score": 13, - "percentage": true - }, - { - "mark": "upright", - "score": 60, - "percentage": true - }, - { - "mark": "imdb", - "score": 4.9, - "percentage": false - } - ] - }, - { - "id": "x7", - "mediaType": "movie", - "tmdbId": 76203, - "title": "12 Years a Slave", - "requesterKey": "justin", - "requesterName": "Justin", - "actorKey": "shared", - "actorName": "Shared", - "stage": "Waiting", - "poster": "movie-12-years-a-slave-2013-poster.jpg", - "ratings": [ - { - "mark": "fresh", - "score": 95, - "percentage": true - }, - { - "mark": "upright", - "score": 90, - "percentage": true - }, - { - "mark": "imdb", - "score": 8.1, - "percentage": false - } - ] - }, - { - "id": "x8", - "mediaType": "movie", - "tmdbId": 14069, - "title": "The Girl Who Leapt Through Time", - "requesterKey": "mindel", - "requesterName": "Mindel", - "actorKey": "justin", - "actorName": "Justin", - "stage": "Waiting", - "poster": "movie-the-girl-who-leapt-through-time-2006-poster.jpg", - "ratings": [ - { - "mark": "fresh", - "score": 87, - "percentage": true - }, - { - "mark": "upright", - "score": 90, - "percentage": true - }, - { - "mark": "imdb", - "score": 7.6, - "percentage": false - } - ] - } - ] -} diff --git a/app/src/debug/assets/wolfskin-requests/movie-12-years-a-slave-2013-poster.jpg b/app/src/debug/assets/wolfskin-requests/movie-12-years-a-slave-2013-poster.jpg deleted file mode 100644 index ff1f55e6..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/movie-12-years-a-slave-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/movie-arctic-dogs-2019-poster.jpg b/app/src/debug/assets/wolfskin-requests/movie-arctic-dogs-2019-poster.jpg deleted file mode 100644 index 07552443..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/movie-arctic-dogs-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/movie-avengers-age-of-ultron-2015-poster.jpg b/app/src/debug/assets/wolfskin-requests/movie-avengers-age-of-ultron-2015-poster.jpg deleted file mode 100644 index 300840ff..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/movie-avengers-age-of-ultron-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/movie-awake-2007-poster.jpg b/app/src/debug/assets/wolfskin-requests/movie-awake-2007-poster.jpg deleted file mode 100644 index 32595897..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/movie-awake-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/movie-die-my-love-2025-poster.jpg b/app/src/debug/assets/wolfskin-requests/movie-die-my-love-2025-poster.jpg deleted file mode 100644 index f6eca818..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/movie-die-my-love-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/movie-the-girl-who-leapt-through-time-2006-poster.jpg b/app/src/debug/assets/wolfskin-requests/movie-the-girl-who-leapt-through-time-2006-poster.jpg deleted file mode 100644 index ded6cd2a..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/movie-the-girl-who-leapt-through-time-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/tv-anne-with-an-e-2017-poster.jpg b/app/src/debug/assets/wolfskin-requests/tv-anne-with-an-e-2017-poster.jpg deleted file mode 100644 index 55d443b4..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/tv-anne-with-an-e-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-requests/tv-the-abbott-and-costello-show-1952-poster.jpg b/app/src/debug/assets/wolfskin-requests/tv-the-abbott-and-costello-show-1952-poster.jpg deleted file mode 100644 index 7d6768ea..00000000 Binary files a/app/src/debug/assets/wolfskin-requests/tv-the-abbott-and-costello-show-1952-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-shell.json b/app/src/debug/assets/wolfskin-shell.json deleted file mode 100644 index d99323a9..00000000 --- a/app/src/debug/assets/wolfskin-shell.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "source": "Locked Opus fixture/fixture.json; shell-only projection; debug/test only", - "profiles": [ - { - "key": "justin", - "name": "Justin", - "initial": "J", - "color": "#62c4ff", - "destinations": { - "Home": { - "kind": "home" - }, - "Movies": { - "kind": "library", - "collection": "movies" - }, - "Shows": { - "kind": "library", - "collection": "shows" - }, - "Anime": { - "kind": "library", - "collection": "anime" - }, - "Discover": { - "kind": "discover", - "collection": "discover" - } - } - }, - { - "key": "mindel", - "name": "Mindel", - "initial": "M", - "color": "#ff9d72", - "destinations": { - "Home": { - "kind": "home" - }, - "Movies": { - "kind": "library", - "collection": "movies" - }, - "Shows": { - "kind": "library", - "collection": "shows" - }, - "Discover": { - "kind": "discover", - "collection": "discover" - } - } - }, - { - "key": "shared", - "name": "Shared", - "initial": "S", - "color": "#9be38f", - "destinations": { - "Home": { - "kind": "home" - }, - "Movies": { - "kind": "library", - "collection": "movies" - }, - "Shows": { - "kind": "library", - "collection": "shows" - }, - "Discover": { - "kind": "discover", - "collection": "discover" - } - } - } - ] -} diff --git a/app/src/debug/assets/wolfskin-titles.json b/app/src/debug/assets/wolfskin-titles.json deleted file mode 100644 index c7559aaa..00000000 --- a/app/src/debug/assets/wolfskin-titles.json +++ /dev/null @@ -1 +0,0 @@ -{"profiles":[{"key":"justin","name":"Justin","initial":"J","color":"#62c4ff","destinations":{"Home":{"kind":"home"},"Movies":{"kind":"library","collection":"movies"},"Shows":{"kind":"library","collection":"shows"},"Anime":{"kind":"library","collection":"anime"},"Discover":{"kind":"discover","collection":"discover"}},"movies":[{"id":"movie-the-girl-who-leapt-through-time-2006","title":"The Girl Who Leapt Through Time","year":2006,"type":"movie","runtimeSeconds":5886,"seasonCount":null,"communityRating":7.772,"criticRating":87,"genres":["Fantasy","Animation","Drama"],"overview":"When 17-year-old Makoto Konno gains the ability to 'leap' backwards through time, she immediately sets about improving her grades and preventing personal mishaps. However, she soon realises that changing the past\u2026","poster":"assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg","backdrop":"assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:39:11.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":14069,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":76917,"tmdb":7.775,"tmdbVotes":2345}},{"id":"movie-locke-2014","title":"Locke","year":2014,"type":"movie","runtimeSeconds":5090,"seasonCount":null,"communityRating":6.891,"criticRating":91,"genres":["Drama","Thriller"],"overview":"Ivan Locke has worked hard to craft a good life for himself. Tonight, that life will collapse around him. On the eve of the biggest challenge of his career, Ivan receives a phone call that sets in motion a series of\u2026","poster":"assets/movie-locke-2014-poster.jpg","backdrop":"assets/movie-locke-2014-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:38:48.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":210479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":171265,"tmdb":6.889,"tmdbVotes":3088}},{"id":"movie-look-who-s-back-2015","title":"Look Who's Back","year":2015,"type":"movie","runtimeSeconds":6936,"seasonCount":null,"communityRating":6.793,"criticRating":null,"genres":["Comedy"],"overview":"When Adolf Hitler reawakens at the site of his former bunker in present-day Berlin, he is mistaken for a comedian and quickly becomes a media phenomenon.","poster":"assets/movie-look-who-s-back-2015-poster.jpg","backdrop":"assets/movie-look-who-s-back-2015-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:48.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":318917,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":54672,"tmdb":6.796,"tmdbVotes":1990}},{"id":"movie-memories-1995","title":"Memories","year":1995,"type":"movie","runtimeSeconds":6805,"seasonCount":null,"communityRating":7.465,"criticRating":null,"genres":["Fantasy","Animation","Science Fiction"],"overview":"Three back-to-back anime films by three different directors make up this sci-fi trilogy three years in the making.","poster":"assets/movie-memories-1995-poster.jpg","backdrop":"assets/movie-memories-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-30T21:36:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":42994,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":26638,"tmdb":7.459,"tmdbVotes":622}},{"id":"movie-reincarnated-2012","title":"Reincarnated","year":2012,"type":"movie","runtimeSeconds":5759,"seasonCount":null,"communityRating":6.145,"criticRating":28,"genres":["Music","Documentary"],"overview":"Evolution as an artist is often times what separates legends from the more mundane. After being heavily influenced by his experience in Jamaica \u2013 and his subsequent name change from Snoop Dogg to Snoop Lion \u2013 the LBC\u2026","poster":"assets/movie-reincarnated-2012-poster.jpg","backdrop":"assets/movie-reincarnated-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":123431,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":28,"rtCriticsLabel":"Rotten","rtAudience":41,"rtAudienceLabel":"Spilled","imdb":6.2,"imdbVotes":2563,"tmdb":6.1,"tmdbVotes":69}},{"id":"movie-shipwrecked-nightmare-at-sea-2026","title":"Shipwrecked: Nightmare at Sea","year":2026,"type":"movie","runtimeSeconds":5385,"seasonCount":null,"communityRating":7.181,"criticRating":null,"genres":["Documentary"],"overview":"Never-before-seen footage and survivor accounts trace the 2012 shipwreck of a luxury cruise and the disaster that ensued, in this immersive documentary.","poster":"assets/movie-shipwrecked-nightmare-at-sea-2026-poster.jpg","backdrop":"assets/movie-shipwrecked-nightmare-at-sea-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T20:11:43.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1715492,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":6.5,"imdbVotes":5080,"tmdb":7.172,"tmdbVotes":87}},{"id":"movie-steins-gate-the-movie-load-region-of-deja-vu-2013","title":"Steins;Gate: The Movie - Load Region of D\u00e9j\u00e0 Vu","year":2013,"type":"movie","runtimeSeconds":5403,"seasonCount":null,"communityRating":7.8,"criticRating":null,"genres":["Animation","Mystery","Sci-Fi"],"overview":"After the events of the anime, Rintarou begins to feel the repercussions of extensive time travel, and eventually completely fades from reality. Kurisu, being the only companion to remember him, now must find a way\u2026","poster":"assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg","backdrop":"assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T14:55:54.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","artwork":{"posterKind":"poster","backdropKind":"backdrop"}}],"shows":[{"id":"tv-andor-2022","title":"Andor","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.293,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"In an era filled with danger, deception and intrigue, Cassian Andor will discover the difference he can make in the struggle against the tyrannical Galactic Empire. He embarks on a path that is destined to turn him\u2026","poster":"assets/tv-andor-2022-poster.jpg","backdrop":"assets/tv-andor-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0432996Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":83867,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.321,"tmdbVotes":2312}},{"id":"tv-the-boys-2019","title":"The Boys","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.443,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure"],"overview":"A group of vigilantes known informally as \u201cThe Boys\u201d set out to take down corrupt superheroes with no more than blue-collar grit and a willingness to fight dirty.","poster":"assets/tv-the-boys-2019-poster.jpg","backdrop":"assets/tv-the-boys-2019-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0410131Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":76479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":13628}},{"id":"tv-californication-2007","title":"Californication","year":2007,"type":"tv","runtimeSeconds":1680,"seasonCount":7,"communityRating":7.632,"criticRating":null,"genres":["Comedy","Drama"],"overview":"A self-loathing, alcoholic writer attempts to repair his damaged relationships with his daughter and her mother while combating sex addiction, a budding drug problem, and the seeming inability to avoid making bad\u2026","poster":"assets/tv-californication-2007-poster.jpg","backdrop":"assets/tv-californication-2007-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0428037Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1215,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.62,"tmdbVotes":1406}},{"id":"tv-firefly-2002","title":"Firefly","year":2002,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.347,"criticRating":null,"genres":["Drama","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In the year 2517, the universe is a vast frontier where lawlessness reigns and adventure awaits! Join Captain Malcolm Reynolds and his ragtag crew aboard the spaceship Serenity as they navigate the fringes of\u2026","poster":"assets/tv-firefly-2002-poster.jpg","backdrop":"assets/tv-firefly-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T11:40:08.4202369Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1437,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":77,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.351,"tmdbVotes":2550}},{"id":"tv-fleabag-2016","title":"Fleabag","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.278,"criticRating":null,"genres":["Comedy","Drama"],"overview":"A portrait into the mind of a dry-witted, sexual, angry, porn-watching, grief-riddled woman, trying to make sense of the world. As she hurls herself headlong at modern living, Fleabag is thrown roughly up against the\u2026","poster":"assets/tv-fleabag-2016-poster.jpg","backdrop":"assets/tv-fleabag-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:42:25.4848214Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":67070,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.3,"tmdbVotes":1993}},{"id":"tv-gravity-falls-2012","title":"Gravity Falls","year":2012,"type":"tv","runtimeSeconds":1320,"seasonCount":2,"communityRating":8.619,"criticRating":null,"genres":["Action & Adventure","Animation","Comedy"],"overview":"Twin brother and sister Dipper and Mabel Pines are in for an unexpected adventure when they spend the summer helping their great uncle Stan run a tourist trap in the mysterious town of Gravity Falls, Oregon.","poster":"assets/tv-gravity-falls-2012-poster.jpg","backdrop":"assets/tv-gravity-falls-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0435827Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":40075,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.624,"tmdbVotes":3647}},{"id":"tv-invincible-2021","title":"INVINCIBLE","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.638,"criticRating":null,"genres":["Animation","Drama","Sci-Fi & Fantasy"],"overview":"Mark Grayson is a normal teenager except for the fact that his father is the most powerful superhero on the planet. Shortly after his seventeenth birthday, Mark begins to develop powers of his own and enters into his\u2026","poster":"assets/tv-invincible-2021-poster.jpg","backdrop":"assets/tv-invincible-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.040159Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":95557,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":6155}},{"id":"tv-the-legend-of-korra-2012","title":"The Legend of Korra","year":2012,"type":"tv","runtimeSeconds":1380,"seasonCount":4,"communityRating":8.2,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Avatar Korra, a headstrong, rebellious, feisty young woman who continually challenges and breaks with tradition, is on her quest to become a fully realized Avatar. In this story, the Avatar struggles to find balance\u2026","poster":"assets/tv-the-legend-of-korra-2012-poster.jpg","backdrop":"assets/tv-the-legend-of-korra-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T00:19:23.2494945Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":33880,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.221,"tmdbVotes":2482}},{"id":"tv-love-death-robots-2019","title":"Love, Death & Robots","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.205,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy"],"overview":"Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.","poster":"assets/tv-love-death-robots-2019-poster.jpg","backdrop":"assets/tv-love-death-robots-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T11:31:20.8818757Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86831,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.203,"tmdbVotes":4238}},{"id":"tv-norsemen-2016","title":"Norsemen","year":2016,"type":"tv","runtimeSeconds":1800,"seasonCount":3,"communityRating":7.177,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Norsemen is an epic and humorous drama series set in the Viking Age. The residents of an 8th-century Viking village experience political rivalry, social change and innovations that upend their culture and way of life.","poster":"assets/tv-norsemen-2016-poster.jpg","backdrop":"assets/tv-norsemen-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.034182Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":68126,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":233}},{"id":"tv-rick-and-morty-2013","title":"Rick and Morty","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":9,"communityRating":8.679,"criticRating":null,"genres":["Animation","Comedy","Sci-Fi & Fantasy"],"overview":"Follows a sociopathic genius scientist who drags his inherently timid grandson on adventures across the universe.","poster":"assets/tv-rick-and-morty-2013-poster.jpg","backdrop":"assets/tv-rick-and-morty-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0419581Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":60625,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":11548}},{"id":"tv-secret-level-2024","title":"Secret Level","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.411,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy","Action & Adventure"],"overview":"Adult animated series of original short stories which are set within the worlds of beloved video games. Each episode serves as a gateway to a new adventure, unlocking exciting worlds from beloved gaming classics and\u2026","poster":"assets/tv-secret-level-2024-poster.jpg","backdrop":"assets/tv-secret-level-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0336417Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":261579,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.392,"tmdbVotes":612}},{"id":"tv-silo-2023","title":"Silo","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":8.178,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama"],"overview":"In a ruined and toxic future, thousands live in a giant silo deep underground. After its sheriff breaks a cardinal rule and residents die mysteriously, engineer Juliette starts to uncover shocking secrets and the\u2026","poster":"assets/tv-silo-2023-poster.jpg","backdrop":"assets/tv-silo-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0346526Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":125988,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.232,"tmdbVotes":2881}},{"id":"tv-south-park-1997","title":"South Park","year":1997,"type":"tv","runtimeSeconds":null,"seasonCount":29,"communityRating":8.336,"criticRating":null,"genres":["Animation","Comedy"],"overview":"Follow the misadventures of four irreverent grade-schoolers in the quiet, dysfunctional town of South Park, Colorado.","poster":"assets/tv-south-park-1997-poster.jpg","backdrop":"assets/tv-south-park-1997-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0456124Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2190,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.332,"tmdbVotes":5250}}],"anime":[{"id":"tv-bleach-2004","title":"Bleach","year":2004,"type":"tv","runtimeSeconds":1440,"seasonCount":17,"communityRating":8.363,"criticRating":null,"genres":["Action & Adventure","Animation","Sci-Fi & Fantasy"],"overview":"For as long as he can remember, Ichigo Kurosaki has been able to see ghosts. But when he meets Rukia, a Soul Reaper who battles evil spirits known as Hollows, he finds his life is changed forever. Now, with a\u2026","poster":"assets/tv-bleach-2004-poster.jpg","backdrop":"assets/tv-bleach-2004-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0359827Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":30984,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":2331}},{"id":"tv-chainsaw-man-2022","title":"Chainsaw Man","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":1,"communityRating":8.515,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Denji has a simple dream\u2014to live a happy and peaceful life, spending time with a girl he likes. This is a far cry from reality, however, as Denji is forced by the yakuza into killing devils in order to pay off his\u2026","poster":"assets/tv-chainsaw-man-2022-poster.jpg","backdrop":"assets/tv-chainsaw-man-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2095501Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":114410,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.508,"tmdbVotes":2364}},{"id":"tv-cyberpunk-edgerunners-2022","title":"Cyberpunk: Edgerunners","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.466,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a dystopia riddled with corruption and cybernetic implants, a talented but reckless street kid strives to become a mercenary outlaw \u2014 an edgerunner.","poster":"assets/tv-cyberpunk-edgerunners-2022-poster.jpg","backdrop":"assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2093107Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":105248,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.507,"tmdbVotes":2132}},{"id":"tv-dan-da-dan-2024","title":"Dan Da Dan","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.538,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"In a bet to prove whether ghosts or aliens exist, two high schoolers face terrifying paranormal threats, gain superpowers and maybe even fall in love?!","poster":"assets/tv-dan-da-dan-2024-poster.jpg","backdrop":"assets/tv-dan-da-dan-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2652252Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":240411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.517,"tmdbVotes":986}},{"id":"tv-dr-stone-2019","title":"Dr. STONE","year":2019,"type":"tv","runtimeSeconds":1440,"seasonCount":4,"communityRating":8.541,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"One fateful day, all of humanity was petrified by a blinding flash of light. After several millennia, high schooler Taiju awakens and finds himself lost in a world of statues. However, he\u2019s not alone! His\u2026","poster":"assets/tv-dr-stone-2019-poster.jpg","backdrop":"assets/tv-dr-stone-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2091579Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86031,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.53,"tmdbVotes":1814}},{"id":"tv-frieren-beyond-journey-s-end-2023","title":"Frieren: Beyond Journey's End","year":2023,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":8.803,"criticRating":null,"genres":["Animation","Action & Adventure","Drama"],"overview":"After the party of heroes defeated the Demon King, they restored peace to the land and returned to lives of solitude. Generations pass, and the elven mage Frieren comes face to face with humanity\u2019s mortality. She\u2026","poster":"assets/tv-frieren-beyond-journey-s-end-2023-poster.jpg","backdrop":"assets/tv-frieren-beyond-journey-s-end-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2094435Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":209867,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.826,"tmdbVotes":1016}},{"id":"tv-fullmetal-alchemist-brotherhood-2009","title":"Fullmetal Alchemist: Brotherhood","year":2009,"type":"tv","runtimeSeconds":1500,"seasonCount":1,"communityRating":8.7,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Disregard for alchemy\u2019s laws ripped half of Edward Elric\u2019s limbs from his body and left his brother Alphonse\u2019s soul clinging to a suit of armor. To restore what was lost, the brothers seek the Philosopher\u2019s Stone.\u2026","poster":"assets/tv-fullmetal-alchemist-brotherhood-2009-poster.jpg","backdrop":"assets/tv-fullmetal-alchemist-brotherhood-2009-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T10:27:12.3573939Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":31911,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":2633}},{"id":"tv-jujutsu-kaisen-2020","title":"JUJUTSU KAISEN","year":2020,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.583,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Yuji Itadori is a boy with tremendous physical strength, though he lives a completely ordinary high school life. One day, to save a classmate who has been attacked by curses, he eats the finger of Ryomen Sukuna,\u2026","poster":"assets/tv-jujutsu-kaisen-2020-poster.jpg","backdrop":"assets/tv-jujutsu-kaisen-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.207573Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":95479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.574,"tmdbVotes":4735}},{"id":"tv-kaiju-no-8-2024","title":"Kaiju No. 8","year":2024,"type":"tv","runtimeSeconds":1440,"seasonCount":2,"communityRating":8.436,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world plagued by creatures known as Kaiju, Kafka Hibino aspired to enlist in The Defense Force. He makes a promise to enlist with his childhood friend, Mina Ashiro. Soon, life takes them in separate ways. While\u2026","poster":"assets/tv-kaiju-no-8-2024-poster.jpg","backdrop":"assets/tv-kaiju-no-8-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2646566Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207468,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":836}},{"id":"tv-my-hero-academia-2016","title":"My Hero Academia","year":2016,"type":"tv","runtimeSeconds":1440,"seasonCount":8,"communityRating":8.607,"criticRating":null,"genres":["Action & Adventure","Animation","Sci-Fi & Fantasy"],"overview":"After he saves a bully from a Villain, a normal student is granted a superpower that allows him to attend a high school training academy for Heroes.","poster":"assets/tv-my-hero-academia-2016-poster.jpg","backdrop":"assets/tv-my-hero-academia-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2089315Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":65930,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":5426}},{"id":"tv-neon-genesis-evangelion-1995","title":"Neon Genesis Evangelion","year":1995,"type":"tv","runtimeSeconds":1500,"seasonCount":1,"communityRating":8.568,"criticRating":null,"genres":["Sci-Fi & Fantasy","Animation","Drama"],"overview":"At the turn of the century, the Angels returned to Earth, seeking to wipe out humanity in an apocalyptic fury. Devastated, mankind's last remnants moved underground to wait for the day when the Angels would come back\u2026","poster":"assets/tv-neon-genesis-evangelion-1995-poster.jpg","backdrop":"assets/tv-neon-genesis-evangelion-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0340629Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":890,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.56,"tmdbVotes":2205}},{"id":"tv-record-of-ragnarok-2021","title":"Record of Ragnarok","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":8.402,"criticRating":null,"genres":["Action & Adventure","Animation","Sci-Fi & Fantasy"],"overview":"Before eradicating humankind from the world, the gods give them one last chance to prove themselves worthy of survival. Let the Ragnarok battles begin.","poster":"assets/tv-record-of-ragnarok-2021-poster.jpg","backdrop":"assets/tv-record-of-ragnarok-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2085766Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":114868,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.403,"tmdbVotes":1929}},{"id":"tv-sakamoto-days-2025","title":"SAKAMOTO DAYS","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.071,"criticRating":null,"genres":["Animation","Comedy","Action & Adventure"],"overview":"Once the greatest hitman of all, Taro Sakamoto retired in the name of love. But when his past catches up, he must fight to protect his beloved family.","poster":"assets/tv-sakamoto-days-2025-poster.jpg","backdrop":"assets/tv-sakamoto-days-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.266066Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207332,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.057,"tmdbVotes":375}},{"id":"tv-sentenced-to-be-a-hero-2025","title":"Sentenced to Be a Hero","year":2025,"type":"tv","runtimeSeconds":1440,"seasonCount":1,"communityRating":8.338,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy","Action & Adventure"],"overview":"In a world where heroism is a punishment, Xylo Forbartz, a condemned goddess killer, battles endless hordes of monstrous abominations as part of Penal Hero Unit 9004. Death is no escape, only a cycle of resurrection\u2026","poster":"assets/tv-sentenced-to-be-a-hero-2025-poster.jpg","backdrop":"assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2666791Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":249907,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.244,"tmdbVotes":129}},{"id":"tv-spy-x-family-2022","title":"SPY x FAMILY","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.506,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"A spy, an assassin and a telepath come together to pose as a family, each for their own reasons, while hiding their true identities from each other.","poster":"assets/tv-spy-x-family-2022-poster.jpg","backdrop":"assets/tv-spy-x-family-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2670364Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":120089,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.5,"tmdbVotes":2382}},{"id":"tv-the-tatami-galaxy-2010","title":"The Tatami Galaxy","year":2010,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":8.271,"criticRating":null,"genres":["Animation","Comedy","Drama"],"overview":"When a college dropout stops for a late night bite at a mysterious ramen stand, he crosses paths with a self-proclaimed deity of matrimony. This bizarre meeting sends the young man hurtling through a horrifying\u2026","poster":"assets/tv-the-tatami-galaxy-2010-poster.jpg","backdrop":"assets/tv-the-tatami-galaxy-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-18T05:52:51.0023256Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":36243,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.3,"tmdbVotes":152}},{"id":"tv-vinland-saga-2019","title":"Vinland Saga","year":2019,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":8.527,"criticRating":null,"genres":["Animation","War & Politics","Action & Adventure"],"overview":"For a thousand years, the Vikings have made quite a name and reputation for themselves as the strongest families with a thirst for violence. Thorfinn, the son of one of the Vikings' greatest warriors, spends his\u2026","poster":"assets/tv-vinland-saga-2019-poster.jpg","backdrop":"assets/tv-vinland-saga-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.207389Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":88803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.509,"tmdbVotes":1064}},{"id":"tv-yu-yu-hakusho-1992","title":"Yu Yu Hakusho","year":1992,"type":"tv","runtimeSeconds":1440,"seasonCount":4,"communityRating":8.5,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"After dying to save a boy, delinquent tough guy Yusuke Urameshi is granted another chance at life by redeeming himself as a \"Spirit Detective.\"","poster":"assets/tv-yu-yu-hakusho-1992-poster.jpg","backdrop":"assets/tv-yu-yu-hakusho-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T21:46:01.5733049Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":30669,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.475,"tmdbVotes":519}}],"resume":[],"nextUp":[{"id":"episode-three-monsters-1992-s1e6","title":"Three Monsters","year":1992,"type":"episode","runtimeSeconds":1421,"seasonCount":null,"communityRating":7.4,"criticRating":null,"genres":["Animation","Action","Adventure"],"overview":"Yusuke receives his first real case as Spirit Detective. Three demon thieves have muscled their way into King Yama's sacred vault, stealing the three Artifacts of Darkness. Yusuke must recover the artifacts and bring\u2026","poster":"assets/episode-three-monsters-1992-s1e6-poster.jpg","backdrop":"assets/episode-three-monsters-1992-s1e6-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T21:59:21.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Yu Yu Hakusho","seasonNumber":1,"episodeNumber":6,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-all-hands-on-deck-class-1-a-2021-s5e1","title":"All Hands on Deck! Class 1-A","year":2021,"type":"episode","runtimeSeconds":1433,"seasonCount":null,"communityRating":6.938,"criticRating":null,"genres":["Animation","Action","Adventure"],"overview":"There's an emergency drill at U.A. High School, and Class 1-A is requested to report for duty!","poster":"assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg","backdrop":"assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg","played":false,"playCount":2,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T04:04:50.0000000Z","lastPlayedAt":"2026-10-19T17:47:09.3028025Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"My Hero Academia","seasonNumber":5,"episodeNumber":1,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-erickerhead-2026-s9e6","title":"Erickerhead","year":2026,"type":"episode","runtimeSeconds":1357,"seasonCount":null,"communityRating":8.196,"criticRating":null,"genres":[],"overview":"Rick's stomach is mad, broh. But it's all-you-can-eat-and-drink here, broh!","poster":"assets/episode-erickerhead-2026-s9e6-poster.jpg","backdrop":"assets/episode-erickerhead-2026-s9e6-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-28T10:23:23.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Rick and Morty","seasonNumber":9,"episodeNumber":6,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-girls-interrupted-2007-s1e7","title":"Girls, Interrupted","year":2007,"type":"episode","runtimeSeconds":1642,"seasonCount":null,"communityRating":7.4,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Hank is disturbed by Becca's negative reaction towards the concept of happy endings, even more so when he finds out that it is partially his fault.\nMarcy comes up with the idea of a threesome with Charlie and Dani,\u2026","poster":"assets/episode-girls-interrupted-2007-s1e7-poster.jpg","backdrop":"assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-05T02:46:18.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Californication","seasonNumber":1,"episodeNumber":7,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-announcement-2022-s1e7","title":"Announcement","year":2022,"type":"episode","runtimeSeconds":3012,"seasonCount":null,"communityRating":7.8,"criticRating":null,"genres":["Action","Adventure","Drama"],"overview":"The repercussions of the Aldhani heist reach all the way to Coruscant, while Cassian returns home.","poster":"assets/episode-announcement-2022-s1e7-poster.jpg","backdrop":"assets/episode-announcement-2022-s1e7-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T12:56:24.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Andor","seasonNumber":1,"episodeNumber":7,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-south-american-biker-gangs-2026-s29e1","title":"South American Biker Gangs","year":2026,"type":"episode","runtimeSeconds":1335,"seasonCount":null,"communityRating":6.4,"criticRating":null,"genres":["Animation","Comedy"],"overview":"When e-bikes arrive in South America, the boys take over the streets and the parents of South America can hardly recognize South America.","poster":"assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg","backdrop":"assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-01T06:40:46.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"South Park","seasonNumber":29,"episodeNumber":1,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}}],"recentlyAdded":[{"id":"movie-steins-gate-the-movie-load-region-of-deja-vu-2013","title":"Steins;Gate: The Movie - Load Region of D\u00e9j\u00e0 Vu","year":2013,"type":"movie","runtimeSeconds":5403,"seasonCount":null,"communityRating":7.8,"criticRating":null,"genres":["Animation","Mystery","Sci-Fi"],"overview":"After the events of the anime, Rintarou begins to feel the repercussions of extensive time travel, and eventually completely fades from reality. Kurisu, being the only companion to remember him, now must find a way\u2026","poster":"assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-poster.jpg","backdrop":"assets/movie-steins-gate-the-movie-load-region-of-deja-vu-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T14:55:54.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"tv-yu-yu-hakusho-1992","title":"Yu Yu Hakusho","year":1992,"type":"tv","runtimeSeconds":1440,"seasonCount":4,"communityRating":8.5,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"After dying to save a boy, delinquent tough guy Yusuke Urameshi is granted another chance at life by redeeming himself as a \"Spirit Detective.\"","poster":"assets/tv-yu-yu-hakusho-1992-poster.jpg","backdrop":"assets/tv-yu-yu-hakusho-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T21:46:01.5733049Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":30669,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.475,"tmdbVotes":519}},{"id":"movie-memories-1995","title":"Memories","year":1995,"type":"movie","runtimeSeconds":6805,"seasonCount":null,"communityRating":7.465,"criticRating":null,"genres":["Fantasy","Animation","Science Fiction"],"overview":"Three back-to-back anime films by three different directors make up this sci-fi trilogy three years in the making.","poster":"assets/movie-memories-1995-poster.jpg","backdrop":"assets/movie-memories-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-30T21:36:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":42994,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":26638,"tmdb":7.459,"tmdbVotes":622}},{"id":"tv-firefly-2002","title":"Firefly","year":2002,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.347,"criticRating":null,"genres":["Drama","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In the year 2517, the universe is a vast frontier where lawlessness reigns and adventure awaits! Join Captain Malcolm Reynolds and his ragtag crew aboard the spaceship Serenity as they navigate the fringes of\u2026","poster":"assets/tv-firefly-2002-poster.jpg","backdrop":"assets/tv-firefly-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T11:40:08.4202369Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1437,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":77,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.351,"tmdbVotes":2550}},{"id":"tv-love-death-robots-2019","title":"Love, Death & Robots","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.205,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy"],"overview":"Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.","poster":"assets/tv-love-death-robots-2019-poster.jpg","backdrop":"assets/tv-love-death-robots-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T11:31:20.8818757Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86831,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.203,"tmdbVotes":4238}},{"id":"tv-the-legend-of-korra-2012","title":"The Legend of Korra","year":2012,"type":"tv","runtimeSeconds":1380,"seasonCount":4,"communityRating":8.2,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Avatar Korra, a headstrong, rebellious, feisty young woman who continually challenges and breaks with tradition, is on her quest to become a fully realized Avatar. In this story, the Avatar struggles to find balance\u2026","poster":"assets/tv-the-legend-of-korra-2012-poster.jpg","backdrop":"assets/tv-the-legend-of-korra-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T00:19:23.2494945Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":33880,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.221,"tmdbVotes":2482}},{"id":"tv-spy-x-family-2022","title":"SPY x FAMILY","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.506,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"A spy, an assassin and a telepath come together to pose as a family, each for their own reasons, while hiding their true identities from each other.","poster":"assets/tv-spy-x-family-2022-poster.jpg","backdrop":"assets/tv-spy-x-family-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2670364Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":120089,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.5,"tmdbVotes":2382}},{"id":"tv-sentenced-to-be-a-hero-2025","title":"Sentenced to Be a Hero","year":2025,"type":"tv","runtimeSeconds":1440,"seasonCount":1,"communityRating":8.338,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy","Action & Adventure"],"overview":"In a world where heroism is a punishment, Xylo Forbartz, a condemned goddess killer, battles endless hordes of monstrous abominations as part of Penal Hero Unit 9004. Death is no escape, only a cycle of resurrection\u2026","poster":"assets/tv-sentenced-to-be-a-hero-2025-poster.jpg","backdrop":"assets/tv-sentenced-to-be-a-hero-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2666791Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":249907,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.244,"tmdbVotes":129}},{"id":"tv-sakamoto-days-2025","title":"SAKAMOTO DAYS","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.071,"criticRating":null,"genres":["Animation","Comedy","Action & Adventure"],"overview":"Once the greatest hitman of all, Taro Sakamoto retired in the name of love. But when his past catches up, he must fight to protect his beloved family.","poster":"assets/tv-sakamoto-days-2025-poster.jpg","backdrop":"assets/tv-sakamoto-days-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.266066Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207332,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.057,"tmdbVotes":375}},{"id":"tv-dan-da-dan-2024","title":"Dan Da Dan","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.538,"criticRating":null,"genres":["Animation","Action & Adventure","Comedy"],"overview":"In a bet to prove whether ghosts or aliens exist, two high schoolers face terrifying paranormal threats, gain superpowers and maybe even fall in love?!","poster":"assets/tv-dan-da-dan-2024-poster.jpg","backdrop":"assets/tv-dan-da-dan-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2652252Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":240411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.517,"tmdbVotes":986}},{"id":"tv-kaiju-no-8-2024","title":"Kaiju No. 8","year":2024,"type":"tv","runtimeSeconds":1440,"seasonCount":2,"communityRating":8.436,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world plagued by creatures known as Kaiju, Kafka Hibino aspired to enlist in The Defense Force. He makes a promise to enlist with his childhood friend, Mina Ashiro. Soon, life takes them in separate ways. While\u2026","poster":"assets/tv-kaiju-no-8-2024-poster.jpg","backdrop":"assets/tv-kaiju-no-8-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-22T10:31:16.2646566Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207468,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":836}},{"id":"tv-chainsaw-man-2022","title":"Chainsaw Man","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":1,"communityRating":8.515,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Denji has a simple dream\u2014to live a happy and peaceful life, spending time with a girl he likes. This is a far cry from reality, however, as Denji is forced by the yakuza into killing devils in order to pay off his\u2026","poster":"assets/tv-chainsaw-man-2022-poster.jpg","backdrop":"assets/tv-chainsaw-man-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T22:28:13.2095501Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":114410,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.508,"tmdbVotes":2364}}],"discover":[{"id":"movie-the-amazing-spider-man-2012","title":"The Amazing Spider-Man","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"A teenage Peter Parker grapples with both high school and amazing super-human crises as his alter-ego Spider-Man.","poster":"assets/movie-the-amazing-spider-man-2012-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1930,"ratings":{"rtCritics":71,"rtCriticsLabel":"Certified Fresh","rtAudience":77,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":791938,"tmdb":6.768,"tmdbVotes":19312}},{"id":"movie-the-amazing-spider-man-2-2014","title":"The Amazing Spider-Man 2","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026","poster":"assets/movie-the-amazing-spider-man-2-2014-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":102382,"ratings":{"rtCritics":51,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":611981,"tmdb":6.567,"tmdbVotes":14919}},{"id":"movie-the-animatrix-2003","title":"The Animatrix","year":2003,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction"],"overview":"Straight from the creators of the groundbreaking Matrix trilogy, this collection of short animated films from the world's leading anime directors fuses computer graphics and Japanese anime to provide the background\u2026","poster":"assets/movie-the-animatrix-2003-poster.jpg","backdrop":"assets/movie-the-animatrix-2003-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":55931,"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":78,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":91929,"tmdb":7.221,"tmdbVotes":1872}},{"id":"movie-ant-man-and-the-wasp-quantumania-2023","title":"Ant-Man and the Wasp: Quantumania","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026","poster":"assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg","backdrop":"assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":640146,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6,"imdbVotes":281738,"tmdb":6.197,"tmdbVotes":6236}},{"id":"movie-appleseed-alpha-2014","title":"Appleseed Alpha","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction","Action"],"overview":"The Third World War (2099\u20132126) devastated part of the Earth. Former nations are reforming while new powers emerge, and groups of humans struggle to survive in ruined cities. The story follows Deunan Knute and her\u2026","poster":"assets/movie-appleseed-alpha-2014-poster.jpg","backdrop":"assets/movie-appleseed-alpha-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":269650,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":59,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":9689,"tmdb":6.808,"tmdbVotes":436}},{"id":"movie-aquaman-and-the-lost-kingdom-2023","title":"Aquaman and the Lost Kingdom","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Fantasy"],"overview":"Black Manta seeks revenge on Aquaman for his father's death. Wielding the Black Trident's power, he becomes a formidable foe. To defend Atlantis, Arthur (Aquaman) forges an alliance with his imprisoned brother. They\u2026","poster":"assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg","backdrop":"assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":572802,"ratings":{"rtCritics":33,"rtCriticsLabel":"Rotten","rtAudience":79,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":122422,"tmdb":6.48,"tmdbVotes":3748}},{"id":"movie-the-avengers-2012","title":"The Avengers","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Adventure"],"overview":"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026","poster":"assets/movie-the-avengers-2012-poster.jpg","backdrop":"assets/movie-the-avengers-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":24428,"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":1590693,"tmdb":8.084,"tmdbVotes":40262}},{"id":"movie-avengers-confidential-black-widow-punisher-2014","title":"Avengers Confidential: Black Widow & Punisher","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Science Fiction","Action"],"overview":"When the Punisher takes out a black-market weapons dealer, he stumbles upon a far-reaching terrorist plot devised by a group known as Leviathan.","poster":"assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg","backdrop":"assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":257346,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":55,"rtAudienceLabel":"Spilled","imdb":5.7,"imdbVotes":5166,"tmdb":6.368,"tmdbVotes":303}},{"id":"movie-avengers-age-of-ultron-2015","title":"Avengers: Age of Ultron","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth\u2019s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron\u2026","poster":"assets/movie-avengers-age-of-ultron-2015-poster.jpg","backdrop":"assets/movie-avengers-age-of-ultron-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":99861,"ratings":{"rtCritics":75,"rtCriticsLabel":"Certified Fresh","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":1025876,"tmdb":7.278,"tmdbVotes":25289}},{"id":"movie-avengers-infinity-war-2018","title":"Avengers: Infinity War","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Adventure","Action","Science Fiction"],"overview":"As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his\u2026","poster":"assets/movie-avengers-infinity-war-2018-poster.jpg","backdrop":"assets/movie-avengers-infinity-war-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":299536,"ratings":{"rtCritics":85,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":8.4,"imdbVotes":1421954,"tmdb":8.246,"tmdbVotes":33331}},{"id":"movie-batman-ninja-2018","title":"Batman Ninja","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action","Science Fiction"],"overview":"Batman, along with many of his allies and adversaries, finds himself transported to feudal Japan by Gorilla Grodd's time displacement machine.","poster":"assets/movie-batman-ninja-2018-poster.jpg","backdrop":"assets/movie-batman-ninja-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":485942,"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":60,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":24579,"tmdb":5.9,"tmdbVotes":1106}},{"id":"movie-batman-v-superman-dawn-of-justice-2016","title":"Batman v Superman: Dawn of Justice","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Fantasy"],"overview":"Fearing the actions of a god-like Super Hero left unchecked, Gotham City\u2019s own formidable, forceful vigilante takes on Metropolis\u2019s most revered, modern-day savior, while the world wrestles with what sort of hero it\u2026","poster":"assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg","backdrop":"assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":209112,"ratings":{"rtCritics":28,"rtCriticsLabel":"Rotten","rtAudience":63,"rtAudienceLabel":"Upright","imdb":6.4,"imdbVotes":801382,"tmdb":6.014,"tmdbVotes":19527}},{"id":"movie-batman-the-dark-knight-returns-part-1-2012","title":"Batman: The Dark Knight Returns, Part 1","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Animation"],"overview":"Batman has not been seen for ten years. A new breed of criminal ravages Gotham City, forcing 55-year-old Bruce Wayne back into the cape and cowl. But, does he still have what it takes to fight crime in a new era?","poster":"assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg","backdrop":"assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":123025,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":68943,"tmdb":7.742,"tmdbVotes":1766}},{"id":"movie-black-adam-2022","title":"Black Adam","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods\u2014and imprisoned just as quickly\u2014Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the\u2026","poster":"assets/movie-black-adam-2022-poster.jpg","backdrop":"assets/movie-black-adam-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":436270,"ratings":{"rtCritics":39,"rtCriticsLabel":"Rotten","rtAudience":87,"rtAudienceLabel":"Upright","imdb":6.1,"imdbVotes":309624,"tmdb":6.747,"tmdbVotes":7370}},{"id":"movie-black-panther-2018","title":"Black Panther","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026","poster":"assets/movie-black-panther-2018-poster.jpg","backdrop":"assets/movie-black-panther-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284054,"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":null,"rtAudienceLabel":"Spilled","imdb":7.3,"imdbVotes":931120,"tmdb":7.36,"tmdbVotes":24190}},{"id":"movie-black-panther-wakanda-forever-2022","title":"Black Panther: Wakanda Forever","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Queen Ramonda, Shuri, M\u2019Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T\u2019Challa\u2019s death. As the Wakandans strive to embrace their next chapter, the\u2026","poster":"assets/movie-black-panther-wakanda-forever-2022-poster.jpg","backdrop":"assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":505642,"ratings":{"rtCritics":84,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":405088,"tmdb":6.984,"tmdbVotes":7920}},{"id":"movie-blame-2017","title":"BLAME!","year":2017,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Drama","Science Fiction"],"overview":"In the distant technological future, civilization has reached its ultimate Net-based form. An \"infection\" in the past caused the automated systems to spiral out of order, resulting in a multi-leveled city structure\u2026","poster":"assets/movie-blame-2017-poster.jpg","backdrop":"assets/movie-blame-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":409421,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":10258,"tmdb":6.523,"tmdbVotes":370}},{"id":"movie-bleach-the-movie-fade-to-black-2008","title":"Bleach the Movie: Fade to Black","year":2008,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Animation","Fantasy"],"overview":"A strange disturbance in the World of the Living sends Ichigo and Kon to the Soul Society, where the Seireitei has been devastated by an explosion of reishi and Rukia is missing. Stranger still, none of the Soul\u2026","poster":"assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg","backdrop":"assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":17165,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":67,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":3693,"tmdb":7.1,"tmdbVotes":206}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-the-100-2014","title":"The 100","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026","poster":"assets/tv-the-100-2014-poster.jpg","backdrop":"assets/tv-the-100-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":48866,"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.878,"tmdbVotes":8855}},{"id":"tv-the-abbott-and-costello-show-1952","title":"The Abbott and Costello Show","year":1952,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"Bud and Lou are unemployed actors living in Mr. Fields\u2019 boarding house. Lou\u2019s girlfriend Hillary lives across the hall. Many situations arise leading to slapstick and puns.","poster":"assets/tv-the-abbott-and-costello-show-1952-poster.jpg","backdrop":"assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":11821,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":22}},{"id":"tv-accel-world-2012","title":"Accel World","year":2012,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Drama"],"overview":"A short, overweight student uses the Neuro Link, an evolution of the Nerve Gear, to escape himself from the reality. But his skills in the virtual world brings him to the attention of Kuroyukihime, a popular student\u2026","poster":"assets/tv-accel-world-2012-poster.jpg","backdrop":"assets/tv-accel-world-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":43167,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.429,"tmdbVotes":163}},{"id":"tv-ahsoka-2023","title":"Ahsoka","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"Former Jedi Knight Ahsoka Tano investigates an emerging threat to a vulnerable galaxy.","poster":"assets/tv-ahsoka-2023-poster.jpg","backdrop":"assets/tv-ahsoka-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":114461,"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":1351}},{"id":"tv-american-dad-2005","title":"American Dad!","year":2005,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Comedy"],"overview":"The series focuses on an eccentric motley crew that is the Smith family and their three housemates: Father, husband, and breadwinner Stan Smith; his better half housewife, Francine Smith; their college-aged daughter,\u2026","poster":"assets/tv-american-dad-2005-poster.jpg","backdrop":"assets/tv-american-dad-2005-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1433,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.022,"tmdbVotes":2481}},{"id":"tv-american-gods-2017","title":"American Gods","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Sci-Fi & Fantasy"],"overview":"An ex-con becomes the traveling partner of a conman who turns out to be one of the older gods trying to recruit troops to battle the upstart deities. Based on Neil Gaiman's fantasy novel.","poster":"assets/tv-american-gods-2017-poster.jpg","backdrop":"assets/tv-american-gods-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":46639,"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":1852}},{"id":"tv-andromeda-2000","title":"Andromeda","year":2000,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Drama","Sci-Fi & Fantasy"],"overview":"Captain Dylan Hunt and his crew quest to restore a government that once presided over an extended peace and prosperity.","poster":"assets/tv-andromeda-2000-poster.jpg","backdrop":"assets/tv-andromeda-2000-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":2985,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.068,"tmdbVotes":315}},{"id":"tv-aqua-teen-hunger-force-2000","title":"Aqua Teen Hunger Force","year":2000,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Comedy"],"overview":"The surreal adventures of three anthropomorphic fast food items: Master Shake, Frylock and Meatwad, and their human nextdoor neighbor, Carl Brutananadilewski.","poster":"assets/tv-aqua-teen-hunger-force-2000-poster.jpg","backdrop":"assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":251,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":272}},{"id":"tv-arifureta-from-commonplace-to-world-s-strongest-2019","title":"Arifureta: From Commonplace to World's Strongest","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"Hajime Nagumo and his high school class are suddenly summoned to a fantastical land as heroes. But while most of his classmates have powerful stats and abilities, Hajime does not. Underappreciated and unprepared, he\u2026","poster":"assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg","backdrop":"assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":86034,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.382,"tmdbVotes":662}},{"id":"tv-arrow-2012","title":"Arrow","year":2012,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Action & Adventure"],"overview":"Spoiled billionaire playboy Oliver Queen is missing and presumed dead when his yacht is lost at sea. He returns five years later a changed man, determined to clean up the city as a hooded vigilante armed with a bow.","poster":"assets/tv-arrow-2012-poster.jpg","backdrop":"assets/tv-arrow-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1412,"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.834,"tmdbVotes":6537}},{"id":"tv-avatar-the-last-airbender-2024","title":"Avatar: The Last Airbender","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.","poster":"assets/tv-avatar-the-last-airbender-2024-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":82452,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":1406}},{"id":"tv-avatar-seven-havens-2026","title":"Avatar: Seven Havens","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026","poster":"assets/tv-avatar-seven-havens-2026-poster.jpg","backdrop":"assets/tv-avatar-seven-havens-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284833,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.9,"tmdbVotes":183}},{"id":"tv-batman-the-brave-and-the-bold-2008","title":"Batman: The Brave and the Bold","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.","poster":"assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","backdrop":"assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":15804,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":295}},{"id":"tv-batwoman-2019","title":"Batwoman","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure"],"overview":"Kate Kane, armed with a passion for social justice and a flair for speaking her mind, soars onto the streets of Gotham as Batwoman, an out lesbian and highly trained street fighter primed to snuff out the failing\u2026","poster":"assets/tv-batwoman-2019-poster.jpg","backdrop":"assets/tv-batwoman-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":89247,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":1619}},{"id":"tv-big-mouth-2017","title":"Big Mouth","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Animation"],"overview":"Teenage friends find their lives upended by the wonders and horrors of puberty in this edgy comedy from real-life pals Nick Kroll and Andrew Goldberg.","poster":"assets/tv-big-mouth-2017-poster.jpg","backdrop":"assets/tv-big-mouth-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":74204,"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.591,"tmdbVotes":1274}},{"id":"tv-black-lightning-2018","title":"Black Lightning","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"Jefferson Pierce is a man wrestling with a secret. As the father of two daughters and principal of a charter high school that also serves as a safe haven for young people in a New Orleans neighborhood overrun by gang\u2026","poster":"assets/tv-black-lightning-2018-poster.jpg","backdrop":"assets/tv-black-lightning-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":71663,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.907,"tmdbVotes":991}}],"discoverySource":{"state":"materialized_snapshot","rankKnown":false,"selection":"First 18 alphabetically indexed movie stubs and first 18 series stubs, not ranked feed order","observedAt":"2026-11-04T20:42:12.030108+00:00"}},{"key":"mindel","name":"Mindel","initial":"M","color":"#ff9d72","destinations":{"Home":{"kind":"home"},"Movies":{"kind":"library","collection":"movies"},"Shows":{"kind":"library","collection":"shows"},"Discover":{"kind":"discover","collection":"discover"}},"movies":[{"id":"movie-chicago-2002","title":"Chicago","year":2002,"type":"movie","runtimeSeconds":6808,"seasonCount":null,"communityRating":7.131,"criticRating":87,"genres":["Comedy","Crime","Drama"],"overview":"Murderesses Velma Kelly and Roxie Hart find themselves on death row together and fight for the fame that will keep them from the gallows in 1920s Chicago.","poster":"assets/movie-chicago-2002-poster.jpg","backdrop":"assets/movie-chicago-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:53.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1574,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":259272,"tmdb":7.135,"tmdbVotes":3025}},{"id":"movie-coherence-2014","title":"Coherence","year":2014,"type":"movie","runtimeSeconds":5286,"seasonCount":null,"communityRating":7.21,"criticRating":89,"genres":["Thriller","Science Fiction"],"overview":"Four couples gather for dinner the night a mysterious and powerful comet passes overhead.","poster":"assets/movie-coherence-2014-poster.jpg","backdrop":"assets/movie-coherence-2014-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2017-08-12T23:49:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":220289,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":180354,"tmdb":7.208,"tmdbVotes":3702}},{"id":"movie-the-killing-of-a-sacred-deer-2017","title":"The Killing of a Sacred Deer","year":2017,"type":"movie","runtimeSeconds":7262,"seasonCount":null,"communityRating":7.003,"criticRating":79,"genres":["Drama","Thriller","Mystery"],"overview":"Dr. Steven Murphy is a renowned cardiovascular surgeon who presides over a spotless household with his wife and two children. Lurking at the margins of his idyllic suburban existence is Martin, a fatherless teen who\u2026","poster":"assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg","backdrop":"assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T21:49:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":399057,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":219488,"tmdb":7.001,"tmdbVotes":4640}},{"id":"movie-night-is-short-walk-on-girl-2017","title":"Night Is Short, Walk On Girl","year":2017,"type":"movie","runtimeSeconds":5548,"seasonCount":null,"communityRating":7.421,"criticRating":90,"genres":["Comedy","Romance","Animation"],"overview":"As a group of university students go out for a night on the town, a sophomore known only as \"The Girl with Black Hair\" experiences a series of surreal encounters with the local nightlife \u2013 all the while unaware of\u2026","poster":"assets/movie-night-is-short-walk-on-girl-2017-poster.jpg","backdrop":"assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:39:14.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":430214,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":7640,"tmdb":7.4,"tmdbVotes":267}},{"id":"movie-thelma-louise-1991","title":"Thelma & Louise","year":1991,"type":"movie","runtimeSeconds":7786,"seasonCount":null,"communityRating":7.596,"criticRating":87,"genres":["Drama","Crime","Adventure"],"overview":"Taking a break from their dreary lives, close friends Thelma and Louise embark on a short weekend trip that ends in unforeseen incriminating circumstances. As fugitives, both women rediscover the strength of their\u2026","poster":"assets/movie-thelma-louise-1991-poster.jpg","backdrop":"assets/movie-thelma-louise-1991-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:40.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1541,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":82,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":197947,"tmdb":7.594,"tmdbVotes":4063}},{"id":"movie-vicky-cristina-barcelona-2008","title":"Vicky Cristina Barcelona","year":2008,"type":"movie","runtimeSeconds":5804,"seasonCount":null,"communityRating":6.798,"criticRating":80,"genres":["Drama","Romance"],"overview":"Two girlfriends on a summer holiday in Spain become enamored with the same painter, unaware that his ex-wife, with whom he has a tempestuous relationship, is about to re-enter the picture.","poster":"assets/movie-vicky-cristina-barcelona-2008-poster.jpg","backdrop":"assets/movie-vicky-cristina-barcelona-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:34.0000000Z","lastPlayedAt":"2026-08-11T20:12:57.3064089Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":5038,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Certified Fresh","rtAudience":74,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":283286,"tmdb":6.797,"tmdbVotes":4164}}],"shows":[{"id":"tv-all-creatures-great-small-2020","title":"All Creatures Great & Small","year":2020,"type":"tv","runtimeSeconds":3000,"seasonCount":7,"communityRating":8.2,"criticRating":null,"genres":["Drama","Comedy"],"overview":"The heartwarming and humorous adventures of a young country vet in the Yorkshire Dales in the 1930s. A remake of the 1978 series.","poster":"assets/tv-all-creatures-great-small-2020-poster.jpg","backdrop":"assets/tv-all-creatures-great-small-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:13:03.4266156Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":108255,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.2,"tmdbVotes":168}},{"id":"tv-back-to-life-2019","title":"Back to Life","year":2019,"type":"tv","runtimeSeconds":1500,"seasonCount":2,"communityRating":7.367,"criticRating":null,"genres":["Comedy","Drama"],"overview":"When Miri Matteson returns home after eighteen years, can she integrate back into her old life? With a terrible event from her past hanging over her, it won\u2019t be easy.","poster":"assets/tv-back-to-life-2019-poster.jpg","backdrop":"assets/tv-back-to-life-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0688758Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":88510,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":65}},{"id":"tv-big-boys-2022","title":"Big Boys","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":7.6,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Two boys from very different ends of the 'spectrum of masculinity' become best friends at Brent University freshers' week; in their first year of uni, they explore, experiment and try to discover themselves, helping\u2026","poster":"assets/tv-big-boys-2022-poster.jpg","backdrop":"assets/tv-big-boys-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:25:58.2156837Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":202851,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":49}},{"id":"tv-call-the-midwife-2012","title":"Call the Midwife","year":2012,"type":"tv","runtimeSeconds":3120,"seasonCount":15,"communityRating":7.5,"criticRating":null,"genres":["Drama"],"overview":"Drama following the lives of a group of midwives working in the poverty-stricken East End of London during the 1950s, based on the best-selling memoirs of Jennifer Worth.","poster":"assets/tv-call-the-midwife-2012-poster.jpg","backdrop":"assets/tv-call-the-midwife-2012-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0377216Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":39793,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.577,"tmdbVotes":267}},{"id":"tv-derry-girls-2018","title":"Derry Girls","year":2018,"type":"tv","runtimeSeconds":1380,"seasonCount":3,"communityRating":8.144,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Amidst the political conflict of Northern Ireland in the 1990s, five secondary school students square off with the universal challenges of being a teenager.","poster":"assets/tv-derry-girls-2018-poster.jpg","backdrop":"assets/tv-derry-girls-2018-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-02T20:01:55.8955537Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":76148,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.138,"tmdbVotes":454}},{"id":"tv-desperate-housewives-2004","title":"Desperate Housewives","year":2004,"type":"tv","runtimeSeconds":2520,"seasonCount":8,"communityRating":7.896,"criticRating":null,"genres":["Mystery","Drama","Comedy"],"overview":"Looking down on her friends and family isn't a way of life for Mary Alice Young... it's a way of death. One day, in her perfect house, in the loveliest of suburbs, Mary Alice ended it all. Now she's taking us into\u2026","poster":"assets/tv-desperate-housewives-2004-poster.jpg","backdrop":"assets/tv-desperate-housewives-2004-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-29T20:56:40.3505596Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":693,"artwork":{"posterKind":"poster","backdropKind":"center_crop"},"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.896,"tmdbVotes":2171}},{"id":"tv-the-diplomat-2023","title":"The Diplomat","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":7.569,"criticRating":null,"genres":["Drama","War & Politics"],"overview":"Amid an international crisis, a US diplomat contends with her high-profile job as ambassador to the UK and her strained marriage to a political star.","poster":"assets/tv-the-diplomat-2023-poster.jpg","backdrop":"assets/tv-the-diplomat-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0640348Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":203857,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.652,"tmdbVotes":521}},{"id":"tv-the-durrells-2016","title":"The Durrells","year":2016,"type":"tv","runtimeSeconds":2760,"seasonCount":4,"communityRating":7.402,"criticRating":null,"genres":["Drama","Comedy","Family"],"overview":"In 1935, financially strapped widow Louisa Durrell, whose life has fallen apart, decides to move from England, with her four children (three sons, one daughter), to the island of Corfu, Greece. Once there, the family\u2026","poster":"assets/tv-the-durrells-2016-poster.jpg","backdrop":"assets/tv-the-durrells-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0619436Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":66134,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":127}},{"id":"tv-etoile-2025","title":"\u00c9toile","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.197,"criticRating":null,"genres":["Comedy","Drama"],"overview":"In an ambitious gambit to save their storied institutions, two world-renowned ballet companies in New York City and Paris swap their most talented stars.","poster":"assets/tv-etoile-2025-poster.jpg","backdrop":"assets/tv-etoile-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0601383Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":225383,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":40}},{"id":"tv-gilmore-girls-2000","title":"Gilmore Girls","year":2000,"type":"tv","runtimeSeconds":2640,"seasonCount":7,"communityRating":7.893,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Set in the charming town of Stars Hollow, Connecticut, the series follows the captivating lives of Lorelai and Rory Gilmore, a mother/daughter pair who have a relationship most people only dream of.","poster":"assets/tv-gilmore-girls-2000-poster.jpg","backdrop":"assets/tv-gilmore-girls-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0631035Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4586,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":1339}},{"id":"tv-ginny-georgia-2021","title":"Ginny & Georgia","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":8.026,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Free-spirited Georgia and her two kids, Ginny and Austin, move north in search of a fresh start but find that the road to new beginnings can be bumpy.","poster":"assets/tv-ginny-georgia-2021-poster.jpg","backdrop":"assets/tv-ginny-georgia-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0694828Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117581,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":68,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8,"tmdbVotes":1840}},{"id":"tv-hellcats-2010","title":"Hellcats","year":2010,"type":"tv","runtimeSeconds":2460,"seasonCount":1,"communityRating":7.5,"criticRating":null,"genres":["Drama"],"overview":"Pre-law student Marti Perkins' world is flipped upside down when she loses her scholarship, and realizes the only way she can stay in school is by reigniting her dormant teen gymnastic skills to win a place on Lancer\u2026","poster":"assets/tv-hellcats-2010-poster.jpg","backdrop":"assets/tv-hellcats-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T20:39:02.7719318Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":32504,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":116}},{"id":"tv-high-potential-2024","title":"High Potential","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.201,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"Morgan is a single mom with an exceptional mind, whose unconventional knack for solving crimes leads to an unusual and unstoppable partnership with a by-the-book seasoned detective.","poster":"assets/tv-high-potential-2024-poster.jpg","backdrop":"assets/tv-high-potential-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0686091Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":226637,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":98,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.3,"tmdbVotes":707}},{"id":"tv-how-to-get-to-heaven-from-belfast-2026","title":"How to Get to Heaven from Belfast","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.027,"criticRating":null,"genres":["Mystery","Comedy","Drama"],"overview":"Three lifelong pals embark on a chaotic quest to solve the mystery of their old friend's suspicious death and keep their own dark secret under wraps.","poster":"assets/tv-how-to-get-to-heaven-from-belfast-2026-poster.jpg","backdrop":"assets/tv-how-to-get-to-heaven-from-belfast-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0683192Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":233079,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.063,"tmdbVotes":95}},{"id":"tv-the-hunting-wives-2025","title":"The Hunting Wives","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":6.932,"criticRating":null,"genres":["Drama","Mystery"],"overview":"Sophie trades New England for East Texas and falls into a wealthy socialite's magnetic orbit \u2014 where a clique of housewives hide deadly secrets.","poster":"assets/tv-the-hunting-wives-2025-poster.jpg","backdrop":"assets/tv-the-hunting-wives-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T19:42:46.3227892Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":244447,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.964,"tmdbVotes":112}},{"id":"tv-the-lost-flowers-of-alice-hart-2023","title":"The Lost Flowers of Alice Hart","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.634,"criticRating":null,"genres":["Drama","Mystery"],"overview":"After losing her parents in a mysterious fire, nine-year-old Alice Hart is raised by her grandmother June on a flower farm where she learns there are secrets within secrets. But years on, an unearthed betrayal sees\u2026","poster":"assets/tv-the-lost-flowers-of-alice-hart-2023-poster.jpg","backdrop":"assets/tv-the-lost-flowers-of-alice-hart-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0607236Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":125816,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":82,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.64,"tmdbVotes":161}},{"id":"tv-love-is-blind-2020","title":"Love Is Blind","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":11,"communityRating":6.261,"criticRating":null,"genres":["Reality"],"overview":"Nick and Vanessa Lachey host this social experiment where single men and women look for love and get engaged, all before meeting in person.","poster":"assets/tv-love-is-blind-2020-poster.jpg","backdrop":"assets/tv-love-is-blind-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T17:11:08.2447226Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":99353,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.233,"tmdbVotes":118}},{"id":"tv-maximum-pleasure-guaranteed-2026","title":"Maximum Pleasure Guaranteed","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":6.676,"criticRating":null,"genres":["Comedy"],"overview":"A newly divorced mom falls down a dangerous rabbit hole of blackmail, murder, and youth soccer.","poster":"assets/tv-maximum-pleasure-guaranteed-2026-poster.jpg","backdrop":"assets/tv-maximum-pleasure-guaranteed-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-07T21:04:02.8277793Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":285404,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.799,"tmdbVotes":92}},{"id":"tv-minx-2022","title":"Minx","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.833,"criticRating":null,"genres":["Comedy","Drama"],"overview":"An earnest young feminist joins forces with a low-rent publisher to make the world's first erotic magazine for women.","poster":"assets/tv-minx-2022-poster.jpg","backdrop":"assets/tv-minx-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0653613Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":118303,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.9,"tmdbVotes":123}},{"id":"tv-offspring-2010","title":"Offspring","year":2010,"type":"tv","runtimeSeconds":3600,"seasonCount":7,"communityRating":6.9,"criticRating":null,"genres":["Drama"],"overview":"An exuberant drama set in Melbourne's Fitzroy, centering on Nina Proudman and her struggle to deal with her fabulously messy family, her hunt for a decent love life and her tendency to overthink and fly off into fantasy.","poster":"assets/tv-offspring-2010-poster.jpg","backdrop":"assets/tv-offspring-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0664581Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":33155,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.9,"tmdbVotes":27}},{"id":"tv-only-murders-in-the-building-2021","title":"Only Murders in the Building","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.398,"criticRating":null,"genres":["Comedy","Mystery","Crime"],"overview":"Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.","poster":"assets/tv-only-murders-in-the-building-2021-poster.jpg","backdrop":"assets/tv-only-murders-in-the-building-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:25:58.2163301Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":107113,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.398,"tmdbVotes":2333}},{"id":"tv-outlander-blood-of-my-blood-2025","title":"Outlander: Blood of My Blood","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.542,"criticRating":null,"genres":["Drama"],"overview":"From the battlefields of World War I to the rugged Highlands of 18th century Scotland, two fated couples must defy the forces that seek to tear them apart, unfolding in surprising and unforeseen ways in this romantic\u2026","poster":"assets/tv-outlander-blood-of-my-blood-2025-poster.jpg","backdrop":"assets/tv-outlander-blood-of-my-blood-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0691373Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":207484,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.738,"tmdbVotes":124}},{"id":"tv-rivals-2024","title":"Rivals","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":7.44,"criticRating":null,"genres":["Drama"],"overview":"It is 1986 and Britain is booming. \"Rivals\" dives headfirst into the cutthroat world of TV where hair-dos are big and ambitions are even bigger. Deals are brokered in boardrooms, as well as bedrooms. Nobody can be\u2026","poster":"assets/tv-rivals-2024-poster.jpg","backdrop":"assets/tv-rivals-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0645815Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":208921,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":93}},{"id":"tv-rooster-2026","title":"Rooster","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.431,"criticRating":null,"genres":["Comedy"],"overview":"On a college campus, an author navigates a complicated relationship with his daughter.","poster":"assets/tv-rooster-2026-poster.jpg","backdrop":"assets/tv-rooster-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.063389Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":254953,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.423,"tmdbVotes":144}},{"id":"tv-sex-and-the-city-1998","title":"Sex and the City","year":1998,"type":"tv","runtimeSeconds":null,"seasonCount":6,"communityRating":7.365,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Based on the bestselling book by Candace Bushnell, Sex and the City tells the story of four best friends, all single and in their late thirties, as they pursue their careers and talk about their sex lives, all while\u2026","poster":"assets/tv-sex-and-the-city-1998-poster.jpg","backdrop":"assets/tv-sex-and-the-city-1998-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0679324Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":105,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":70,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.351,"tmdbVotes":2220}},{"id":"tv-skymed-2022","title":"SkyMed","year":2022,"type":"tv","runtimeSeconds":2640,"seasonCount":4,"communityRating":6.665,"criticRating":null,"genres":["Drama"],"overview":"Life, death and drama at 20,000 feet. The series weaves together intense character journeys and high-stakes medical rescues, as we follow the triumphs, heartbreaks and tribulations of budding nurses and pilots flying\u2026","poster":"assets/tv-skymed-2022-poster.jpg","backdrop":"assets/tv-skymed-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T19:44:29.2431354Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":133908,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.699,"tmdbVotes":98}},{"id":"tv-so-you-think-you-can-dance-2008","title":"So You Think You Can Dance","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":9.8,"criticRating":null,"genres":["Family","Reality"],"overview":"So You Think You Can Dance is a joint Belgian-Dutch dance competition show broadcast on RTL 5 and vtm television stations. The first season was broadcast only in the Netherlands, whereas starting with season 2, the\u2026","poster":"assets/tv-so-you-think-you-can-dance-2008-poster.jpg","backdrop":"assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-23T11:11:35.4124418Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":40231,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":9.8,"tmdbVotes":4}},{"id":"tv-the-white-lotus-2021","title":"The White Lotus","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.597,"criticRating":null,"genres":["Comedy","Drama","Mystery"],"overview":"Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026","poster":"assets/tv-the-white-lotus-2021-poster.jpg","backdrop":"assets/tv-the-white-lotus-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T02:45:52.8742674Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.599,"tmdbVotes":1604}},{"id":"tv-yellowjackets-2021","title":"Yellowjackets","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.431,"criticRating":null,"genres":["Drama","Mystery"],"overview":"This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026","poster":"assets/tv-yellowjackets-2021-poster.jpg","backdrop":"assets/tv-yellowjackets-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:31:47.4592756Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.431,"tmdbVotes":1070}}],"anime":[],"resume":[{"id":"episode-sex-and-the-country-2001-s4e9","title":"Sex and the Country","year":2001,"type":"episode","runtimeSeconds":1751,"seasonCount":null,"communityRating":5.9,"criticRating":null,"genres":["Comedy","Drama","Romance"],"overview":"Carrie is dragged to Aidan's country home in Suffern, New York. Charlotte visits the MacDougal \"compound\" in Connecticut. Miranda yells at Steve when he tells her he has testicular cancer but has no clue about his\u2026","poster":"assets/episode-sex-and-the-country-2001-s4e9-poster.jpg","backdrop":"assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":96.08,"progress":0.0549,"addedAt":"2026-07-17T10:53:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Sex and the City","seasonNumber":4,"episodeNumber":9,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}}],"nextUp":[{"id":"episode-what-more-do-i-need-2008-s5e7","title":"What More Do I Need?","year":2008,"type":"episode","runtimeSeconds":2521,"seasonCount":null,"communityRating":8,"criticRating":null,"genres":["Comedy","Drama","Mystery"],"overview":"Lynette and Tom uncover the unsettling truth about son Porter. Gabrielle suspects Carlos' best client, Virginia, has ulterior motives. Bree's moment of weakness could bring her great embarrassment and shame.\u2026","poster":"assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg","backdrop":"assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-29T14:37:28.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Desperate Housewives","seasonNumber":5,"episodeNumber":7,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"}},{"id":"episode-new-day-2021-s1e2","title":"New Day","year":2021,"type":"episode","runtimeSeconds":3445,"seasonCount":null,"communityRating":7.143,"criticRating":null,"genres":["Comedy","Drama"],"overview":"A cautiously optimistic Belinda walks a fine line with Tanya, while Armond brainstorms ways to deal with Shane. After her husband belittles her work, Rachel approaches Nicole for advice. Paula and Olivia lose track\u2026","poster":"assets/episode-new-day-2021-s1e2-poster.jpg","backdrop":"assets/episode-new-day-2021-s1e2-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-19T22:18:57.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"The White Lotus","seasonNumber":1,"episodeNumber":2,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-the-uncomfortability-2010-s1e2","title":"The Uncomfortability","year":2010,"type":"episode","runtimeSeconds":2687,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Nina's insecurities are revealed when a patient poses an awkward request. Meanwhile Geraldine decides to try online dating, and Jimmy is an unexpected saviour to Cherie and Darcy.","poster":"assets/episode-the-uncomfortability-2010-s1e2-poster.jpg","backdrop":"assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T11:47:42.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Offspring","seasonNumber":1,"episodeNumber":2,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-episode-2-2016-s1e2","title":"Episode 2","year":2016,"type":"episode","runtimeSeconds":2766,"seasonCount":null,"communityRating":8.3,"criticRating":null,"genres":["Biography","Comedy","Drama"],"overview":"Larry falls dangerously ill, and it falls to Theo and his neighbour Sven to put their rudimentary medical skills to the task of saving his life. The family's money worries continue, but Spiros brings them good news\u2026","poster":"assets/episode-episode-2-2016-s1e2-poster.jpg","backdrop":"assets/episode-episode-2-2016-s1e2-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-31T07:29:17.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"The Durrells","seasonNumber":1,"episodeNumber":2,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-remembrance-day-2026-s2e1","title":"Remembrance Day","year":2026,"type":"episode","runtimeSeconds":3822,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Fantasy","Romance"],"overview":"The fiery crosses have been lit across the Highlands; lovers must part, as the call to arms brings friends and enemies home and the Jacobite Rebellion begins in earnest.","poster":"assets/episode-remembrance-day-2026-s2e1-poster.jpg","backdrop":"assets/episode-remembrance-day-2026-s2e1-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-19T03:50:07.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Outlander: Blood of My Blood","seasonNumber":2,"episodeNumber":1,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-one-of-us-2024-s1e7","title":"One of Us","year":2024,"type":"episode","runtimeSeconds":2555,"seasonCount":null,"communityRating":7.7,"criticRating":null,"genres":[],"overview":"Morgan agrees to show Ava around the LAPD, but things quickly take a dangerous turn when the precinct is held hostage by the friends of a recently convicted man. Can Morgan and the detectives outsmart their captors\u2026","poster":"assets/episode-one-of-us-2024-s1e7-poster.jpg","backdrop":"assets/episode-one-of-us-2024-s1e7-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T13:57:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"High Potential","seasonNumber":1,"episodeNumber":7,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-the-breakup-part-2-2001-s1e17","title":"The Breakup, Part 2","year":2001,"type":"episode","runtimeSeconds":2669,"seasonCount":null,"communityRating":7.4,"criticRating":null,"genres":["Comedy","Drama"],"overview":"After recent events, Rory keeps busy, refusing to give in to self-pity. Meanwhile, Stars Hollow's residents rally around her, much to her dismay.","poster":"assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg","backdrop":"assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T11:28:05.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Gilmore Girls","seasonNumber":1,"episodeNumber":17,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-i-heart-ny-2002-s4e18","title":"I Heart NY","year":2002,"type":"episode","runtimeSeconds":1905,"seasonCount":null,"communityRating":6.667,"criticRating":null,"genres":["Comedy","Drama","Romance"],"overview":"Carrie is shocked to find out that Big is moving to Napa, California and plans to give him a New York send-off. Charlotte begins dating another recent divorc\u00e9. Miranda prepares for the new baby as her delivery date\u2026","poster":"assets/episode-i-heart-ny-2002-s4e18-poster.jpg","backdrop":"assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T12:35:14.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Sex and the City","seasonNumber":4,"episodeNumber":18,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}}],"recentlyAdded":[{"id":"tv-yellowjackets-2021","title":"Yellowjackets","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.431,"criticRating":null,"genres":["Drama","Mystery"],"overview":"This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026","poster":"assets/tv-yellowjackets-2021-poster.jpg","backdrop":"assets/tv-yellowjackets-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:31:47.4592756Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.431,"tmdbVotes":1070}},{"id":"tv-only-murders-in-the-building-2021","title":"Only Murders in the Building","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.398,"criticRating":null,"genres":["Comedy","Mystery","Crime"],"overview":"Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.","poster":"assets/tv-only-murders-in-the-building-2021-poster.jpg","backdrop":"assets/tv-only-murders-in-the-building-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:25:58.2163301Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":107113,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.398,"tmdbVotes":2333}},{"id":"tv-big-boys-2022","title":"Big Boys","year":2022,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":7.6,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Two boys from very different ends of the 'spectrum of masculinity' become best friends at Brent University freshers' week; in their first year of uni, they explore, experiment and try to discover themselves, helping\u2026","poster":"assets/tv-big-boys-2022-poster.jpg","backdrop":"assets/tv-big-boys-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:25:58.2156837Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":202851,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":49}},{"id":"tv-all-creatures-great-small-2020","title":"All Creatures Great & Small","year":2020,"type":"tv","runtimeSeconds":3000,"seasonCount":7,"communityRating":8.2,"criticRating":null,"genres":["Drama","Comedy"],"overview":"The heartwarming and humorous adventures of a young country vet in the Yorkshire Dales in the 1930s. A remake of the 1978 series.","poster":"assets/tv-all-creatures-great-small-2020-poster.jpg","backdrop":"assets/tv-all-creatures-great-small-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T20:13:03.4266156Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":108255,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.2,"tmdbVotes":168}},{"id":"movie-the-killing-of-a-sacred-deer-2017","title":"The Killing of a Sacred Deer","year":2017,"type":"movie","runtimeSeconds":7262,"seasonCount":null,"communityRating":7.003,"criticRating":79,"genres":["Drama","Thriller","Mystery"],"overview":"Dr. Steven Murphy is a renowned cardiovascular surgeon who presides over a spotless household with his wife and two children. Lurking at the margins of his idyllic suburban existence is Martin, a fatherless teen who\u2026","poster":"assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg","backdrop":"assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T21:49:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":399057,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":219488,"tmdb":7.001,"tmdbVotes":4640}},{"id":"tv-derry-girls-2018","title":"Derry Girls","year":2018,"type":"tv","runtimeSeconds":1380,"seasonCount":3,"communityRating":8.144,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Amidst the political conflict of Northern Ireland in the 1990s, five secondary school students square off with the universal challenges of being a teenager.","poster":"assets/tv-derry-girls-2018-poster.jpg","backdrop":"assets/tv-derry-girls-2018-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-02T20:01:55.8955537Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":76148,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.138,"tmdbVotes":454}},{"id":"tv-so-you-think-you-can-dance-2008","title":"So You Think You Can Dance","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":9.8,"criticRating":null,"genres":["Family","Reality"],"overview":"So You Think You Can Dance is a joint Belgian-Dutch dance competition show broadcast on RTL 5 and vtm television stations. The first season was broadcast only in the Netherlands, whereas starting with season 2, the\u2026","poster":"assets/tv-so-you-think-you-can-dance-2008-poster.jpg","backdrop":"assets/tv-so-you-think-you-can-dance-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-23T11:11:35.4124418Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":40231,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":9.8,"tmdbVotes":4}},{"id":"tv-hellcats-2010","title":"Hellcats","year":2010,"type":"tv","runtimeSeconds":2460,"seasonCount":1,"communityRating":7.5,"criticRating":null,"genres":["Drama"],"overview":"Pre-law student Marti Perkins' world is flipped upside down when she loses her scholarship, and realizes the only way she can stay in school is by reigniting her dormant teen gymnastic skills to win a place on Lancer\u2026","poster":"assets/tv-hellcats-2010-poster.jpg","backdrop":"assets/tv-hellcats-2010-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T20:39:02.7719318Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":32504,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":116}},{"id":"tv-skymed-2022","title":"SkyMed","year":2022,"type":"tv","runtimeSeconds":2640,"seasonCount":4,"communityRating":6.665,"criticRating":null,"genres":["Drama"],"overview":"Life, death and drama at 20,000 feet. The series weaves together intense character journeys and high-stakes medical rescues, as we follow the triumphs, heartbreaks and tribulations of budding nurses and pilots flying\u2026","poster":"assets/tv-skymed-2022-poster.jpg","backdrop":"assets/tv-skymed-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T19:44:29.2431354Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":133908,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.699,"tmdbVotes":98}},{"id":"tv-love-is-blind-2020","title":"Love Is Blind","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":11,"communityRating":6.261,"criticRating":null,"genres":["Reality"],"overview":"Nick and Vanessa Lachey host this social experiment where single men and women look for love and get engaged, all before meeting in person.","poster":"assets/tv-love-is-blind-2020-poster.jpg","backdrop":"assets/tv-love-is-blind-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-22T17:11:08.2447226Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":99353,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.233,"tmdbVotes":118}},{"id":"tv-the-hunting-wives-2025","title":"The Hunting Wives","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":6.932,"criticRating":null,"genres":["Drama","Mystery"],"overview":"Sophie trades New England for East Texas and falls into a wealthy socialite's magnetic orbit \u2014 where a clique of housewives hide deadly secrets.","poster":"assets/tv-the-hunting-wives-2025-poster.jpg","backdrop":"assets/tv-the-hunting-wives-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T19:42:46.3227892Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":244447,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.964,"tmdbVotes":112}},{"id":"tv-the-white-lotus-2021","title":"The White Lotus","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.597,"criticRating":null,"genres":["Comedy","Drama","Mystery"],"overview":"Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026","poster":"assets/tv-the-white-lotus-2021-poster.jpg","backdrop":"assets/tv-the-white-lotus-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T02:45:52.8742674Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.599,"tmdbVotes":1604}}],"discover":[{"id":"movie-across-the-hall-2009","title":"Across the Hall","year":2009,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Mystery"],"overview":"Convinced that his fiancee is cheating on him, a man follows her to a hotel and calls his best friend to help him avoid a calamity.","poster":"assets/movie-across-the-hall-2009-poster.jpg","backdrop":"assets/movie-across-the-hall-2009-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":21469,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":39,"rtAudienceLabel":"Spilled","imdb":5.8,"imdbVotes":5130,"tmdb":6,"tmdbVotes":129}},{"id":"movie-addicted-2014","title":"Addicted","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.","poster":"assets/movie-addicted-2014-poster.jpg","backdrop":"assets/movie-addicted-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":235271,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":51,"rtAudienceLabel":"Spilled","imdb":5,"imdbVotes":13131,"tmdb":5.928,"tmdbVotes":466}},{"id":"movie-adulterers-2016","title":"Adulterers","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Drama"],"overview":"On a steamy New Orleans afternoon, Sam Deuprey decides to surprise his beautiful wife, Ashley, for their anniversary and comes home early. But Ashley is not alone in the bedroom. She and her lover, Damien are about\u2026","poster":"assets/movie-adulterers-2016-poster.jpg","backdrop":"assets/movie-adulterers-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":376004,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":37,"rtAudienceLabel":"Spilled","imdb":5.1,"imdbVotes":2144,"tmdb":6.2,"tmdbVotes":116}},{"id":"movie-alone-with-her-2006","title":"Alone With Her","year":2006,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Thriller"],"overview":"The harrowing story of a disturbed young man's attempts to win the affections of an unsuspecting young woman. When Doug first sees Amy, he instantly falls for her and begins to watch her every move, going so far as\u2026","poster":"assets/movie-alone-with-her-2006-poster.jpg","backdrop":"assets/movie-alone-with-her-2006-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":18206,"ratings":{"rtCritics":70,"rtCriticsLabel":"Fresh","rtAudience":46,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":4359,"tmdb":5.9,"tmdbVotes":94}},{"id":"movie-awake-2007","title":"Awake","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Crime","Mystery"],"overview":"While undergoing heart surgery, a man experiences a phenomenon called \u2018anesthetic awareness\u2019, which leaves him awake but paralyzed throughout the operation. As various obstacles present themselves, his wife must make\u2026","poster":"assets/movie-awake-2007-poster.jpg","backdrop":"assets/movie-awake-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13483,"ratings":{"rtCritics":23,"rtCriticsLabel":"Rotten","rtAudience":55,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":89389,"tmdb":6.586,"tmdbVotes":1649}},{"id":"movie-black-bear-2020","title":"Black Bear","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026","poster":"assets/movie-black-bear-2020-poster.jpg","backdrop":"assets/movie-black-bear-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":624788,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":18408,"tmdb":6.337,"tmdbVotes":315}},{"id":"movie-butterfly-on-a-wheel-2007","title":"Butterfly on a Wheel","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Thriller"],"overview":"A sociopathic kidnapper methodically pushes a desperate pair of parents to their absolute breaking point.","poster":"assets/movie-butterfly-on-a-wheel-2007-poster.jpg","backdrop":"assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13250,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":35409,"tmdb":6.457,"tmdbVotes":595}},{"id":"movie-careful-what-you-wish-for-2015","title":"Careful What You Wish For","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.","poster":"assets/movie-careful-what-you-wish-for-2015-poster.jpg","backdrop":"assets/movie-careful-what-you-wish-for-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":317744,"ratings":{"rtCritics":17,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":9452,"tmdb":6.129,"tmdbVotes":446}},{"id":"movie-crime-of-passion-1956","title":"Crime of Passion","year":1956,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Thriller"],"overview":"Kathy leaves the newspaper business to marry homicide detective Bill, but is frustrated by his lack of ambition and the banality of life in the suburbs. Her drive to advance Bill's career soon takes her down a\u2026","poster":"assets/movie-crime-of-passion-1956-poster.jpg","backdrop":"assets/movie-crime-of-passion-1956-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1920,"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":34,"rtAudienceLabel":"Spilled","imdb":6.4,"imdbVotes":3750,"tmdb":6.1,"tmdbVotes":57}},{"id":"movie-deep-water-2022","title":"Deep Water","year":2022,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Thriller"],"overview":"A well-to-do husband who allows his wife to have affairs in order to avoid a divorce becomes a prime suspect in the disappearance of her lovers.","poster":"assets/movie-deep-water-2022-poster.jpg","backdrop":"assets/movie-deep-water-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":619979,"ratings":{"rtCritics":35,"rtCriticsLabel":"Rotten","rtAudience":25,"rtAudienceLabel":"Spilled","imdb":5.5,"imdbVotes":74098,"tmdb":5.834,"tmdbVotes":1755}},{"id":"movie-derailed-2005","title":"Derailed","year":2005,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"When two married business executives having an affair are blackmailed by a violent criminal, they are forced to turn the tables on him to save their families.","poster":"assets/movie-derailed-2005-poster.jpg","backdrop":"assets/movie-derailed-2005-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":8999,"ratings":{"rtCritics":20,"rtCriticsLabel":"Rotten","rtAudience":58,"rtAudienceLabel":"Spilled","imdb":6.6,"imdbVotes":79563,"tmdb":6.405,"tmdbVotes":1193}},{"id":"movie-die-my-love-2025","title":"Die My Love","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"After inheriting a remote Montana house, Jackson moves there from New York with his partner Grace, and the couple soon welcome a child. As Jackson becomes increasingly absent and rural isolation sets in, Grace\u2026","poster":"assets/movie-die-my-love-2025-poster.jpg","backdrop":"assets/movie-die-my-love-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1033148,"ratings":{"rtCritics":74,"rtCriticsLabel":"Certified Fresh","rtAudience":45,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":50664,"tmdb":6.1,"tmdbVotes":683}},{"id":"movie-dream-house-2011","title":"Dream House","year":2011,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Drama","Thriller"],"overview":"Publisher Will Atenton quits a lucrative job in New York to relocate his wife, Libby, and their daughters to a quaint town in New England. However, as they settle into their home the Atentons discover that a woman\u2026","poster":"assets/movie-dream-house-2011-poster.jpg","backdrop":"assets/movie-dream-house-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":69668,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":36,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":71386,"tmdb":6.212,"tmdbVotes":1480}},{"id":"movie-dream-lover-1994","title":"Dream Lover","year":1994,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery","Thriller"],"overview":"Not long after they cross paths at an art gallery, architect Ray Reardon and hypnotically sensual Lena are married with children. But as strange incidents occur, Ray begins to realize he may not really know the woman\u2026","poster":"assets/movie-dream-lover-1994-poster.jpg","backdrop":"assets/movie-dream-lover-1994-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":18190,"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":38,"rtAudienceLabel":"Spilled","imdb":6.2,"imdbVotes":7865,"tmdb":6.844,"tmdbVotes":167}},{"id":"movie-drop-2025","title":"Drop","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Thriller"],"overview":"Violet, a widowed mother on her first date in years, arrives at an upscale restaurant where she is relieved that her date, Henry, is more charming and handsome than she expected. But their chemistry begins to curdle\u2026","poster":"assets/movie-drop-2025-poster.jpg","backdrop":"assets/movie-drop-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1249213,"ratings":{"rtCritics":83,"rtCriticsLabel":"Certified Fresh","rtAudience":79,"rtAudienceLabel":"Upright","imdb":6.1,"imdbVotes":61018,"tmdb":6.275,"tmdbVotes":1023}},{"id":"movie-eastern-promises-2007","title":"Eastern Promises","year":2007,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","Crime","Mystery"],"overview":"A Russian teenager living in London dies during childbirth but leaves clues in her diary that could tie her child to a rape involving a violent Russian mob family.","poster":"assets/movie-eastern-promises-2007-poster.jpg","backdrop":"assets/movie-eastern-promises-2007-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":2252,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":281015,"tmdb":7.349,"tmdbVotes":3900}},{"id":"movie-faces-1968","title":"Faces","year":1968,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"Middle-aged suburban husband Richard abruptly tells his wife, Maria, that he wants a divorce. As Richard takes up with a younger woman, Maria enjoys a night on the town with her friends and meets a younger man. As\u2026","poster":"assets/movie-faces-1968-poster.jpg","backdrop":"assets/movie-faces-1968-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":753,"ratings":{"rtCritics":83,"rtCriticsLabel":"Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":12950,"tmdb":7.093,"tmdbVotes":227}},{"id":"movie-fatale-2020","title":"Fatale","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"After a wild one-night stand, successful sports agent Derrick Tyler watches his perfect life slowly disappear when he discovers the sexy and mysterious woman he risked everything for is determined police detective\u2026","poster":"assets/movie-fatale-2020-poster.jpg","backdrop":"assets/movie-fatale-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":549514,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":87,"rtAudienceLabel":"Upright","imdb":5.5,"imdbVotes":16392,"tmdb":6,"tmdbVotes":401}},{"id":"tv-56-days-2026","title":"56 Days","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"When an unidentified body is found in a luxury apartment linked to Oliver Kennedy and his girlfriend Ciara Wyse, Detectives Lee Reardon and Karl Connolly reconstruct the couple's deadly romance across the past 56 days.","poster":"assets/tv-56-days-2026-poster.jpg","backdrop":"assets/tv-56-days-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":241372,"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.388,"tmdbVotes":245}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-absolutely-fabulous-1992","title":"Absolutely Fabulous","year":1992,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"Set in the world of fashion and PR, immature fun-loving mother Edina Monsoon and her best friend Patsy drive Eddie's sensible daughter, Saffron, up the wall with their constant drug abuse and outrageous selfishness.\u2026","poster":"assets/tv-absolutely-fabulous-1992-poster.jpg","backdrop":"assets/tv-absolutely-fabulous-1992-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":4493,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":198}},{"id":"tv-the-afterparty-2022","title":"The Afterparty","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Mystery","Comedy","Crime"],"overview":"Nothing is as it seems in this hilarious murder mystery where each suspect's story is told through a different film genre.","poster":"assets/tv-the-afterparty-2022-poster.jpg","backdrop":"assets/tv-the-afterparty-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":106454,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.071,"tmdbVotes":245}},{"id":"tv-agatha-christie-s-marple-2004","title":"Agatha Christie's Marple","year":2004,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime","Mystery"],"overview":"The adventures of Miss Jane Marple, an elderly spinster living in the quiet little village of St Mary Mead. During her many visits to friends and relatives in other villages, Miss Marple often stumbles upon\u2026","poster":"assets/tv-agatha-christie-s-marple-2004-poster.jpg","backdrop":"assets/tv-agatha-christie-s-marple-2004-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":6127,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":152}},{"id":"tv-all-american-homecoming-2022","title":"All American: Homecoming","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama"],"overview":"Follow the journeys of Simone Hicks, a tennis hopeful from Beverly Hills, and elite baseball player Damon Sims from Chicago, as they navigate life at the prestigious HBCU Bringston University.","poster":"assets/tv-all-american-homecoming-2022-poster.jpg","backdrop":"assets/tv-all-american-homecoming-2022-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":153723,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":70}},{"id":"tv-and-just-like-that-2021","title":"And Just Like That\u2026","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"This new chapter of \u201cSex and the City\u201d follows Carrie, Miranda and Charlotte as they navigate the journey from the complicated reality of life and friendship in their 30s to the even more complicated reality of life\u2026","poster":"assets/tv-and-just-like-that-2021-poster.jpg","backdrop":"assets/tv-and-just-like-that-2021-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":116450,"ratings":{"rtCritics":52,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.211,"tmdbVotes":556}},{"id":"tv-anne-with-an-e-2017","title":"Anne with an E","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Family"],"overview":"A coming-of-age story about an outsider who, against all odds and numerous challenges, fights for love and acceptance and for her place in the world. The series centers on a young orphaned girl in the late 1890s,\u2026","poster":"assets/tv-anne-with-an-e-2017-poster.jpg","backdrop":"assets/tv-anne-with-an-e-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":70785,"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.7,"tmdbVotes":5019}},{"id":"tv-awkward-2011","title":"Awkward.","year":2011,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"An irreverent look at the conflict, chaos and humor that defines teenage life through the eyes of 15-year-old Jenna Hamilton whose life begins to change when a simple accident becomes an epic misunderstanding and is\u2026","poster":"assets/tv-awkward-2011-poster.jpg","backdrop":"assets/tv-awkward-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":39859,"ratings":{"rtCritics":78,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.4,"tmdbVotes":427}},{"id":"tv-the-baby-sitters-club-2020","title":"The Baby-Sitters Club","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Kids","Drama"],"overview":"In this contemporary take on the beloved book series, five best friends launch a baby-sitting business that's big on fun and adventure.","poster":"assets/tv-the-baby-sitters-club-2020-poster.jpg","backdrop":"assets/tv-the-baby-sitters-club-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":100412,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.9,"tmdbVotes":506}},{"id":"tv-ballers-2015","title":"Ballers","year":2015,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Looking at the lives of former and current football players, the show follows former superstar Spencer Strasmore as he gets his life on track in retirement while mentoring other current and former players through the\u2026","poster":"assets/tv-ballers-2015-poster.jpg","backdrop":"assets/tv-ballers-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":62704,"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.809,"tmdbVotes":820}},{"id":"tv-baretta-1975","title":"Baretta","year":1975,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Mystery"],"overview":"Baretta is an American detective television series which ran on ABC from 1975 to 1978. The show was a milder version of a successful 1973\u201374 ABC series, Toma, starring Tony Musante as chameleon-like, real-life New\u2026","poster":"assets/tv-baretta-1975-poster.jpg","backdrop":"assets/tv-baretta-1975-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":3216,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":43}},{"id":"tv-based-on-a-true-story-2023","title":"Based on a True Story","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The lives of a realtor, a plumber and a former tennis star unexpectedly collide, exposing America\u2019s obsession with true crime, murder and the slow-close toilet seat.","poster":"assets/tv-based-on-a-true-story-2023-poster.jpg","backdrop":"assets/tv-based-on-a-true-story-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":197373,"ratings":{"rtCritics":79,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":160}},{"id":"tv-the-beast-in-me-2025","title":"The Beast in Me","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Mystery"],"overview":"A famous author is pulled into a twisted mind game with her rich, powerful new neighbor \u2014 who might be a murderer.","poster":"assets/tv-the-beast-in-me-2025-poster.jpg","backdrop":"assets/tv-the-beast-in-me-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":250504,"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.072,"tmdbVotes":334}},{"id":"tv-big-city-greens-2018","title":"Big City Greens","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Family","Animation","Comedy"],"overview":"The offbeat adventures of 10-year-old Cricket Green, a mischievous and optimistic country boy who moves to the big city with his wildly out of place family \u2013 older sister Tilly, father Bill and Gramma Alice.","poster":"assets/tv-big-city-greens-2018-poster.jpg","backdrop":"assets/tv-big-city-greens-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":80587,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.555,"tmdbVotes":82}},{"id":"tv-the-bold-type-2017","title":"The Bold Type","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Comedy"],"overview":"A glimpse into the outrageous lives of Jane, Kat and Sutton, who are working at the nation's top women's magazine, Scarlet, while navigating their careers, identities and individual voices.","poster":"assets/tv-the-bold-type-2017-poster.jpg","backdrop":"assets/tv-the-bold-type-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":69630,"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":204}},{"id":"tv-brassic-2019","title":"Brassic","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama","Crime"],"overview":"A group of working-class friends finding unconventional ways to win at life in suburban northern England. These lads have dealt, scammed, bribed and conned their way through adolescence, but now, their dealing and\u2026","poster":"assets/tv-brassic-2019-poster.jpg","backdrop":"assets/tv-brassic-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":92621,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.336,"tmdbVotes":263}}],"discoverySource":{"state":"materialized_snapshot","rankKnown":false,"selection":"First 18 alphabetically indexed movie stubs and first 18 series stubs, not ranked feed order","observedAt":"2026-11-04T20:42:12.030108+00:00"}},{"key":"shared","name":"Shared","initial":"S","color":"#9be38f","destinations":{"Home":{"kind":"home"},"Movies":{"kind":"library","collection":"movies"},"Shows":{"kind":"library","collection":"shows"},"Discover":{"kind":"discover","collection":"discover"}},"movies":[{"id":"movie-american-gangster-2007","title":"American Gangster","year":2007,"type":"movie","runtimeSeconds":10546,"seasonCount":null,"communityRating":7.595,"criticRating":81,"genres":["Drama","Crime"],"overview":"Loosely based on the criminal career of Frank Lucas, a gangster from La Grange, North Carolina, who smuggled heroin into the United States on American service planes returning from the Vietnam War, before being\u2026","poster":"assets/movie-american-gangster-2007-poster.jpg","backdrop":"assets/movie-american-gangster-2007-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-08-23T10:22:36.0000000Z","lastPlayedAt":"2026-08-30T21:05:39.7952451Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4982,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":495656,"tmdb":7.599,"tmdbVotes":6350}},{"id":"movie-annie-hall-1977","title":"Annie Hall","year":1977,"type":"movie","runtimeSeconds":5604,"seasonCount":null,"communityRating":7.712,"criticRating":97,"genres":["Comedy","Drama","Romance"],"overview":"New York comedian Alvy Singer falls in love with the ditsy Annie Hall.","poster":"assets/movie-annie-hall-1977-poster.jpg","backdrop":"assets/movie-annie-hall-1977-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:23.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":703,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":293349,"tmdb":7.71,"tmdbVotes":4391}},{"id":"movie-apocalypse-now-1979","title":"Apocalypse Now","year":1979,"type":"movie","runtimeSeconds":8835,"seasonCount":null,"communityRating":8.261,"criticRating":91,"genres":["Drama","War"],"overview":"At the height of the Vietnam war, Captain Benjamin Willard is sent on a dangerous mission that, officially, \"does not exist, nor will it ever exist.\" His goal is to locate - and eliminate - a mysterious Green Beret\u2026","poster":"assets/movie-apocalypse-now-1979-poster.jpg","backdrop":"assets/movie-apocalypse-now-1979-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:25.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":28,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":94,"rtAudienceLabel":"Upright","imdb":8.4,"imdbVotes":781424,"tmdb":8.3,"tmdbVotes":9526}},{"id":"movie-arrival-2016","title":"Arrival","year":2016,"type":"movie","runtimeSeconds":6967,"seasonCount":null,"communityRating":7.636,"criticRating":94,"genres":["Drama","Science Fiction","Mystery"],"overview":"Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.","poster":"assets/movie-arrival-2016-poster.jpg","backdrop":"assets/movie-arrival-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-14T19:54:12.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":329865,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":883621,"tmdb":7.638,"tmdbVotes":20125}},{"id":"movie-avatar-aang-the-last-airbender-2026","title":"Avatar Aang: The Last Airbender","year":2026,"type":"movie","runtimeSeconds":5930,"seasonCount":null,"communityRating":9.147,"criticRating":90,"genres":["Animation","Adventure","Fantasy"],"overview":"Avatar Aang, the world's last Airbender, learns of an ancient power that could save his culture from extinction. With the help of his friends, he embarks on a global quest to find it before it falls into the wrong\u2026","poster":"assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg","backdrop":"assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-25T09:53:09.0000000Z","lastPlayedAt":"2026-08-02T19:22:33.6155383Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":980431,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":99,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":30792,"tmdb":9,"tmdbVotes":1417}},{"id":"movie-bohemian-rhapsody-2018","title":"Bohemian Rhapsody","year":2018,"type":"movie","runtimeSeconds":8077,"seasonCount":null,"communityRating":7.95,"criticRating":60,"genres":["Music","Drama"],"overview":"Singer Freddie Mercury, guitarist Brian May, drummer Roger Taylor and bass guitarist John Deacon take the music world by storm when they form the rock 'n' roll band Queen in 1970. Hit songs become instant classics.\u2026","poster":"assets/movie-bohemian-rhapsody-2018-poster.jpg","backdrop":"assets/movie-bohemian-rhapsody-2018-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":424694,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":655894,"tmdb":7.947,"tmdbVotes":18196}},{"id":"movie-the-boy-and-the-heron-2023","title":"The Boy and the Heron","year":2023,"type":"movie","runtimeSeconds":7437,"seasonCount":null,"communityRating":7.4,"criticRating":96,"genres":["Animation","Fantasy","Drama"],"overview":"While the Second World War rages, the teenage Mahito, haunted by his mother's tragic death, is relocated from Tokyo to the serene rural home of his new stepmother Natsuko, a woman who bears a striking resemblance to\u2026","poster":"assets/movie-the-boy-and-the-heron-2023-poster.jpg","backdrop":"assets/movie-the-boy-and-the-heron-2023-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":508883,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":88,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":152169,"tmdb":7.387,"tmdbVotes":2893}},{"id":"movie-braveheart-1995","title":"Braveheart","year":1995,"type":"movie","runtimeSeconds":10665,"seasonCount":null,"communityRating":7.945,"criticRating":74,"genres":["Drama","History","War"],"overview":"Enraged at the slaughter of Murron, his new bride and childhood love, Scottish warrior William Wallace slays a platoon of the local English lord's soldiers. This leads the village to revolt and, eventually, the\u2026","poster":"assets/movie-braveheart-1995-poster.jpg","backdrop":"assets/movie-braveheart-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T21:24:28.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":197,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":74,"rtCriticsLabel":"Certified Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":8.3,"imdbVotes":1174990,"tmdb":7.947,"tmdbVotes":11584}},{"id":"movie-the-breakfast-club-1985","title":"The Breakfast Club","year":1985,"type":"movie","runtimeSeconds":5833,"seasonCount":null,"communityRating":7.7,"criticRating":87,"genres":["Comedy","Drama"],"overview":"Five high school students from different walks of life endure a Saturday detention under a power-hungry principal. The disparate group includes rebel John, princess Claire, outcast Allison, brainy Brian and Andrew,\u2026","poster":"assets/movie-the-breakfast-club-1985-poster.jpg","backdrop":"assets/movie-the-breakfast-club-1985-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2108,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":478743,"tmdb":7.713,"tmdbVotes":8815}},{"id":"movie-castle-in-the-sky-1986","title":"Castle in the Sky","year":1986,"type":"movie","runtimeSeconds":7474,"seasonCount":null,"communityRating":8,"criticRating":97,"genres":["Adventure","Fantasy","Animation"],"overview":"A young boy and a girl with a magic crystal must race against pirates and foreign agents in a search for a legendary floating castle.","poster":"assets/movie-castle-in-the-sky-1986-poster.jpg","backdrop":"assets/movie-castle-in-the-sky-1986-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:09.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":10515,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":201545,"tmdb":7.972,"tmdbVotes":4868}},{"id":"movie-citizen-kane-1941","title":"Citizen Kane","year":1941,"type":"movie","runtimeSeconds":7173,"seasonCount":null,"communityRating":7.966,"criticRating":99,"genres":["Mystery","Drama"],"overview":"Newspaper magnate Charles Foster Kane is taken from his mother as a boy and made the ward of a rich industrialist. As a result, every well-meaning, tyrannical or self-destructive move he makes for the rest of his\u2026","poster":"assets/movie-citizen-kane-1941-poster.jpg","backdrop":"assets/movie-citizen-kane-1941-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:16.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":15,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Certified Fresh","rtAudience":98,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":498321,"tmdb":7.962,"tmdbVotes":6324}},{"id":"movie-conclave-2024","title":"Conclave","year":2024,"type":"movie","runtimeSeconds":7230,"seasonCount":null,"communityRating":7.19,"criticRating":93,"genres":["Drama","Thriller"],"overview":"After the unexpected death of the Pope, Cardinal Lawrence is tasked with managing the covert and ancient ritual of electing a new one. Sequestered in the Vatican with the Catholic Church\u2019s most powerful leaders until\u2026","poster":"assets/movie-conclave-2024-poster.jpg","backdrop":"assets/movie-conclave-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:41.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":974576,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":290498,"tmdb":7.19,"tmdbVotes":3744}},{"id":"movie-the-dry-2021","title":"The Dry","year":2021,"type":"movie","runtimeSeconds":7037,"seasonCount":null,"communityRating":6.779,"criticRating":null,"genres":["Crime","Thriller","Mystery"],"overview":"Aaron Falk returns to his drought-stricken hometown to attend a tragic funeral. But his return opens a decades-old wound - the unsolved death of a teenage girl.","poster":"assets/movie-the-dry-2021-poster.jpg","backdrop":"assets/movie-the-dry-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":567797,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":41935,"tmdb":6.8,"tmdbVotes":892}},{"id":"movie-easy-a-2010","title":"Easy A","year":2010,"type":"movie","runtimeSeconds":5545,"seasonCount":null,"communityRating":6.8,"criticRating":85,"genres":["Comedy"],"overview":"Olive, an average high school student, sees her below-the-radar existence turn around overnight once she decides to use the school's gossip grapevine to advance her social standing. Now her classmates are turning\u2026","poster":"assets/movie-easy-a-2010-poster.jpg","backdrop":"assets/movie-easy-a-2010-backdrop.jpg","played":true,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-17T19:59:04.0000000Z","lastPlayedAt":"2026-10-17T20:02:51.1083337Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":37735,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":85,"rtCriticsLabel":"Certified Fresh","rtAudience":77,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":448780,"tmdb":6.8,"tmdbVotes":7619}},{"id":"movie-el-camino-a-breaking-bad-movie-2019","title":"El Camino: A Breaking Bad Movie","year":2019,"type":"movie","runtimeSeconds":7362,"seasonCount":null,"communityRating":7.055,"criticRating":92,"genres":["Crime","Drama","Thriller"],"overview":"In the wake of his dramatic escape from captivity, Jesse Pinkman must come to terms with his past in order to forge some kind of future.","poster":"assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg","backdrop":"assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-21T23:31:11.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":559969,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Certified Fresh","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":354488,"tmdb":7.056,"tmdbVotes":5950}},{"id":"movie-emancipation-2022","title":"Emancipation","year":2022,"type":"movie","runtimeSeconds":7949,"seasonCount":null,"communityRating":7.7,"criticRating":45,"genres":["Drama","War","History"],"overview":"Inspired by the gripping true story of a man who would do anything for his family\u2014and for freedom. When Peter, an enslaved man, risks his life to escape and return to his family, he embarks on a perilous journey of\u2026","poster":"assets/movie-emancipation-2022-poster.jpg","backdrop":"assets/movie-emancipation-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T20:37:34.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":715931,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":45,"rtCriticsLabel":"Rotten","rtAudience":58,"rtAudienceLabel":"Spilled","imdb":6.3,"imdbVotes":32079,"tmdb":7.744,"tmdbVotes":1319}},{"id":"movie-fighting-with-my-family-2019","title":"Fighting with My Family","year":2019,"type":"movie","runtimeSeconds":6509,"seasonCount":null,"communityRating":6.84,"criticRating":93,"genres":["Comedy"],"overview":"Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training\u2026","poster":"assets/movie-fighting-with-my-family-2019-poster.jpg","backdrop":"assets/movie-fighting-with-my-family-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T22:10:59.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":445629,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":94576,"tmdb":6.837,"tmdbVotes":1952}},{"id":"movie-force-of-nature-the-dry-2-2024","title":"Force of Nature: The Dry 2","year":2024,"type":"movie","runtimeSeconds":6753,"seasonCount":null,"communityRating":6.3,"criticRating":66,"genres":["Mystery","Thriller"],"overview":"When five women take part in a corporate hiking retreat and only four come out on the other side, Federal Agents Aaron Falk and Carmen Cooper head deep into the Victorian mountain ranges to investigate in the hopes\u2026","poster":"assets/movie-force-of-nature-the-dry-2-2024-poster.jpg","backdrop":"assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:49.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":832262,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":43,"rtAudienceLabel":"Spilled","imdb":5.9,"imdbVotes":10343,"tmdb":6.269,"tmdbVotes":305}},{"id":"movie-free-state-of-jones-2016","title":"Free State of Jones","year":2016,"type":"movie","runtimeSeconds":8376,"seasonCount":null,"communityRating":6.79,"criticRating":48,"genres":["Action","Drama","History"],"overview":"In 1863, Mississippi farmer Newt Knight serves as a medic for the Confederate Army. Opposed to slavery, Knight would rather help the wounded than fight the Union. After his nephew dies in battle, Newt returns home to\u2026","poster":"assets/movie-free-state-of-jones-2016-poster.jpg","backdrop":"assets/movie-free-state-of-jones-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-11T17:56:53.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":316152,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":48,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":68931,"tmdb":6.788,"tmdbVotes":1730}},{"id":"movie-from-up-on-poppy-hill-2011","title":"From Up on Poppy Hill","year":2011,"type":"movie","runtimeSeconds":5482,"seasonCount":null,"communityRating":7.5,"criticRating":87,"genres":["Animation","Drama"],"overview":"Yokohama, 1963. Japan is picking itself up from the devastation of World War II and preparing to host the 1964 Olympics\u2014and the mood is one of both optimism and conflict as the young generation struggles to throw off\u2026","poster":"assets/movie-from-up-on-poppy-hill-2011-poster.jpg","backdrop":"assets/movie-from-up-on-poppy-hill-2011-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-07T14:41:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":83389,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":80,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":60769,"tmdb":7.509,"tmdbVotes":2026}},{"id":"movie-ghost-in-the-shell-1995","title":"Ghost in the Shell","year":1995,"type":"movie","runtimeSeconds":4959,"seasonCount":null,"communityRating":7.929,"criticRating":95,"genres":["Action","Animation","Science Fiction"],"overview":"In the year 2029, the barriers of our world have been broken down by the net and by cybernetics, but this brings new vulnerability to humans in the form of brain-hacking. When a highly-wanted hacker known as 'The\u2026","poster":"assets/movie-ghost-in-the-shell-1995-poster.jpg","backdrop":"assets/movie-ghost-in-the-shell-1995-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:57.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":9323,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":178061,"tmdb":7.928,"tmdbVotes":4130}},{"id":"movie-ghost-in-the-shell-2-innocence-2004","title":"Ghost in the Shell 2: Innocence","year":2004,"type":"movie","runtimeSeconds":5950,"seasonCount":null,"communityRating":7.352,"criticRating":65,"genres":["Animation","Drama","Science Fiction"],"overview":"Cyborg detective Batou is assigned to investigate a series of murders committed by gynoids\u2014doll-like cyborgs, which all malfunctioned, killed, then self-destructed afterwards. The brains of the gynoids initialize in\u2026","poster":"assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg","backdrop":"assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:38.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":12140,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":65,"rtCriticsLabel":"Fresh","rtAudience":80,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":43280,"tmdb":7.356,"tmdbVotes":1049}},{"id":"movie-the-godfather-1972","title":"The Godfather","year":1972,"type":"movie","runtimeSeconds":10603,"seasonCount":null,"communityRating":8.687,"criticRating":97,"genres":["Drama","Crime"],"overview":"Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son,\u2026","poster":"assets/movie-the-godfather-1972-poster.jpg","backdrop":"assets/movie-the-godfather-1972-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:12.0000000Z","lastPlayedAt":"2026-09-18T19:24:44.9855867Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":238,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":98,"rtAudienceLabel":"Upright","imdb":9.2,"imdbVotes":2262024,"tmdb":8.688,"tmdbVotes":23907}},{"id":"movie-the-godfather-part-ii-1974","title":"The Godfather Part II","year":1974,"type":"movie","runtimeSeconds":12031,"seasonCount":null,"communityRating":8.573,"criticRating":96,"genres":["Drama","Crime"],"overview":"In the continuing saga of the Corleone crime family, a young Vito Corleone grows up in Sicily and in 1910s New York. In the 1950s, Michael Corleone attempts to expand the family business into Las Vegas, Hollywood and\u2026","poster":"assets/movie-the-godfather-part-ii-1974-poster.jpg","backdrop":"assets/movie-the-godfather-part-ii-1974-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:18.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":240,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":97,"rtAudienceLabel":"Upright","imdb":9,"imdbVotes":1521308,"tmdb":8.571,"tmdbVotes":14559}},{"id":"movie-the-godfather-part-iii-1990","title":"The Godfather Part III","year":1990,"type":"movie","runtimeSeconds":9476,"seasonCount":null,"communityRating":7.416,"criticRating":67,"genres":["Crime","Drama","Thriller"],"overview":"In the midst of trying to legitimize his business dealings in 1979 New York and Italy, aging mafia don, Michael Corleone seeks forgiveness for his sins while taking a young protege under his wing.","poster":"assets/movie-the-godfather-part-iii-1990-poster.jpg","backdrop":"assets/movie-the-godfather-part-iii-1990-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:29.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":242,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":68,"rtCriticsLabel":"Fresh","rtAudience":68,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":462448,"tmdb":7.413,"tmdbVotes":7159}},{"id":"movie-gosford-park-2001","title":"Gosford Park","year":2001,"type":"movie","runtimeSeconds":8247,"seasonCount":null,"communityRating":6.815,"criticRating":87,"genres":["Drama","Mystery","Thriller"],"overview":"In 1930s England, a group of pretentious rich and famous gather together for a weekend of relaxation at a hunting resort. But when a murder occurs, each one of these interesting characters becomes a suspect.","poster":"assets/movie-gosford-park-2001-poster.jpg","backdrop":"assets/movie-gosford-park-2001-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:49.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":5279,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":78,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":103883,"tmdb":6.822,"tmdbVotes":1317}},{"id":"movie-grave-of-the-fireflies-1988","title":"Grave of the Fireflies","year":1988,"type":"movie","runtimeSeconds":5313,"seasonCount":null,"communityRating":8.45,"criticRating":100,"genres":["Animation","Drama","War"],"overview":"In the final months of World War II, 14-year-old Seita and his sister Setsuko are orphaned when their mother is killed during an air raid in Kobe, Japan. After a falling out with their aunt, they move into an\u2026","poster":"assets/movie-grave-of-the-fireflies-1988-poster.jpg","backdrop":"assets/movie-grave-of-the-fireflies-1988-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:14.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":12477,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Certified Fresh","rtAudience":95,"rtAudienceLabel":"Upright","imdb":8.5,"imdbVotes":399802,"tmdb":8.45,"tmdbVotes":6938}},{"id":"movie-the-great-gatsby-2013","title":"The Great Gatsby","year":2013,"type":"movie","runtimeSeconds":8525,"seasonCount":null,"communityRating":7.346,"criticRating":49,"genres":["Drama","Romance"],"overview":"Nick Carraway arrives in New York in 1922, an era of loose morals, glittering jazz and bootleg kings. Chasing his own American Dream, Nick encounters the mysterious millionaire Gatsby and his bewitching cousin Daisy.\u2026","poster":"assets/movie-the-great-gatsby-2013-poster.jpg","backdrop":"assets/movie-the-great-gatsby-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:50.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":64682,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":48,"rtCriticsLabel":"Rotten","rtAudience":67,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":651882,"tmdb":7.346,"tmdbVotes":13290}},{"id":"movie-the-hours-2002","title":"The Hours","year":2002,"type":"movie","runtimeSeconds":6887,"seasonCount":null,"communityRating":7.25,"criticRating":79,"genres":["Drama"],"overview":"The story of three women searching for more potent, meaningful lives. Each is alive at a different time and place, all are linked by their yearnings and their fears. Their stories intertwine, and finally come\u2026","poster":"assets/movie-the-hours-2002-poster.jpg","backdrop":"assets/movie-the-hours-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:27.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":590,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":148367,"tmdb":7.249,"tmdbVotes":1931}},{"id":"movie-howl-s-moving-castle-2004","title":"Howl's Moving Castle","year":2004,"type":"movie","runtimeSeconds":7149,"seasonCount":null,"communityRating":8.4,"criticRating":88,"genres":["Fantasy","Animation","Adventure"],"overview":"Sophie, a young milliner, is turned into an elderly woman by a witch who enters her shop and curses her. She encounters a wizard named Howl and gets caught up in his resistance to fighting for the king.","poster":"assets/movie-howl-s-moving-castle-2004-poster.jpg","backdrop":"assets/movie-howl-s-moving-castle-2004-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:47.0000000Z","lastPlayedAt":"2026-05-21T18:46:08.0000000Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4935,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":524520,"tmdb":8.391,"tmdbVotes":11642}},{"id":"movie-in-the-heights-2021","title":"In the Heights","year":2021,"type":"movie","runtimeSeconds":8579,"seasonCount":null,"communityRating":7.111,"criticRating":null,"genres":["Drama","Romance"],"overview":"The story of Usnavi, a bodega owner who has mixed feelings about closing his store and retiring to the Dominican Republic or staying in Washington Heights.","poster":"assets/movie-in-the-heights-2021-poster.jpg","backdrop":"assets/movie-in-the-heights-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:11.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":467909,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":94,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":56513,"tmdb":7.104,"tmdbVotes":826}},{"id":"movie-jennifer-s-body-2009","title":"Jennifer's Body","year":2009,"type":"movie","runtimeSeconds":6450,"seasonCount":null,"communityRating":6.012,"criticRating":47,"genres":["Horror","Comedy"],"overview":"Jennifer, a gorgeous, seductive cheerleader takes evil to a whole new level after she's possessed by a sinister demon. Now it's up to her best friend to stop Jennifer's reign of terror before it's too late.","poster":"assets/movie-jennifer-s-body-2009-poster.jpg","backdrop":"assets/movie-jennifer-s-body-2009-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-30T03:37:04.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":19994,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":47,"rtCriticsLabel":"Rotten","rtAudience":36,"rtAudienceLabel":"Spilled","imdb":5.6,"imdbVotes":179710,"tmdb":6.013,"tmdbVotes":4481}},{"id":"movie-kiki-s-delivery-service-1989","title":"Kiki's Delivery Service","year":1989,"type":"movie","runtimeSeconds":6183,"seasonCount":null,"communityRating":7.839,"criticRating":null,"genres":["Animation","Family","Fantasy"],"overview":"A young witch, on her mandatory year of independent life, finds fitting into a new community difficult while she supports herself by running an air courier service.","poster":"assets/movie-kiki-s-delivery-service-1989-poster.jpg","backdrop":"assets/movie-kiki-s-delivery-service-1989-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":16859,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":98,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":192477,"tmdb":7.839,"tmdbVotes":4811}},{"id":"movie-the-last-of-the-mohicans-1992","title":"The Last of the Mohicans","year":1992,"type":"movie","runtimeSeconds":6874,"seasonCount":null,"communityRating":7.388,"criticRating":88,"genres":["History","War","Drama"],"overview":"In war-torn colonial America, in the midst of a bloody battle between British, the French and Native American allies, the aristocratic daughter of a British Colonel and her party are captured by a group of Huron\u2026","poster":"assets/movie-the-last-of-the-mohicans-1992-poster.jpg","backdrop":"assets/movie-the-last-of-the-mohicans-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T20:59:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":9361,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Certified Fresh","rtAudience":88,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":210791,"tmdb":7.39,"tmdbVotes":3593}},{"id":"movie-the-last-samurai-2003","title":"The Last Samurai","year":2003,"type":"movie","runtimeSeconds":9251,"seasonCount":null,"communityRating":7.61,"criticRating":65,"genres":["Drama","Action","War"],"overview":"Nathan Algren is an American hired to instruct the Japanese army in the ways of modern warfare, which finds him learning to respect the samurai and the honorable principles that rule them. Pressed to destroy the\u2026","poster":"assets/movie-the-last-samurai-2003-poster.jpg","backdrop":"assets/movie-the-last-samurai-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T05:25:10.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":616,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":512982,"tmdb":7.614,"tmdbVotes":7887}},{"id":"movie-lemony-snicket-s-a-series-of-unfortunate-events-2004","title":"Lemony Snicket's A Series of Unfortunate Events","year":2004,"type":"movie","runtimeSeconds":6479,"seasonCount":null,"communityRating":6.8,"criticRating":72,"genres":["Adventure","Comedy","Family"],"overview":"Three wealthy children's parents are killed in a fire. When they are sent to a distant relative, they find out that he is plotting to kill them and seize their fortune.","poster":"assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg","backdrop":"assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:09.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":11774,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":233636,"tmdb":6.82,"tmdbVotes":5447}},{"id":"movie-the-lighthouse-2019","title":"The Lighthouse","year":2019,"type":"movie","runtimeSeconds":6549,"seasonCount":null,"communityRating":7.463,"criticRating":90,"genres":["Drama","Fantasy","Thriller"],"overview":"Two lighthouse keepers try to maintain their sanity while living on a remote and mysterious New England island in the 1890s.","poster":"assets/movie-the-lighthouse-2019-poster.jpg","backdrop":"assets/movie-the-lighthouse-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:01.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":503919,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":312344,"tmdb":7.46,"tmdbVotes":5895}},{"id":"movie-locke-2014","title":"Locke","year":2014,"type":"movie","runtimeSeconds":5090,"seasonCount":null,"communityRating":6.888,"criticRating":91,"genres":["Drama","Thriller"],"overview":"Ivan Locke has worked hard to craft a good life for himself. Tonight, that life will collapse around him. On the eve of the biggest challenge of his career, Ivan receives a phone call that sets in motion a series of\u2026","poster":"assets/movie-locke-2014-poster.jpg","backdrop":"assets/movie-locke-2014-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:38:48.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":210479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":171265,"tmdb":6.889,"tmdbVotes":3088}},{"id":"movie-the-matrix-1999","title":"The Matrix","year":1999,"type":"movie","runtimeSeconds":8179,"seasonCount":null,"communityRating":8.259,"criticRating":83,"genres":["Action","Science Fiction"],"overview":"Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.","poster":"assets/movie-the-matrix-1999-poster.jpg","backdrop":"assets/movie-the-matrix-1999-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:18:12.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":603,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":83,"rtCriticsLabel":"Certified Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":8.7,"imdbVotes":2289808,"tmdb":8.262,"tmdbVotes":29153}},{"id":"movie-the-matrix-reloaded-2003","title":"The Matrix Reloaded","year":2003,"type":"movie","runtimeSeconds":8298,"seasonCount":null,"communityRating":7.085,"criticRating":74,"genres":["Adventure","Action","Thriller"],"overview":"The Resistance builds in numbers as humans are freed from the Matrix and brought to the city of Zion. Neo discovers his superpowers, including the ability to see the code inside the Matrix. With machine sentinels\u2026","poster":"assets/movie-the-matrix-reloaded-2003-poster.jpg","backdrop":"assets/movie-the-matrix-reloaded-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":604,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":73,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":678013,"tmdb":7.087,"tmdbVotes":12504}},{"id":"movie-the-matrix-resurrections-2021","title":"The Matrix Resurrections","year":2021,"type":"movie","runtimeSeconds":8875,"seasonCount":null,"communityRating":6.303,"criticRating":63,"genres":["Science Fiction","Action","Adventure"],"overview":"Plagued by strange memories, Neo's life takes an unexpected turn when he finds himself back inside the Matrix.","poster":"assets/movie-the-matrix-resurrections-2021-poster.jpg","backdrop":"assets/movie-the-matrix-resurrections-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":624860,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":63,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":307780,"tmdb":6.294,"tmdbVotes":6842}},{"id":"movie-the-matrix-revolutions-2003","title":"The Matrix Revolutions","year":2003,"type":"movie","runtimeSeconds":7756,"seasonCount":null,"communityRating":6.758,"criticRating":33,"genres":["Adventure","Action","Thriller"],"overview":"The human city of Zion defends itself against the massive invasion of the machines as Neo fights to end the war at another front while also opposing the rogue Agent Smith.","poster":"assets/movie-the-matrix-revolutions-2003-poster.jpg","backdrop":"assets/movie-the-matrix-revolutions-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:25:47.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":605,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":34,"rtCriticsLabel":"Rotten","rtAudience":60,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":581553,"tmdb":6.762,"tmdbVotes":11292}},{"id":"movie-mercy-2026","title":"Mercy","year":2026,"type":"movie","runtimeSeconds":5968,"seasonCount":null,"communityRating":7.077,"criticRating":null,"genres":["Science Fiction","Thriller","Crime"],"overview":"In the near future, a detective stands on trial accused of murdering his wife. He has ninety minutes to prove his innocence to the advanced AI Judge he once championed, before it determines his fate.","poster":"assets/movie-mercy-2026-poster.jpg","backdrop":"assets/movie-mercy-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:40.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1236153,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":24,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6.2,"imdbVotes":83878,"tmdb":7.054,"tmdbVotes":1812}},{"id":"movie-normal-2026","title":"Normal","year":2026,"type":"movie","runtimeSeconds":5514,"seasonCount":null,"communityRating":7.267,"criticRating":77,"genres":["Action","Crime","Thriller"],"overview":"An unassuming substitute sheriff with a troubled past responds to a bank robbery in a small, sleepy town and unknowingly uncovers something far more explosive.","poster":"assets/movie-normal-2026-poster.jpg","backdrop":"assets/movie-normal-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-08-21T08:33:18.0000000Z","lastPlayedAt":"2026-09-24T23:07:52.1161581Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1242332,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":78,"rtCriticsLabel":"Certified Fresh","rtAudience":75,"rtAudienceLabel":"Upright","imdb":6.3,"imdbVotes":37501,"tmdb":7.241,"tmdbVotes":786}},{"id":"movie-outlaw-king-2018","title":"Outlaw King","year":2018,"type":"movie","runtimeSeconds":7297,"seasonCount":null,"communityRating":6.829,"criticRating":63,"genres":["Action","History","War"],"overview":"Forced into exile by the English after being crowned King of Scotland, legendary warrior Robert the Bruce fights to reclaim the throne.","poster":"assets/movie-outlaw-king-2018-poster.jpg","backdrop":"assets/movie-outlaw-king-2018-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:32.0000000Z","lastPlayedAt":"2026-07-26T22:35:35.0000000Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":454227,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":63,"rtCriticsLabel":"Fresh","rtAudience":69,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":89876,"tmdb":6.844,"tmdbVotes":1780}},{"id":"movie-panic-room-2002","title":"Panic Room","year":2002,"type":"movie","runtimeSeconds":6717,"seasonCount":null,"communityRating":6.794,"criticRating":76,"genres":["Crime","Drama","Thriller"],"overview":"Trapped in their New York brownstone's panic room, a hidden chamber built as a sanctuary in the event of break-ins, newly divorced Meg Altman and her young daughter Sarah play a deadly game of cat-and-mouse with\u2026","poster":"assets/movie-panic-room-2002-poster.jpg","backdrop":"assets/movie-panic-room-2002-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-19T20:50:01.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4547,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":76,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":317468,"tmdb":6.796,"tmdbVotes":5653}},{"id":"movie-the-patriot-2000","title":"The Patriot","year":2000,"type":"movie","runtimeSeconds":9887,"seasonCount":null,"communityRating":7.214,"criticRating":61,"genres":["Drama","History","War"],"overview":"After proving himself on the field of battle in the French and Indian War, Benjamin Martin wants nothing more to do with such things, preferring the simple life of a farmer. But when his son Gabriel enlists in the\u2026","poster":"assets/movie-the-patriot-2000-poster.jpg","backdrop":"assets/movie-the-patriot-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T20:50:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2024,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":61,"rtCriticsLabel":"Fresh","rtAudience":81,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":317639,"tmdb":7.215,"tmdbVotes":4535}},{"id":"movie-peaky-blinders-the-immortal-man-2026","title":"Peaky Blinders: The Immortal Man","year":2026,"type":"movie","runtimeSeconds":6844,"seasonCount":null,"communityRating":7.187,"criticRating":90,"genres":["Crime","Drama"],"overview":"After his estranged son gets embroiled in a Nazi plot, self-exiled gangster Tommy Shelby must return to Birmingham to save his family \u2014 and his nation.","poster":"assets/movie-peaky-blinders-the-immortal-man-2026-poster.jpg","backdrop":"assets/movie-peaky-blinders-the-immortal-man-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:01.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":875828,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":92130,"tmdb":7.189,"tmdbVotes":1304}},{"id":"movie-ponyo-2008","title":"Ponyo","year":2008,"type":"movie","runtimeSeconds":6530,"seasonCount":null,"communityRating":7.763,"criticRating":91,"genres":["Animation","Fantasy","Family"],"overview":"When Sosuke, a young boy who lives on a clifftop overlooking the sea, rescues a stranded goldfish named Ponyo, he discovers more than he bargained for. Ponyo is a curious, energetic young creature who yearns to be\u2026","poster":"assets/movie-ponyo-2008-poster.jpg","backdrop":"assets/movie-ponyo-2008-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:53.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":12429,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.6,"imdbVotes":190294,"tmdb":7.759,"tmdbVotes":5050}},{"id":"movie-porco-rosso-1992","title":"Porco Rosso","year":1992,"type":"movie","runtimeSeconds":5600,"seasonCount":null,"communityRating":7.794,"criticRating":96,"genres":["Animation","Adventure","Fantasy"],"overview":"In Italy in the 1930s, sky pirates in biplanes terrorize wealthy cruise ships as they sail the Adriatic Sea. The only pilot brave enough to stop the scourge is the mysterious Porco Rosso, a former World War I flying\u2026","poster":"assets/movie-porco-rosso-1992-poster.jpg","backdrop":"assets/movie-porco-rosso-1992-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":11621,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":119396,"tmdb":7.789,"tmdbVotes":3879}},{"id":"movie-princess-mononoke-1997","title":"Princess Mononoke","year":1997,"type":"movie","runtimeSeconds":8003,"seasonCount":null,"communityRating":8.321,"criticRating":93,"genres":["Adventure","Fantasy","Animation"],"overview":"Ashitaka, a prince of the disappearing Emishi people, is cursed by a demonized boar god and must journey to the west to find a cure. Along the way, he encounters San, a young human woman fighting to protect the\u2026","poster":"assets/movie-princess-mononoke-1997-poster.jpg","backdrop":"assets/movie-princess-mononoke-1997-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:28.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":128,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":99,"rtAudienceLabel":"Upright","imdb":8.3,"imdbVotes":493735,"tmdb":8.321,"tmdbVotes":9438}},{"id":"movie-prisoners-2013","title":"Prisoners","year":2013,"type":"movie","runtimeSeconds":9192,"seasonCount":null,"communityRating":8.106,"criticRating":81,"genres":["Drama","Thriller","Crime"],"overview":"After his six-year-old daughter is abducted, a desperate father takes justice into his own hands when he loses faith in the detective leading the investigation, setting both men on a collision course as the search\u2026","poster":"assets/movie-prisoners-2013-poster.jpg","backdrop":"assets/movie-prisoners-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:33.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":146233,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":82,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":963062,"tmdb":8.105,"tmdbVotes":13722}},{"id":"movie-racing-stripes-2005","title":"Racing Stripes","year":2005,"type":"movie","runtimeSeconds":6117,"seasonCount":null,"communityRating":5.945,"criticRating":35,"genres":["Comedy","Family"],"overview":"Shattered illusions are hard to repair -- especially for a good-hearted zebra named Stripes who's spent his life on a Kentucky farm amidst the sorely mistaken notion that he's a debonair thoroughbred. Once he faces\u2026","poster":"assets/movie-racing-stripes-2005-poster.jpg","backdrop":"assets/movie-racing-stripes-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:02.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":6439,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":35,"rtCriticsLabel":"Rotten","rtAudience":42,"rtAudienceLabel":"Spilled","imdb":5.3,"imdbVotes":19569,"tmdb":5.962,"tmdbVotes":705}},{"id":"movie-requiem-for-a-dream-2000","title":"Requiem for a Dream","year":2000,"type":"movie","runtimeSeconds":6086,"seasonCount":null,"communityRating":8.005,"criticRating":80,"genres":["Crime","Drama"],"overview":"The drug-induced utopias of four Coney Island residents are shattered when their addictions run deep.","poster":"assets/movie-requiem-for-a-dream-2000-poster.jpg","backdrop":"assets/movie-requiem-for-a-dream-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":641,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":8.3,"imdbVotes":989090,"tmdb":8,"tmdbVotes":11454}},{"id":"movie-rogue-one-a-star-wars-story-2016","title":"Rogue One: A Star Wars Story","year":2016,"type":"movie","runtimeSeconds":8038,"seasonCount":null,"communityRating":7.51,"criticRating":84,"genres":["Action","Adventure","Science Fiction"],"overview":"A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.","poster":"assets/movie-rogue-one-a-star-wars-story-2016-poster.jpg","backdrop":"assets/movie-rogue-one-a-star-wars-story-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":330459,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":768365,"tmdb":7.512,"tmdbVotes":16937}},{"id":"movie-secondhand-lions-2003","title":"Secondhand Lions","year":2003,"type":"movie","runtimeSeconds":6547,"seasonCount":null,"communityRating":7.237,"criticRating":60,"genres":["Family","Comedy","Drama"],"overview":"The comedic adventures of an introverted boy left on the doorstep of a pair of reluctant, eccentric great-uncles, whose exotic remembrances stir the boy's spirit and re-ignite the men's lives.","poster":"assets/movie-secondhand-lions-2003-poster.jpg","backdrop":"assets/movie-secondhand-lions-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-12T07:16:13.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":13156,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":59,"rtCriticsLabel":"Rotten","rtAudience":84,"rtAudienceLabel":"Upright","imdb":7.5,"imdbVotes":66911,"tmdb":7.242,"tmdbVotes":872}},{"id":"movie-the-secret-life-of-pets-2-2019","title":"The Secret Life of Pets 2","year":2019,"type":"movie","runtimeSeconds":5164,"seasonCount":null,"communityRating":6.95,"criticRating":60,"genres":["Animation","Comedy","Family"],"overview":"Max the terrier must cope with some major life changes when his owner gets married and has a baby. When the family takes a trip to the countryside, nervous Max has numerous run-ins with canine-intolerant cows,\u2026","poster":"assets/movie-the-secret-life-of-pets-2-2019-poster.jpg","backdrop":"assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-05T07:00:53.0000000Z","lastPlayedAt":"2026-09-17T18:07:00.7024231Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":412117,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":6.4,"imdbVotes":80464,"tmdb":6.948,"tmdbVotes":3450}},{"id":"movie-self-reliance-2024","title":"Self Reliance","year":2024,"type":"movie","runtimeSeconds":5461,"seasonCount":null,"communityRating":5.888,"criticRating":73,"genres":["Comedy","Thriller"],"overview":"When a man is offered a million dollars to play a game in which hunters try to kill him, he thinks he has found the perfect loophole: they can only attack when he\u2019s alone. His only problem is that none of his friends\u2026","poster":"assets/movie-self-reliance-2024-poster.jpg","backdrop":"assets/movie-self-reliance-2024-backdrop.jpg","played":false,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:00.0000000Z","lastPlayedAt":"2026-10-02T19:59:16.9286868Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1072876,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":73,"rtCriticsLabel":"Certified Fresh","rtAudience":56,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":23771,"tmdb":5.866,"tmdbVotes":346}},{"id":"movie-serpico-1973","title":"Serpico","year":1973,"type":"movie","runtimeSeconds":7805,"seasonCount":null,"communityRating":7.5,"criticRating":93,"genres":["Crime","Drama","History"],"overview":"New York cop Frank Serpico blows the whistle on the rampant corruption in the force only to have his comrades turn against him.","poster":"assets/movie-serpico-1973-poster.jpg","backdrop":"assets/movie-serpico-1973-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T21:45:02.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":9040,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":88,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":149750,"tmdb":7.525,"tmdbVotes":2323}},{"id":"movie-shaft-2000","title":"Shaft","year":2000,"type":"movie","runtimeSeconds":5960,"seasonCount":null,"communityRating":6.026,"criticRating":67,"genres":["Action","Adventure","Crime"],"overview":"New York police detective John Shaft arrests Walter Wade Jr. for a racially motivated slaying. But the only eyewitness disappears, and Wade jumps bail for Switzerland. Two years later Wade returns to face trial,\u2026","poster":"assets/movie-shaft-2000-poster.jpg","backdrop":"assets/movie-shaft-2000-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T21:30:40.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":479,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":68,"rtCriticsLabel":"Fresh","rtAudience":40,"rtAudienceLabel":"Spilled","imdb":6,"imdbVotes":86257,"tmdb":6.024,"tmdbVotes":1531}},{"id":"movie-a-silent-voice-the-movie-2016","title":"A Silent Voice: The Movie","year":2016,"type":"movie","runtimeSeconds":7804,"seasonCount":null,"communityRating":8.398,"criticRating":null,"genres":["Animation","Drama","Romance"],"overview":"Shouya Ishida starts bullying the new girl in class, Shouko Nishimiya, because she is deaf. But as the teasing continues, the rest of the class starts to turn on Shouya for his lack of compassion. When they leave\u2026","poster":"assets/movie-a-silent-voice-the-movie-2016-poster.jpg","backdrop":"assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T17:39:46.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":378064,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":94,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":151926,"tmdb":8.396,"tmdbVotes":4764}},{"id":"movie-sinbad-legend-of-the-seven-seas-2003","title":"Sinbad: Legend of the Seven Seas","year":2003,"type":"movie","runtimeSeconds":5143,"seasonCount":null,"communityRating":7.067,"criticRating":44,"genres":["Action","Adventure","Animation"],"overview":"The sailor of legend is framed by the goddess Eris for the theft of the Book of Peace, and must travel to her realm at the end of the world to retrieve it and save the life of his childhood friend Prince Proteus.","poster":"assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg","backdrop":"assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:26:25.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":14411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":57,"rtAudienceLabel":"Spilled","imdb":6.8,"imdbVotes":65298,"tmdb":7.071,"tmdbVotes":2323}},{"id":"movie-sleeping-with-other-people-2015","title":"Sleeping with Other People","year":2015,"type":"movie","runtimeSeconds":6076,"seasonCount":null,"communityRating":6.274,"criticRating":64,"genres":["Comedy","Romance"],"overview":"Can two serial cheaters get a second chance at love? After a one-night stand in college, New Yorkers Lainey and Jake meet by chance twelve years later and discover they each have the same problem: because of their\u2026","poster":"assets/movie-sleeping-with-other-people-2015-poster.jpg","backdrop":"assets/movie-sleeping-with-other-people-2015-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:42.0000000Z","lastPlayedAt":"2026-07-17T17:50:44.0000000Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":288036,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":56,"rtAudienceLabel":"Spilled","imdb":6.5,"imdbVotes":52275,"tmdb":6.275,"tmdbVotes":1020}},{"id":"movie-snowpiercer-2013","title":"Snowpiercer","year":2013,"type":"movie","runtimeSeconds":7573,"seasonCount":null,"communityRating":6.909,"criticRating":94,"genres":["Action","Science Fiction","Drama"],"overview":"In a future where a failed global-warming experiment kills off most life on the planet, a class system evolves aboard the Snowpiercer; a train that travels around the globe via a perpetual-motion engine.","poster":"assets/movie-snowpiercer-2013-poster.jpg","backdrop":"assets/movie-snowpiercer-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:27:07.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":110415,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Certified Fresh","rtAudience":72,"rtAudienceLabel":"Upright","imdb":7.1,"imdbVotes":426976,"tmdb":6.9,"tmdbVotes":10807}},{"id":"movie-spider-man-brand-new-day-2026","title":"Spider-Man: Brand New Day","year":2026,"type":"movie","runtimeSeconds":8701,"seasonCount":null,"communityRating":8.374,"criticRating":90,"genres":["Science Fiction","Action","Adventure"],"overview":"Fighting crime full-time as Spider-Man in a world that doesn't remember him\u2014and the pressure of seeing his old friends move on without him\u2014sparks a change in Peter Parker he may not have the power to control. But\u2026","poster":"assets/movie-spider-man-brand-new-day-2026-poster.jpg","backdrop":"assets/movie-spider-man-brand-new-day-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:14:27.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":969681,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":97,"rtAudienceLabel":"Upright","imdb":7.9,"imdbVotes":366886,"tmdb":8.5,"tmdbVotes":6537}},{"id":"movie-spider-man-far-from-home-2019","title":"Spider-Man: Far From Home","year":2019,"type":"movie","runtimeSeconds":7762,"seasonCount":null,"communityRating":7.389,"criticRating":91,"genres":["Action","Adventure","Science Fiction"],"overview":"Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and\u2026","poster":"assets/movie-spider-man-far-from-home-2019-poster.jpg","backdrop":"assets/movie-spider-man-far-from-home-2019-backdrop.jpg","played":true,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:25:47.0000000Z","lastPlayedAt":"2026-10-10T17:50:35.6193119Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":429617,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":95,"rtAudienceLabel":"Upright","imdb":7.3,"imdbVotes":682887,"tmdb":7.387,"tmdbVotes":17847}},{"id":"movie-spider-man-homecoming-2017","title":"Spider-Man: Homecoming","year":2017,"type":"movie","runtimeSeconds":8008,"seasonCount":null,"communityRating":7.333,"criticRating":92,"genres":["Action","Adventure","Science Fiction"],"overview":"Following the events of Captain America: Civil War, Peter Parker, with the help of his mentor Tony Stark, tries to balance his life as an ordinary high school student in Queens, New York City, with fighting crime as\u2026","poster":"assets/movie-spider-man-homecoming-2017-poster.jpg","backdrop":"assets/movie-spider-man-homecoming-2017-backdrop.jpg","played":true,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:33:13.0000000Z","lastPlayedAt":"2026-10-10T20:31:23.4444213Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":315635,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Certified Fresh","rtAudience":87,"rtAudienceLabel":"Upright","imdb":7.4,"imdbVotes":840974,"tmdb":7.332,"tmdbVotes":24183}},{"id":"movie-spider-man-no-way-home-2021","title":"Spider-Man: No Way Home","year":2021,"type":"movie","runtimeSeconds":8890,"seasonCount":null,"communityRating":7.947,"criticRating":93,"genres":["Action","Adventure","Science Fiction"],"overview":"Peter Parker is unmasked and no longer able to separate his normal life from the high-stakes of being a super-hero. When he asks for help from Doctor Strange the stakes become even more dangerous, forcing him to\u2026","poster":"assets/movie-spider-man-no-way-home-2021-poster.jpg","backdrop":"assets/movie-spider-man-no-way-home-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-10T14:21:05.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":634649,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":97,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":1082660,"tmdb":7.947,"tmdbVotes":23505}},{"id":"movie-spirited-away-2001","title":"Spirited Away","year":2001,"type":"movie","runtimeSeconds":7472,"seasonCount":null,"communityRating":8.535,"criticRating":96,"genres":["Animation","Family","Fantasy"],"overview":"A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family.","poster":"assets/movie-spirited-away-2001-poster.jpg","backdrop":"assets/movie-spirited-away-2001-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:42.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":129,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":99,"rtAudienceLabel":"Upright","imdb":8.6,"imdbVotes":987658,"tmdb":8.536,"tmdbVotes":19176}},{"id":"movie-the-substance-2024","title":"The Substance","year":2024,"type":"movie","runtimeSeconds":8531,"seasonCount":null,"communityRating":7.135,"criticRating":89,"genres":["Horror","Science Fiction"],"overview":"A fading celebrity decides to use a black market drug, a cell-replicating substance that temporarily creates a younger, better version of herself.","poster":"assets/movie-the-substance-2024-poster.jpg","backdrop":"assets/movie-the-substance-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:22.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":933260,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":76,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":466542,"tmdb":7.13,"tmdbVotes":6746}},{"id":"movie-the-tale-of-the-princess-kaguya-2013","title":"The Tale of The Princess Kaguya","year":2013,"type":"movie","runtimeSeconds":8218,"seasonCount":null,"communityRating":8.1,"criticRating":100,"genres":["Animation","Drama","Fantasy"],"overview":"Found inside a shining stalk of bamboo by an old bamboo cutter and his wife, a tiny girl grows rapidly into an exquisite young lady. The mysterious young princess enthrals all who encounter her. But, ultimately, she\u2026","poster":"assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg","backdrop":"assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-07T13:40:15.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":149871,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":63574,"tmdb":8.083,"tmdbVotes":2079}},{"id":"movie-taxi-driver-1976","title":"Taxi Driver","year":1976,"type":"movie","runtimeSeconds":6831,"seasonCount":null,"communityRating":8.121,"criticRating":89,"genres":["Crime","Drama"],"overview":"Suffering from insomnia, disturbed loner Travis Bickle takes a job as a New York City cabbie, haunting the streets nightly, growing increasingly detached from reality as he dreams of cleaning up the filthy city.","poster":"assets/movie-taxi-driver-1976-poster.jpg","backdrop":"assets/movie-taxi-driver-1976-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:23:04.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":103,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":93,"rtAudienceLabel":"Upright","imdb":8.2,"imdbVotes":1046700,"tmdb":8.116,"tmdbVotes":13973}},{"id":"movie-triangle-of-sadness-2022","title":"Triangle of Sadness","year":2022,"type":"movie","runtimeSeconds":8833,"seasonCount":null,"communityRating":7.005,"criticRating":71,"genres":["Comedy","Drama"],"overview":"A celebrity model couple are invited on a luxury cruise for the uber-rich, helmed by an unhinged, alcoholic captain. What first appears Instagrammable ends catastrophically, leaving the survivors stranded on a desert\u2026","poster":"assets/movie-triangle-of-sadness-2022-poster.jpg","backdrop":"assets/movie-triangle-of-sadness-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:29:00.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":497828,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":80,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":221069,"tmdb":7,"tmdbVotes":3104}},{"id":"movie-tsotsi-2005","title":"Tsotsi","year":2005,"type":"movie","runtimeSeconds":5663,"seasonCount":null,"communityRating":6.937,"criticRating":82,"genres":["Crime","Drama"],"overview":"A young South African boy from the Johannesburg ghetto named Tsotsi, meaning Gangster, leaves home as a child to get away from his helpless parents. Now a teenage thug, Tsotsi finds a baby in the back seat of a car\u2026","poster":"assets/movie-tsotsi-2005-poster.jpg","backdrop":"assets/movie-tsotsi-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:58.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":868,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":82,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":32526,"tmdb":6.941,"tmdbVotes":372}},{"id":"movie-unknown-number-the-high-school-catfish-2025","title":"Unknown Number: The High School Catfish","year":2025,"type":"movie","runtimeSeconds":5691,"seasonCount":null,"communityRating":6.746,"criticRating":85,"genres":["Documentary"],"overview":"Vulgar, taunting texts blow up the phones of a teen and her boyfriend. Who's sending them \u2014 and why? This twisty documentary reveals the shocking answer.","poster":"assets/movie-unknown-number-the-high-school-catfish-2025-poster.jpg","backdrop":"assets/movie-unknown-number-the-high-school-catfish-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:32:10.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1517772,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":85,"rtCriticsLabel":"Fresh","rtAudience":69,"rtAudienceLabel":"Upright","imdb":6.7,"imdbVotes":23039,"tmdb":6.737,"tmdbVotes":257}},{"id":"movie-the-uprising-2026","title":"The Uprising","year":2026,"type":"movie","runtimeSeconds":7684,"seasonCount":null,"communityRating":7.846,"criticRating":55,"genres":["Action","Drama","History"],"overview":"As war and plague devastate 14th-century England, a humble peasant ignites a rebellion, uniting an army of commoners to defy the King\u2019s might in a desperate fight for justice and survival.","poster":"assets/movie-the-uprising-2026-poster.jpg","backdrop":"assets/movie-the-uprising-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-02T16:37:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":977942,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":65,"rtAudienceLabel":"Upright","imdb":6.2,"imdbVotes":6622,"tmdb":7.8,"tmdbVotes":404}},{"id":"movie-vice-2018","title":"Vice","year":2018,"type":"movie","runtimeSeconds":7948,"seasonCount":null,"communityRating":7.013,"criticRating":64,"genres":["Drama","Comedy","History"],"overview":"George W. Bush picks Dick Cheney, the CEO of Halliburton Co., to be his Republican running mate in the 2000 presidential election. No stranger to politics, Cheney's impressive r\u00e9sum\u00e9 includes stints as White House\u2026","poster":"assets/movie-vice-2018-poster.jpg","backdrop":"assets/movie-vice-2018-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:28:30.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":429197,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":61,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":177299,"tmdb":7.014,"tmdbVotes":3723}},{"id":"movie-warriors-of-the-wind-1984","title":"Warriors of the Wind","year":1984,"type":"movie","runtimeSeconds":7022,"seasonCount":null,"communityRating":7.941,"criticRating":87,"genres":["Adventure","Animation","Fantasy"],"overview":"After a global war, the seaside kingdom known as the Valley of the Wind remains one of the last strongholds on Earth untouched by a poisonous jungle and the powerful insects that guard it. Led by the courageous\u2026","poster":"assets/movie-warriors-of-the-wind-1984-poster.jpg","backdrop":"assets/movie-warriors-of-the-wind-1984-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:22:20.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":81,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":200154,"tmdb":7.94,"tmdbVotes":4260}},{"id":"movie-we-bury-the-dead-2026","title":"We Bury the Dead","year":2026,"type":"movie","runtimeSeconds":5693,"seasonCount":null,"communityRating":6.153,"criticRating":87,"genres":["Horror","Thriller"],"overview":"After a catastrophic military disaster reanimates the dead, Ava searches for her missing husband amidst an increasingly hostile world.","poster":"assets/movie-we-bury-the-dead-2026-poster.jpg","backdrop":"assets/movie-we-bury-the-dead-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-08-26T05:36:00.4272328Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1198984,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Certified Fresh","rtAudience":45,"rtAudienceLabel":"Spilled","imdb":5.6,"imdbVotes":20693,"tmdb":6.137,"tmdbVotes":433}},{"id":"movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022","title":"White Hot: The Rise & Fall of Abercrombie & Fitch","year":2022,"type":"movie","runtimeSeconds":5187,"seasonCount":null,"communityRating":5.663,"criticRating":66,"genres":["Documentary"],"overview":"All the cool kids were wearing it. This documentary explores A&F's pop culture reign in the late '90s and early 2000s and how it thrived on exclusion.","poster":"assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-poster.jpg","backdrop":"assets/movie-white-hot-the-rise-fall-of-abercrombie-fitch-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:30:34.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":953371,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":29,"rtAudienceLabel":"Spilled","imdb":5.8,"imdbVotes":5655,"tmdb":5.7,"tmdbVotes":102}},{"id":"movie-the-wild-robot-2024","title":"The Wild Robot","year":2024,"type":"movie","runtimeSeconds":6101,"seasonCount":null,"communityRating":8.308,"criticRating":97,"genres":["Family","Animation","Science Fiction"],"overview":"After a shipwreck, an intelligent robot called Roz is stranded on an uninhabited island. To survive the harsh environment, Roz bonds with the island's animals and cares for an orphaned baby goose.","poster":"assets/movie-the-wild-robot-2024-poster.jpg","backdrop":"assets/movie-the-wild-robot-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:31:49.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1184918,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Certified Fresh","rtAudience":98,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":241134,"tmdb":8.309,"tmdbVotes":6819}},{"id":"movie-the-wind-rises-2013","title":"The Wind Rises","year":2013,"type":"movie","runtimeSeconds":8031,"seasonCount":null,"communityRating":7.784,"criticRating":88,"genres":["Animation","Drama","History"],"overview":"A lifelong love of flight inspires Japanese aviation engineer Jiro Horikoshi, whose storied career includes the creation of the A-6M World War II fighter plane.","poster":"assets/movie-the-wind-rises-2013-poster.jpg","backdrop":"assets/movie-the-wind-rises-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T10:24:38.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":149870,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":85,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":115756,"tmdb":7.783,"tmdbVotes":3536}},{"id":"movie-the-witcher-nightmare-of-the-wolf-2021","title":"The Witcher: Nightmare of the Wolf","year":2021,"type":"movie","runtimeSeconds":5015,"seasonCount":null,"communityRating":7.422,"criticRating":100,"genres":["Animation","Action","Fantasy"],"overview":"Escaping from poverty to become a witcher, Vesemir slays monsters for coin and glory, but when a new menace rises, he must face the demons of his past.","poster":"assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg","backdrop":"assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T04:42:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":666243,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":83,"rtAudienceLabel":"Upright","imdb":7.2,"imdbVotes":55759,"tmdb":7.407,"tmdbVotes":1159}},{"id":"movie-the-witcher-sirens-of-the-deep-2025","title":"The Witcher: Sirens of the Deep","year":2025,"type":"movie","runtimeSeconds":5517,"seasonCount":null,"communityRating":6.8,"criticRating":60,"genres":["Animation","Fantasy","Action"],"overview":"When human sailors are attacked by mysterious creatures of the deep, only one person can stop the war between land and sea: the Witcher, Geralt of Rivia.","poster":"assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg","backdrop":"assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T05:03:26.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1203329,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":60,"rtCriticsLabel":"Fresh","rtAudience":37,"rtAudienceLabel":"Spilled","imdb":6.1,"imdbVotes":11799,"tmdb":6.741,"tmdbVotes":303}}],"shows":[{"id":"tv-adults-2025","title":"Adults","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.495,"criticRating":null,"genres":["Comedy"],"overview":"A group of twenty-somethings in New York trying to be good people, despite being neither \"good\" nor \"people\" yet.","poster":"assets/tv-adults-2025-poster.jpg","backdrop":"assets/tv-adults-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-23T21:01:41.1814765Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":260458,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":102}},{"id":"tv-american-crime-story-2016","title":"American Crime Story","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.573,"criticRating":null,"genres":["Crime","Drama"],"overview":"An anthology series centered around some of history's most famous criminal investigations.","poster":"assets/tv-american-crime-story-2016-poster.jpg","backdrop":"assets/tv-american-crime-story-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0490488Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":64513,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.585,"tmdbVotes":1094}},{"id":"tv-american-primeval-2025","title":"American Primeval","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.691,"criticRating":null,"genres":["Western","Drama","Action & Adventure"],"overview":"A mother and son fleeing from their past form a found family while confronting a harsh landscape of freedom and cruelty in the American West.","poster":"assets/tv-american-primeval-2025-poster.jpg","backdrop":"assets/tv-american-primeval-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0573221Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":215866,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":71,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.741,"tmdbVotes":538}},{"id":"tv-arrested-development-2003","title":"Arrested Development","year":2003,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":7.925,"criticRating":null,"genres":["Comedy"],"overview":"The story of a wealthy family that lost everything, and the one son who had no choice but to keep them all together.","poster":"assets/tv-arrested-development-2003-poster.jpg","backdrop":"assets/tv-arrested-development-2003-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0513048Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":4589,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.906,"tmdbVotes":1793}},{"id":"tv-avatar-the-last-airbender-2005","title":"Avatar: The Last Airbender","year":2005,"type":"tv","runtimeSeconds":1440,"seasonCount":3,"communityRating":8.78,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a war-torn world of elemental magic, a young boy reawakens to undertake a dangerous mystic quest to fulfill his destiny as the Avatar, and bring peace to the world.","poster":"assets/tv-avatar-the-last-airbender-2005-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0504536Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":246,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.787,"tmdbVotes":5215}},{"id":"tv-b-b-vol-liefde-2021","title":"B&B Vol Liefde","year":2021,"type":"tv","runtimeSeconds":3720,"seasonCount":1,"communityRating":7.2,"criticRating":null,"genres":["Reality"],"overview":"B&B Vol Liefde is a heartwarming adventure, full of romance, excitement, and unforgettable moments. Adventurous Dutch B&B owners abroad throw their doors wide open and let their hearts speak, determined to find that\u2026","poster":"assets/tv-b-b-vol-liefde-2021-poster.jpg","backdrop":"assets/tv-b-b-vol-liefde-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-31T22:30:11.129657Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":124428,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.2,"tmdbVotes":10}},{"id":"tv-the-bear-2022","title":"The Bear","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.139,"criticRating":null,"genres":["Drama","Comedy"],"overview":"Carmy, a young fine-dining chef, comes home to Chicago to run his family sandwich shop. As he fights to transform the shop and himself, he works alongside a rough-around-the-edges crew that ultimately reveal\u2026","poster":"assets/tv-the-bear-2022-poster.jpg","backdrop":"assets/tv-the-bear-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0467371Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":136315,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.158,"tmdbVotes":2025}},{"id":"tv-blue-planet-ii-2017","title":"Blue Planet II","year":2017,"type":"tv","runtimeSeconds":3600,"seasonCount":1,"communityRating":8.64,"criticRating":null,"genres":["Documentary","Family"],"overview":"There is nowhere more powerful and unforgiving yet more beautiful and compelling than the ocean. Join us and explore the greatest yet least known parts of our planet.","poster":"assets/tv-blue-planet-ii-2017-poster.jpg","backdrop":"assets/tv-blue-planet-ii-2017-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0634849Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":74313,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":380}},{"id":"tv-breeders-2020","title":"Breeders","year":2020,"type":"tv","runtimeSeconds":1500,"seasonCount":4,"communityRating":6.8,"criticRating":null,"genres":["Comedy"],"overview":"Exposing the parental-paradox that it is possible, in the very same moment, to love your child to the horizon of the universe, while being apoplectically angry enough to want to send them there.","poster":"assets/tv-breeders-2020-poster.jpg","backdrop":"assets/tv-breeders-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T18:54:21.8885435Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":93290,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":104}},{"id":"tv-chernobyl-2019","title":"Chernobyl","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.718,"criticRating":null,"genres":["Drama"],"overview":"The true story of one of the worst man-made catastrophes in history: the catastrophic nuclear accident at Chernobyl. A tale of the brave men and women who sacrificed to save Europe from unimaginable disaster.","poster":"assets/tv-chernobyl-2019-poster.jpg","backdrop":"assets/tv-chernobyl-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0469903Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":87108,"artwork":{"posterKind":"poster","backdropKind":"center_crop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.731,"tmdbVotes":8496}},{"id":"tv-community-2009","title":"Community","year":2009,"type":"tv","runtimeSeconds":null,"seasonCount":6,"communityRating":7.997,"criticRating":null,"genres":["Comedy"],"overview":"Follow the lives of a group of students at what is possibly the world\u2019s worst community college in the fictional locale of Greendale, Colorado.","poster":"assets/tv-community-2009-poster.jpg","backdrop":"assets/tv-community-2009-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0531416Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":18347,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.994,"tmdbVotes":2226}},{"id":"tv-dead-end-job-2026","title":"Dead-End Job","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.7,"criticRating":null,"genres":["Mystery"],"overview":"Drowning in debt, a young man takes lucrative part-time jobs from a mysterious agency \u2014 only to find his hustle for more money turning nightmarish.","poster":"assets/tv-dead-end-job-2026-poster.jpg","backdrop":"assets/tv-dead-end-job-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T18:58:01.8352857Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":289517,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":38}},{"id":"tv-dopesick-2021","title":"Dopesick","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.015,"criticRating":null,"genres":["Crime","Drama"],"overview":"The story of how one company triggered the worst drug epidemic in American history. Look into the epicenter of America's struggle with opioid addiction, from a distressed Virginia mining community, to the hallways of\u2026","poster":"assets/tv-dopesick-2021-poster.jpg","backdrop":"assets/tv-dopesick-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0580168Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":110695,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.035,"tmdbVotes":565}},{"id":"tv-dune-prophecy-2024","title":"Dune: Prophecy","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.213,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama","Action & Adventure"],"overview":"Ten thousand years before the ascension of Paul Atreides, sisters Valya and Tula Harkonnen establish the fabled sect and female order that would become known as the Bene Gesserit to control the future of humankind.","poster":"assets/tv-dune-prophecy-2024-poster.jpg","backdrop":"assets/tv-dune-prophecy-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T18:23:01.2908568Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":90228,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.213,"tmdbVotes":677}},{"id":"tv-east-of-eden-2026","title":"East of Eden","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.931,"criticRating":null,"genres":["Drama","Crime"],"overview":"In the early 1900s, a fiercely independent woman drives a rift between two brothers, igniting a saga that unfolds across two generations.","poster":"assets/tv-east-of-eden-2026-poster.jpg","backdrop":"assets/tv-east-of-eden-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-23T18:07:11.3965096Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":258165,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.982,"tmdbVotes":139}},{"id":"tv-fallout-2024","title":"Fallout","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.124,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"The story of haves and have-nots in a world in which there's almost nothing left to have. 200 years after the apocalypse, the gentle denizens of luxury fallout shelters are forced to return to the irradiated\u2026","poster":"assets/tv-fallout-2024-poster.jpg","backdrop":"assets/tv-fallout-2024-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0669262Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":106379,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.106,"tmdbVotes":3233}},{"id":"tv-fawlty-towers-1975","title":"Fawlty Towers","year":1975,"type":"tv","runtimeSeconds":1800,"seasonCount":2,"communityRating":8.295,"criticRating":null,"genres":["Comedy"],"overview":"Owner Basil Fawlty, his wife Sybil, a chambermaid Polly, and Spanish waiter Manuel attempt to run their hotel amidst farcical situations and an array of demanding guests.","poster":"assets/tv-fawlty-towers-1975-poster.jpg","backdrop":"assets/tv-fawlty-towers-1975-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T23:26:39.7658344Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2207,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.296,"tmdbVotes":867}},{"id":"tv-the-final-problem-2026","title":"The Final Problem","year":2026,"type":"tv","runtimeSeconds":3420,"seasonCount":1,"communityRating":7.041,"criticRating":null,"genres":["Crime","Mystery"],"overview":"In the spring of 1959, a retired actor, who previously played Sherlock Holmes, must investigate a murder at a small hotel on a tiny island near Mallorca.","poster":"assets/tv-the-final-problem-2026-poster.jpg","backdrop":"assets/tv-the-final-problem-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T19:16:12.4822791Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":290720,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.041,"tmdbVotes":49}},{"id":"tv-the-four-seasons-2025","title":"The Four Seasons","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.879,"criticRating":null,"genres":["Comedy"],"overview":"The decades-long friendship between three married couples is tested when one divorces, complicating their tradition of quarterly weekend getaways.","poster":"assets/tv-the-four-seasons-2025-poster.jpg","backdrop":"assets/tv-the-four-seasons-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T18:36:42.5326384Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":243316,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.892,"tmdbVotes":204}},{"id":"tv-the-get-down-2016","title":"The Get Down","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.3,"criticRating":null,"genres":["Drama"],"overview":"In 1977 New York City, the talented and soulful youth of the South Bronx chase dreams and breakneck beats to transform music history.","poster":"assets/tv-the-get-down-2016-poster.jpg","backdrop":"assets/tv-the-get-down-2016-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0497737Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":65345,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.3,"tmdbVotes":196}},{"id":"tv-how-to-change-your-mind-2022","title":"How to Change Your Mind","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.76,"criticRating":null,"genres":["Documentary","Mystery"],"overview":"Author Michael Pollan leads the way in this docuseries exploring the history and uses of psychedelics, including LSD, psilocybin, MDMA and mescaline.","poster":"assets/tv-how-to-change-your-mind-2022-poster.jpg","backdrop":"assets/tv-how-to-change-your-mind-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0601399Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":204304,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":53}},{"id":"tv-interrogation-2020","title":"Interrogation","year":2020,"type":"tv","runtimeSeconds":2700,"seasonCount":1,"communityRating":6.788,"criticRating":null,"genres":["Drama","Crime"],"overview":"Based on a true story that spanned more than 30 years in which a young man was charged and convicted of brutally murdering his mother. Each episode is structured around an interrogation taken directly from the real\u2026","poster":"assets/tv-interrogation-2020-poster.jpg","backdrop":"assets/tv-interrogation-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T05:52:06.1068207Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":96488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.9,"tmdbVotes":54}},{"id":"tv-it-s-always-sunny-in-philadelphia-2005","title":"It's Always Sunny in Philadelphia","year":2005,"type":"tv","runtimeSeconds":null,"seasonCount":18,"communityRating":8.2,"criticRating":null,"genres":["Comedy"],"overview":"Four egocentric friends run a neighborhood Irish pub in Philadelphia and try to find their way through the adult world of work and relationships. Unfortunately, their warped views and precarious judgments often lead\u2026","poster":"assets/tv-it-s-always-sunny-in-philadelphia-2005-poster.jpg","backdrop":"assets/tv-it-s-always-sunny-in-philadelphia-2005-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0485774Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2710,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.253,"tmdbVotes":1457}},{"id":"tv-a-knight-of-the-seven-kingdoms-2026","title":"A Knight of the Seven Kingdoms","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.431,"criticRating":null,"genres":["Drama","Sci-Fi & Fantasy","Action & Adventure"],"overview":"A century before the events of Game of Thrones, two unlikely heroes wandered Westeros: a young, naive but courageous knight, Ser Duncan the Tall, and his diminutive squire, Egg. Set in an age when the Targaryen line\u2026","poster":"assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg","backdrop":"assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0493344Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":224372,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":1266}},{"id":"tv-landman-2024","title":"Landman","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.194,"criticRating":null,"genres":["Drama"],"overview":"Set in the proverbial boomtowns of West-Texas and a modern-day tale of fortune-seeking in the world of oil rigs, the series is an upstairs/downstairs story of roughnecks and wildcat billionaires that are fueling a\u2026","poster":"assets/tv-landman-2024-poster.jpg","backdrop":"assets/tv-landman-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T21:38:18.0973875Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":157741,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.209,"tmdbVotes":668}},{"id":"tv-the-last-of-us-2023","title":"The Last of Us","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.419,"criticRating":null,"genres":["Drama"],"overview":"Twenty years after modern civilization has been destroyed, Joel, a hardened survivor, is hired to smuggle Ellie, a 14-year-old girl, out of an oppressive quarantine zone. What starts as a small job soon becomes a\u2026","poster":"assets/tv-the-last-of-us-2023-poster.jpg","backdrop":"assets/tv-the-last-of-us-2023-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0640955Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":100088,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.4,"tmdbVotes":7485}},{"id":"tv-last-samurai-standing-2025","title":"Last Samurai Standing","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.805,"criticRating":null,"genres":["Action & Adventure","Drama"],"overview":"In the early Meiji era, Shujiro, once known as an undefeated samurai, decides to participate in a deadly survival game to save his family and villagers.","poster":"assets/tv-last-samurai-standing-2025-poster.jpg","backdrop":"assets/tv-last-samurai-standing-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0622526Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":252193,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.812,"tmdbVotes":271}},{"id":"tv-lord-of-the-flies-2026","title":"Lord of the Flies","year":2026,"type":"tv","runtimeSeconds":3480,"seasonCount":1,"communityRating":7.571,"criticRating":null,"genres":["Drama","Action & Adventure"],"overview":"After a plane crash, a group of schoolboys find themselves stranded on a tropical island without adults. Ralph is elected leader and, with the help of the intelligent Piggy, strives to maintain order and\u2026","poster":"assets/tv-lord-of-the-flies-2026-poster.jpg","backdrop":"assets/tv-lord-of-the-flies-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0645648Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":270572,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":91,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.58,"tmdbVotes":138}},{"id":"tv-the-lord-of-the-rings-the-rings-of-power-2022","title":"The Lord of the Rings: The Rings of Power","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":7.233,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"Beginning in a time of relative peace, we follow an ensemble cast of characters as they confront the re-emergence of evil to Middle-earth. From the darkest depths of the Misty Mountains, to the majestic forests of\u2026","poster":"assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg","backdrop":"assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0618904Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":84773,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.211,"tmdbVotes":4003}},{"id":"tv-love-death-robots-2019","title":"Love, Death & Robots","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":8.21,"criticRating":null,"genres":["Animation","Sci-Fi & Fantasy"],"overview":"Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.","poster":"assets/tv-love-death-robots-2019-poster.jpg","backdrop":"assets/tv-love-death-robots-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0460964Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86831,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":89,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.203,"tmdbVotes":4238}},{"id":"tv-maniac-2018","title":"Maniac","year":2018,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.338,"criticRating":null,"genres":["Comedy","Drama","Sci-Fi & Fantasy"],"overview":"Two strangers are drawn to a mysterious pharmaceutical trial that will, they're assured, with no complications or side-effects whatsoever, solve all of their problems, permanently. Things do not go as planned.","poster":"assets/tv-maniac-2018-poster.jpg","backdrop":"assets/tv-maniac-2018-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-20T19:42:30.955548Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":73411,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":84,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.323,"tmdbVotes":1073}},{"id":"tv-mussolini-son-of-the-century-2024","title":"Mussolini: Son of the Century","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.135,"criticRating":null,"genres":["Drama","War & Politics"],"overview":"The rise of Benito Mussolini and the birth of fascism in Italy, chronicling a country\u2019s surrender to dictatorship and the relentless ascent of a man who rose from his ashes time and again.","poster":"assets/tv-mussolini-son-of-the-century-2024-poster.jpg","backdrop":"assets/tv-mussolini-son-of-the-century-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0586588Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":212333,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.1,"tmdbVotes":179}},{"id":"tv-the-night-manager-2016","title":"The Night Manager","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":7.74,"criticRating":null,"genres":["Drama","Mystery","Crime"],"overview":"Former British soldier Jonathan Pine navigates the shadowy recesses of Whitehall and Washington where an unholy alliance operates between the intelligence community and the secret arms trade. To infiltrate the inner\u2026","poster":"assets/tv-the-night-manager-2016-poster.jpg","backdrop":"assets/tv-the-night-manager-2016-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0663672Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":61859,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":1422}},{"id":"tv-one-piece-2023","title":"ONE PIECE","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.118,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy"],"overview":"With his straw hat and ragtag crew, young pirate Monkey D. Luffy goes on an epic voyage for treasure.","poster":"assets/tv-one-piece-2023-poster.jpg","backdrop":"assets/tv-one-piece-2023-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0439378Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111110,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.104,"tmdbVotes":1988}},{"id":"tv-only-murders-in-the-building-2021","title":"Only Murders in the Building","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.438,"criticRating":null,"genres":["Comedy","Mystery","Crime"],"overview":"Three strangers share an obsession with true crime and suddenly find themselves wrapped up in one.","poster":"assets/tv-only-murders-in-the-building-2021-poster.jpg","backdrop":"assets/tv-only-murders-in-the-building-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0594614Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":107113,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.398,"tmdbVotes":2333}},{"id":"tv-over-the-garden-wall-2014","title":"Over the Garden Wall","year":2014,"type":"tv","runtimeSeconds":720,"seasonCount":1,"communityRating":8.6,"criticRating":null,"genres":["Mystery","Sci-Fi & Fantasy","Animation"],"overview":"Two brothers, Wirt and Greg, find themselves lost in the Unknown; a strange forest adrift in time. With the help of a wise old Woodsman and a foul-tempered bluebird named Beatrice, Wirt and Greg must travel across\u2026","poster":"assets/tv-over-the-garden-wall-2014-poster.jpg","backdrop":"assets/tv-over-the-garden-wall-2014-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0455757Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":61617,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.6,"tmdbVotes":1854}},{"id":"tv-peaky-blinders-2013","title":"Peaky Blinders","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":6,"communityRating":8.5,"criticRating":null,"genres":["Drama","Crime"],"overview":"A gangster family epic set in 1919 Birmingham, England and centered on a gang who sew razor blades in the peaks of their caps, and their fierce boss Tommy Shelby, who means to move up in the world.","poster":"assets/tv-peaky-blinders-2013-poster.jpg","backdrop":"assets/tv-peaky-blinders-2013-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0539185Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":60574,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.5,"tmdbVotes":11506}},{"id":"tv-the-pitt-2025","title":"The Pitt","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.729,"criticRating":null,"genres":["Drama"],"overview":"The staff of Pittsburgh's Trauma Medical Center work around the clock to save lives in an overcrowded and underfunded emergency department.","poster":"assets/tv-the-pitt-2025-poster.jpg","backdrop":"assets/tv-the-pitt-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0582942Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":250307,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.731,"tmdbVotes":1046}},{"id":"tv-pluribus-2025","title":"Pluribus","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.936,"criticRating":null,"genres":["Drama","Sci-Fi & Fantasy"],"overview":"The most miserable person on Earth must save the world from happiness.","poster":"assets/tv-pluribus-2025-poster.jpg","backdrop":"assets/tv-pluribus-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0674462Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":225171,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":99,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.921,"tmdbVotes":1191}},{"id":"tv-ripley-2024","title":"RIPLEY","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.911,"criticRating":null,"genres":["Drama"],"overview":"A grifter in 1960s New York is hired to convince a wealthy man's son to return home from Italy and begins a life of deceit, fraud and murder.","poster":"assets/tv-ripley-2024-poster.jpg","backdrop":"assets/tv-ripley-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0658942Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":94028,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":86,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.909,"tmdbVotes":453}},{"id":"tv-say-nothing-2024","title":"Say Nothing","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.462,"criticRating":null,"genres":["Drama","Crime"],"overview":"Through the eyes of various Irish Republican Army (IRA) members, explore the extremes some people will go to in the name of their beliefs, the way a deeply divided society can suddenly tip over into armed conflict,\u2026","poster":"assets/tv-say-nothing-2024-poster.jpg","backdrop":"assets/tv-say-nothing-2024-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0499752Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":245256,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":94,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.475,"tmdbVotes":122}},{"id":"tv-severance-2022","title":"Severance","year":2022,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.396,"criticRating":null,"genres":["Drama","Mystery","Sci-Fi & Fantasy"],"overview":"Mark leads a team of office workers whose memories have been surgically divided between their work and personal lives. When a mysterious colleague appears outside of work, it begins a journey to discover the truth\u2026","poster":"assets/tv-severance-2022-poster.jpg","backdrop":"assets/tv-severance-2022-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0598478Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":95396,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":95,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.389,"tmdbVotes":3095}},{"id":"tv-six-feet-under-2001","title":"Six Feet Under","year":2001,"type":"tv","runtimeSeconds":null,"seasonCount":5,"communityRating":8.115,"criticRating":null,"genres":["Drama"],"overview":"When death is your business, what is your life? For the Fisher family, the world outside of their family-owned funeral home continues to be at least as challenging as\u2014and far less predictable than\u2014the one inside.","poster":"assets/tv-six-feet-under-2001-poster.jpg","backdrop":"assets/tv-six-feet-under-2001-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-09-26T04:04:05.108787Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":1274,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.1,"tmdbVotes":1192}},{"id":"tv-undercover-2019","title":"Undercover","year":2019,"type":"tv","runtimeSeconds":3000,"seasonCount":3,"communityRating":7.5,"criticRating":null,"genres":["Crime","Drama"],"overview":"Undercover agents infiltrate a drug kingpin's operation by posing as a couple at the campground where he spends his weekends. Inspired by real events.","poster":"assets/tv-undercover-2019-poster.jpg","backdrop":"assets/tv-undercover-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0570153Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86942,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":186}},{"id":"tv-upload-2020","title":"Upload","year":2020,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":7.781,"criticRating":null,"genres":["Comedy","Sci-Fi & Fantasy","Drama"],"overview":"In 2033, people who are near death can be \u201cuploaded\u201d into virtual reality hotels run by 6 tech firms. Cash-strapped Nora lives in Brooklyn and works customer service for the luxurious \u201cLakeview\u201d digital afterlife.\u2026","poster":"assets/tv-upload-2020-poster.jpg","backdrop":"assets/tv-upload-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0566967Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":86248,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":87,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.757,"tmdbVotes":1511}},{"id":"tv-the-walk-in-2022","title":"The Walk-In","year":2022,"type":"tv","runtimeSeconds":2760,"seasonCount":1,"communityRating":6.9,"criticRating":null,"genres":["Drama","Crime"],"overview":"Forced to flee the UK and go into hiding in the 1990s, when he became a mole from within the BNP, Matthew Collins returned to Britain to make a new life for himself, working with young white men in the fight against\u2026","poster":"assets/tv-the-walk-in-2022-poster.jpg","backdrop":"assets/tv-the-walk-in-2022-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0654569Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":210664,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":50}},{"id":"tv-the-white-lotus-2021","title":"The White Lotus","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.596,"criticRating":null,"genres":["Comedy","Drama","Mystery"],"overview":"Follow the exploits of various guests and employees at an exclusive tropical resort over the span of a week as with each passing day, a darker complexity emerges in these picture-perfect travelers, the hotel\u2019s\u2026","poster":"assets/tv-the-white-lotus-2021-poster.jpg","backdrop":"assets/tv-the-white-lotus-2021-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0546438Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":111803,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":90,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.599,"tmdbVotes":1604}},{"id":"tv-widow-s-bay-2026","title":"Widow's Bay","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":8.172,"criticRating":null,"genres":["Drama","Comedy","Mystery"],"overview":"The mayor of a New England town resolves to turn it into the next tourist hot spot, despite local warnings that it's cursed.","poster":"assets/tv-widow-s-bay-2026-poster.jpg","backdrop":"assets/tv-widow-s-bay-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-20T10:27:12.7003979Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":270476,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":98,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8,"tmdbVotes":585}},{"id":"tv-the-witcher-2019","title":"The Witcher","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":4,"communityRating":7.905,"criticRating":null,"genres":["Drama","Action & Adventure"],"overview":"Geralt of Rivia, a mutated monster-hunter for hire, journeys toward his destiny in a turbulent world where people often prove more wicked than beasts.","poster":"assets/tv-the-witcher-2019-poster.jpg","backdrop":"assets/tv-the-witcher-2019-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0552369Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":71912,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":75,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.88,"tmdbVotes":6997}},{"id":"tv-yellowjackets-2021","title":"Yellowjackets","year":2021,"type":"tv","runtimeSeconds":null,"seasonCount":3,"communityRating":7.449,"criticRating":null,"genres":["Drama","Mystery"],"overview":"This equal parts survival epic, psychological horror story and coming-of-age drama tells the saga of a team of wildly talented high school girls soccer players who become the (un)lucky survivors of a plane crash deep\u2026","poster":"assets/tv-yellowjackets-2021-poster.jpg","backdrop":"assets/tv-yellowjackets-2021-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T15:45:22.0577612Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":117488,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.431,"tmdbVotes":1070}}],"anime":[],"resume":[{"id":"episode-episode-1-2025-s1e1","title":"Episode 1","year":2025,"type":"episode","runtimeSeconds":3618,"seasonCount":null,"communityRating":7.357,"criticRating":null,"genres":["Biography","Drama","History"],"overview":"Mussolini had just founded the Fasci di Combattimento when D'Annunzio took Fiume. Mussolini hoped to capitalize on the poet's achievement in the electoral arena.","poster":"assets/episode-episode-1-2025-s1e1-poster.jpg","backdrop":"assets/episode-episode-1-2025-s1e1-backdrop.jpg","played":false,"playCount":1,"resumeSeconds":489.55,"progress":0.1353,"addedAt":"2026-07-17T13:52:48.0000000Z","lastPlayedAt":"2026-11-04T19:01:49.9731435Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Mussolini: Son of the Century","seasonNumber":1,"episodeNumber":1,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"}}],"nextUp":[{"id":"episode-no-honeymoon-2020-s1e8","title":"No Honeymoon","year":2020,"type":"episode","runtimeSeconds":1499,"seasonCount":null,"communityRating":6.7,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Paul attempts to focus on his upcoming wedding but is constantly thwarted by his family; Ally is distracted by work, Luke is misbehaving at school and Leah (Stella Gonet) doesn\u2019t understand why they are even\u2026","poster":"assets/episode-no-honeymoon-2020-s1e8-poster.jpg","backdrop":"assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg","played":false,"playCount":1,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T19:00:50.0000000Z","lastPlayedAt":"2026-11-03T20:57:58.3314947Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Breeders","seasonNumber":1,"episodeNumber":8,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-notapusy-2005-s3e4","title":"Notapusy","year":2005,"type":"episode","runtimeSeconds":1334,"seasonCount":null,"communityRating":8.125,"criticRating":null,"genres":["Comedy"],"overview":"To prove he's a man to Rita, Michael and his nephew Steve Holt compete in the Church And State Fair's annual triathlon. George Sr. tries to scare some at-risk youth, and Tobias tries to help George-Michael's\u2026","poster":"assets/episode-notapusy-2005-s3e4-poster.jpg","backdrop":"assets/episode-notapusy-2005-s3e4-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T12:17:58.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Arrested Development","seasonNumber":3,"episodeNumber":4,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-the-man-who-would-be-vogue-2018-s2e1","title":"The Man Who Would Be Vogue","year":2018,"type":"episode","runtimeSeconds":3203,"seasonCount":null,"communityRating":7.459,"criticRating":null,"genres":["Biography","Crime","Drama"],"overview":"The murder of Gianni Versace turns the eyes of the world onto Miami Beach.","poster":"assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg","backdrop":"assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T13:29:35.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"American Crime Story","seasonNumber":2,"episodeNumber":1,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-tomorrow-2024-s3e1","title":"Tomorrow","year":2024,"type":"episode","runtimeSeconds":2207,"seasonCount":null,"communityRating":7.6,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The next day and the days that led to it.","poster":"assets/episode-tomorrow-2024-s3e1-poster.jpg","backdrop":"assets/episode-tomorrow-2024-s3e1-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-07-17T13:46:39.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"The Bear","seasonNumber":3,"episodeNumber":1,"artwork":{"posterKind":"poster","backdropKind":"backdrop"}},{"id":"episode-episode-1-2025-s1e1","title":"Episode 1","year":2025,"type":"episode","runtimeSeconds":3618,"seasonCount":null,"communityRating":7.357,"criticRating":null,"genres":["Biography","Drama","History"],"overview":"Mussolini had just founded the Fasci di Combattimento when D'Annunzio took Fiume. Mussolini hoped to capitalize on the poet's achievement in the electoral arena.","poster":"assets/episode-episode-1-2025-s1e1-poster.jpg","backdrop":"assets/episode-episode-1-2025-s1e1-backdrop.jpg","played":false,"playCount":1,"resumeSeconds":489.55,"progress":0.1353,"addedAt":"2026-07-17T13:52:48.0000000Z","lastPlayedAt":"2026-11-04T19:01:49.9731435Z","availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","seriesTitle":"Mussolini: Son of the Century","seasonNumber":1,"episodeNumber":1,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"}}],"recentlyAdded":[{"id":"tv-the-final-problem-2026","title":"The Final Problem","year":2026,"type":"tv","runtimeSeconds":3420,"seasonCount":1,"communityRating":7.041,"criticRating":null,"genres":["Crime","Mystery"],"overview":"In the spring of 1959, a retired actor, who previously played Sherlock Holmes, must investigate a murder at a small hotel on a tiny island near Mallorca.","poster":"assets/tv-the-final-problem-2026-poster.jpg","backdrop":"assets/tv-the-final-problem-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T19:16:12.4822791Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":290720,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.041,"tmdbVotes":49}},{"id":"tv-dead-end-job-2026","title":"Dead-End Job","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.7,"criticRating":null,"genres":["Mystery"],"overview":"Drowning in debt, a young man takes lucrative part-time jobs from a mysterious agency \u2014 only to find his hustle for more money turning nightmarish.","poster":"assets/tv-dead-end-job-2026-poster.jpg","backdrop":"assets/tv-dead-end-job-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T18:58:01.8352857Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":289517,"artwork":{"posterKind":"center_crop","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":38}},{"id":"tv-dune-prophecy-2024","title":"Dune: Prophecy","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.213,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama","Action & Adventure"],"overview":"Ten thousand years before the ascension of Paul Atreides, sisters Valya and Tula Harkonnen establish the fabled sect and female order that would become known as the Bene Gesserit to control the future of humankind.","poster":"assets/tv-dune-prophecy-2024-poster.jpg","backdrop":"assets/tv-dune-prophecy-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-04T18:23:01.2908568Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":90228,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":64,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.213,"tmdbVotes":677}},{"id":"movie-the-uprising-2026","title":"The Uprising","year":2026,"type":"movie","runtimeSeconds":7684,"seasonCount":null,"communityRating":7.846,"criticRating":55,"genres":["Action","Drama","History"],"overview":"As war and plague devastate 14th-century England, a humble peasant ignites a rebellion, uniting an army of commoners to defy the King\u2019s might in a desperate fight for justice and survival.","poster":"assets/movie-the-uprising-2026-poster.jpg","backdrop":"assets/movie-the-uprising-2026-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-11-02T16:37:03.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":977942,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":57,"rtCriticsLabel":"Rotten","rtAudience":65,"rtAudienceLabel":"Upright","imdb":6.2,"imdbVotes":6622,"tmdb":7.8,"tmdbVotes":404}},{"id":"movie-jennifer-s-body-2009","title":"Jennifer's Body","year":2009,"type":"movie","runtimeSeconds":6450,"seasonCount":null,"communityRating":6.012,"criticRating":47,"genres":["Horror","Comedy"],"overview":"Jennifer, a gorgeous, seductive cheerleader takes evil to a whole new level after she's possessed by a sinister demon. Now it's up to her best friend to stop Jennifer's reign of terror before it's too late.","poster":"assets/movie-jennifer-s-body-2009-poster.jpg","backdrop":"assets/movie-jennifer-s-body-2009-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-30T03:37:04.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":19994,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":47,"rtCriticsLabel":"Rotten","rtAudience":36,"rtAudienceLabel":"Spilled","imdb":5.6,"imdbVotes":179710,"tmdb":6.013,"tmdbVotes":4481}},{"id":"tv-breeders-2020","title":"Breeders","year":2020,"type":"tv","runtimeSeconds":1500,"seasonCount":4,"communityRating":6.8,"criticRating":null,"genres":["Comedy"],"overview":"Exposing the parental-paradox that it is possible, in the very same moment, to love your child to the horizon of the universe, while being apoplectically angry enough to want to send them there.","poster":"assets/tv-breeders-2020-poster.jpg","backdrop":"assets/tv-breeders-2020-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T18:54:21.8885435Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":93290,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.8,"tmdbVotes":104}},{"id":"tv-the-four-seasons-2025","title":"The Four Seasons","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.879,"criticRating":null,"genres":["Comedy"],"overview":"The decades-long friendship between three married couples is tested when one divorces, complicating their tradition of quarterly weekend getaways.","poster":"assets/tv-the-four-seasons-2025-poster.jpg","backdrop":"assets/tv-the-four-seasons-2025-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-28T18:36:42.5326384Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":243316,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":81,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.892,"tmdbVotes":204}},{"id":"tv-adults-2025","title":"Adults","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":6.495,"criticRating":null,"genres":["Comedy"],"overview":"A group of twenty-somethings in New York trying to be good people, despite being neither \"good\" nor \"people\" yet.","poster":"assets/tv-adults-2025-poster.jpg","backdrop":"assets/tv-adults-2025-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-23T21:01:41.1814765Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":260458,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":102}},{"id":"tv-east-of-eden-2026","title":"East of Eden","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":1,"communityRating":7.931,"criticRating":null,"genres":["Drama","Crime"],"overview":"In the early 1900s, a fiercely independent woman drives a rift between two brothers, igniting a saga that unfolds across two generations.","poster":"assets/tv-east-of-eden-2026-poster.jpg","backdrop":"assets/tv-east-of-eden-2026-backdrop.jpg","played":true,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-23T18:07:11.3965096Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":258165,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.982,"tmdbVotes":139}},{"id":"movie-fighting-with-my-family-2019","title":"Fighting with My Family","year":2019,"type":"movie","runtimeSeconds":6509,"seasonCount":null,"communityRating":6.84,"criticRating":93,"genres":["Comedy"],"overview":"Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training\u2026","poster":"assets/movie-fighting-with-my-family-2019-poster.jpg","backdrop":"assets/movie-fighting-with-my-family-2019-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T22:10:59.0000000Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":445629,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":93,"rtCriticsLabel":"Certified Fresh","rtAudience":86,"rtAudienceLabel":"Upright","imdb":7,"imdbVotes":94576,"tmdb":6.837,"tmdbVotes":1952}},{"id":"tv-landman-2024","title":"Landman","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":2,"communityRating":8.194,"criticRating":null,"genres":["Drama"],"overview":"Set in the proverbial boomtowns of West-Texas and a modern-day tale of fortune-seeking in the world of oil rigs, the series is an upstairs/downstairs story of roughnecks and wildcat billionaires that are fueling a\u2026","poster":"assets/tv-landman-2024-poster.jpg","backdrop":"assets/tv-landman-2024-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-22T21:38:18.0973875Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":157741,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":80,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.209,"tmdbVotes":668}},{"id":"tv-fawlty-towers-1975","title":"Fawlty Towers","year":1975,"type":"tv","runtimeSeconds":1800,"seasonCount":2,"communityRating":8.295,"criticRating":null,"genres":["Comedy"],"overview":"Owner Basil Fawlty, his wife Sybil, a chambermaid Polly, and Spanish waiter Manuel attempt to run their hotel amidst farcical situations and an array of demanding guests.","poster":"assets/tv-fawlty-towers-1975-poster.jpg","backdrop":"assets/tv-fawlty-towers-1975-backdrop.jpg","played":false,"playCount":0,"resumeSeconds":0.0,"progress":0,"addedAt":"2026-10-19T23:26:39.7658344Z","lastPlayedAt":null,"availability":"in_library","availabilityEvidence":"viewer_scoped_jellyfin_library","source":"jellyfin_library","tmdbId":2207,"artwork":{"posterKind":"poster","backdropKind":"backdrop"},"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.296,"tmdbVotes":867}}],"discover":[{"id":"movie-12-years-a-slave-2013","title":"12 Years a Slave","year":2013,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","History"],"overview":"In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay\u2026","poster":"assets/movie-12-years-a-slave-2013-poster.jpg","backdrop":"assets/movie-12-years-a-slave-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":76203,"ratings":{"rtCritics":95,"rtCriticsLabel":"Certified Fresh","rtAudience":90,"rtAudienceLabel":"Upright","imdb":8.1,"imdbVotes":796313,"tmdb":7.927,"tmdbVotes":12335}},{"id":"movie-101-dalmatians-ii-patch-s-london-adventure-2003","title":"101 Dalmatians II: Patch's London Adventure","year":2003,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Family","Animation","Adventure"],"overview":"Being one of 101 takes its toll on Patch, who doesn't feel unique. When he's accidentally left behind on moving day, he meets his idol, Thunderbolt, who enlists him on a publicity campaign.","poster":"assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg","backdrop":"assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":13654,"ratings":{"rtCritics":67,"rtCriticsLabel":"Fresh","rtAudience":41,"rtAudienceLabel":"Spilled","imdb":5.7,"imdbVotes":12343,"tmdb":6.025,"tmdbVotes":1274}},{"id":"movie-absolution-2024","title":"Absolution","year":2024,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Crime","Thriller"],"overview":"An aging ex-boxer gangster working as muscle for a Boston crime boss receives an upsetting diagnosis. Despite a faltering memory, he attempts to rectify the sins of his past and reconnect with his estranged\u2026","poster":"assets/movie-absolution-2024-poster.jpg","backdrop":"assets/movie-absolution-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":974453,"ratings":{"rtCritics":56,"rtCriticsLabel":"Rotten","rtAudience":32,"rtAudienceLabel":"Spilled","imdb":5.2,"imdbVotes":11413,"tmdb":5.906,"tmdbVotes":564}},{"id":"movie-addicted-2014","title":"Addicted","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.","poster":"assets/movie-addicted-2014-poster.jpg","backdrop":"assets/movie-addicted-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":235271,"ratings":{"rtCritics":7,"rtCriticsLabel":"Rotten","rtAudience":51,"rtAudienceLabel":"Spilled","imdb":5,"imdbVotes":13131,"tmdb":5.928,"tmdbVotes":466}},{"id":"movie-all-the-money-in-the-world-2017","title":"All the Money in the World","year":2017,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","History","Thriller"],"overview":"The story of the kidnapping of 16-year-old John Paul Getty III and the desperate attempt by his devoted mother to convince his billionaire grandfather Jean Paul Getty to pay the ransom.","poster":"assets/movie-all-the-money-in-the-world-2017-poster.jpg","backdrop":"assets/movie-all-the-money-in-the-world-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":446791,"ratings":{"rtCritics":79,"rtCriticsLabel":"Certified Fresh","rtAudience":66,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":97448,"tmdb":6.493,"tmdbVotes":2493}},{"id":"movie-the-alto-knights-2025","title":"The Alto Knights","year":2025,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama"],"overview":"Two of New York's most notorious organized crime bosses, Frank Costello and Vito Genovese, vie for control of the city's streets. Once the best of friends, petty jealousies and a series of betrayals place them on a\u2026","poster":"assets/movie-the-alto-knights-2025-poster.jpg","backdrop":"assets/movie-the-alto-knights-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1013601,"ratings":{"rtCritics":39,"rtCriticsLabel":"Rotten","rtAudience":71,"rtAudienceLabel":"Upright","imdb":5.9,"imdbVotes":23017,"tmdb":6.223,"tmdbVotes":485}},{"id":"movie-the-amazing-spider-man-2-2014","title":"The Amazing Spider-Man 2","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about\u2026","poster":"assets/movie-the-amazing-spider-man-2-2014-poster.jpg","backdrop":"assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":102382,"ratings":{"rtCritics":51,"rtCriticsLabel":"Rotten","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.6,"imdbVotes":611981,"tmdb":6.567,"tmdbVotes":14919}},{"id":"movie-ant-man-and-the-wasp-quantumania-2023","title":"Ant-Man and the Wasp: Quantumania","year":2023,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange\u2026","poster":"assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg","backdrop":"assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":640146,"ratings":{"rtCritics":46,"rtCriticsLabel":"Rotten","rtAudience":81,"rtAudienceLabel":"Upright","imdb":6,"imdbVotes":281738,"tmdb":6.197,"tmdbVotes":6236}},{"id":"movie-arctic-dogs-2019","title":"Arctic Dogs","year":2019,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Adventure","Comedy"],"overview":"Animals band together to save the day when the evil Otto Von Walrus hatches a sinister scheme to accelerate global warming and melt the Arctic Circle.","poster":"assets/movie-arctic-dogs-2019-poster.jpg","backdrop":"assets/movie-arctic-dogs-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":416153,"ratings":{"rtCritics":13,"rtCriticsLabel":"Rotten","rtAudience":60,"rtAudienceLabel":"Upright","imdb":4.9,"imdbVotes":2580,"tmdb":5.9,"tmdbVotes":148}},{"id":"movie-the-avengers-2012","title":"The Avengers","year":2012,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Science Fiction","Action","Adventure"],"overview":"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back\u2026","poster":"assets/movie-the-avengers-2012-poster.jpg","backdrop":"assets/movie-the-avengers-2012-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":24428,"ratings":{"rtCritics":91,"rtCriticsLabel":"Certified Fresh","rtAudience":91,"rtAudienceLabel":"Upright","imdb":8,"imdbVotes":1590693,"tmdb":8.084,"tmdbVotes":40262}},{"id":"movie-black-bear-2020","title":"Black Bear","year":2020,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"At a remote lake house in the Adirondack Mountains, a couple entertains an out-of-town guest looking for inspiration in her filmmaking. The group quickly falls into a calculated game of desire, manipulation, and\u2026","poster":"assets/movie-black-bear-2020-poster.jpg","backdrop":"assets/movie-black-bear-2020-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":624788,"ratings":{"rtCritics":89,"rtCriticsLabel":"Certified Fresh","rtAudience":64,"rtAudienceLabel":"Upright","imdb":6.5,"imdbVotes":18408,"tmdb":6.337,"tmdbVotes":315}},{"id":"movie-black-panther-2018","title":"Black Panther","year":2018,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"King T'Challa returns home to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions\u2026","poster":"assets/movie-black-panther-2018-poster.jpg","backdrop":"assets/movie-black-panther-2018-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284054,"ratings":{"rtCritics":96,"rtCriticsLabel":"Certified Fresh","rtAudience":null,"rtAudienceLabel":"Spilled","imdb":7.3,"imdbVotes":931120,"tmdb":7.36,"tmdbVotes":24190}},{"id":"movie-a-bronx-tale-1993","title":"A Bronx Tale","year":1993,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"Set in the Bronx during the tumultuous 1960s, an adolescent boy is torn between his honest, working-class father and a violent yet charismatic crime boss. Complicating matters is the youngster's growing attraction -\u2026","poster":"assets/movie-a-bronx-tale-1993-poster.jpg","backdrop":"assets/movie-a-bronx-tale-1993-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1607,"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":191930,"tmdb":7.913,"tmdbVotes":3075}},{"id":"movie-by-any-means-2026","title":"By Any Means","year":2026,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Thriller","History","Crime"],"overview":"In 1960s Mississippi, a young Black FBI agent is forced to partner with a notorious mafia hitman to investigate a wave of brutal civil rights murders.","poster":"assets/movie-by-any-means-2026-poster.jpg","backdrop":"assets/movie-by-any-means-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1380417,"ratings":{"rtCritics":49,"rtCriticsLabel":"Rotten","rtAudience":88,"rtAudienceLabel":"Upright","imdb":6.8,"imdbVotes":3534,"tmdb":6.6,"tmdbVotes":92}},{"id":"movie-captain-america-civil-war-2016","title":"Captain America: Civil War","year":2016,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Adventure","Action","Science Fiction"],"overview":"Following the events of Age of Ultron, the collective governments of the world pass an act designed to regulate all superhuman activity. This polarizes opinion amongst the Avengers, causing two factions to side with\u2026","poster":"assets/movie-captain-america-civil-war-2016-poster.jpg","backdrop":"assets/movie-captain-america-civil-war-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":271110,"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":89,"rtAudienceLabel":"Upright","imdb":7.8,"imdbVotes":954943,"tmdb":7.455,"tmdbVotes":24808}},{"id":"movie-captain-america-the-first-avenger-2011","title":"Captain America: The First Avenger","year":2011,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"During World War II, Steve Rogers is a sickly man from Brooklyn who's transformed into super-soldier Captain America to aid in the war effort. Rogers must stop the Red Skull \u2013 Adolf Hitler's ruthless head of\u2026","poster":"assets/movie-captain-america-the-first-avenger-2011-poster.jpg","backdrop":"assets/movie-captain-america-the-first-avenger-2011-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1771,"ratings":{"rtCritics":80,"rtCriticsLabel":"Certified Fresh","rtAudience":75,"rtAudienceLabel":"Upright","imdb":6.9,"imdbVotes":993453,"tmdb":7.012,"tmdbVotes":23473}},{"id":"movie-captain-america-the-winter-soldier-2014","title":"Captain America: The Winter Soldier","year":2014,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action","Adventure","Science Fiction"],"overview":"After the cataclysmic events in New York with The Avengers, Steve Rogers, aka Captain America is living quietly in Washington, D.C. and trying to adjust to the modern world. But when a S.H.I.E.L.D. colleague comes\u2026","poster":"assets/movie-captain-america-the-winter-soldier-2014-poster.jpg","backdrop":"assets/movie-captain-america-the-winter-soldier-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":100402,"ratings":{"rtCritics":90,"rtCriticsLabel":"Certified Fresh","rtAudience":92,"rtAudienceLabel":"Upright","imdb":7.7,"imdbVotes":995514,"tmdb":7.654,"tmdbVotes":20688}},{"id":"movie-careful-what-you-wish-for-2015","title":"Careful What You Wish For","year":2015,"type":"movie","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Thriller"],"overview":"A guy gets more than he bargained for after entering into an affair with the wife of an investment banker. Soon, a suspicious death and substantial life insurance policy embroil him in a scandal.","poster":"assets/movie-careful-what-you-wish-for-2015-poster.jpg","backdrop":"assets/movie-careful-what-you-wish-for-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":317744,"ratings":{"rtCritics":17,"rtCriticsLabel":"Rotten","rtAudience":61,"rtAudienceLabel":"Upright","imdb":5.6,"imdbVotes":9452,"tmdb":6.129,"tmdbVotes":446}},{"id":"tv-12-monkeys-2015","title":"12 Monkeys","year":2015,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Drama"],"overview":"The provocative story of Cole, a time traveler from a decimated future in a high-stakes race against the clock. Utilizing a dangerous and untested method of time travel, he journeys from 2043 to the present day on a\u2026","poster":"assets/tv-12-monkeys-2015-poster.jpg","backdrop":"assets/tv-12-monkeys-2015-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":60948,"ratings":{"rtCritics":88,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.441,"tmdbVotes":1245}},{"id":"tv-90-day-fiance-before-the-90-days-2017","title":"90 Day Fianc\u00e9: Before the 90 Days","year":2017,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Follow couples who have an existing relationship online, but haven't met in person. We'll experience their journey as they travel to the other's foreign country for the first time in an attempt to establish an\u2026","poster":"assets/tv-90-day-fiance-before-the-90-days-2017-poster.jpg","backdrop":"assets/tv-90-day-fiance-before-the-90-days-2017-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":73319,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.5,"tmdbVotes":45}},{"id":"tv-90-day-fiance-the-other-way-2019","title":"90 Day Fianc\u00e9: The Other Way","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Americans move to a foreign country all in the name of love. Will uprooting their lives in the United States pay off, or will they be packing up and moving back to America brokenhearted?","poster":"assets/tv-90-day-fiance-the-other-way-2019-poster.jpg","backdrop":"assets/tv-90-day-fiance-the-other-way-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":90046,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":34}},{"id":"tv-the-100-2014","title":"The 100","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Sci-Fi & Fantasy","Action & Adventure","Drama"],"overview":"100 years in the future, when the Earth has been abandoned due to radioactivity, the last surviving humans live on an ark orbiting the planet \u2014 but the ark won't last forever. So the repressive regime picks 100\u2026","poster":"assets/tv-the-100-2014-poster.jpg","backdrop":"assets/tv-the-100-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":48866,"ratings":{"rtCritics":93,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.878,"tmdbVotes":8855}},{"id":"tv-accused-2023","title":"Accused","year":2023,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama"],"overview":"In this crime anthology series, viewers discover how an ordinary person got caught up in an extraordinary situation, ultimately revealing how one wrong turn leads to another, until it\u2019s too late to turn back. Told\u2026","poster":"assets/tv-accused-2023-poster.jpg","backdrop":"assets/tv-accused-2023-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":202097,"ratings":{"rtCritics":61,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.851,"tmdbVotes":87}},{"id":"tv-after-life-2019","title":"After Life","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"Tony had a perfect life. But after his wife Lisa suddenly dies, Tony changes. After contemplating taking his own life, he decides instead to live long enough to punish the world by saying and doing whatever he likes\u2026","poster":"assets/tv-after-life-2019-poster.jpg","backdrop":"assets/tv-after-life-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":79410,"ratings":{"rtCritics":72,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.902,"tmdbVotes":1169}},{"id":"tv-all-creatures-great-and-small-1978","title":"All Creatures Great and Small","year":1978,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy","Drama"],"overview":"The trials and misadventures of the staff at a country veterinary office in Yorkshire. James Herriot, a young animal surgeon, moves to a small Yorkshire town to begin his first job.","poster":"assets/tv-all-creatures-great-and-small-1978-poster.jpg","backdrop":"assets/tv-all-creatures-great-and-small-1978-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":7406,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":50}},{"id":"tv-american-housewife-2016","title":"American Housewife","year":2016,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A family comedy narrated by Katie, a strong-willed mother, raising her flawed family in a wealthy town filled with perfect wives and their perfect offspring.","poster":"assets/tv-american-housewife-2016-poster.jpg","backdrop":"assets/tv-american-housewife-2016-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":67117,"ratings":{"rtCritics":null,"rtCriticsLabel":"Rotten","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.7,"tmdbVotes":187}},{"id":"tv-atlantis-2013","title":"Atlantis","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy"],"overview":"A fantasy drama set in a world of legendary heroes and mythical creatures. Far from home and desperate for answers, Jason washes up on the shores of an ancient land. A mysterious place; a world of bull leaping, of\u2026","poster":"assets/tv-atlantis-2013-poster.jpg","backdrop":"assets/tv-atlantis-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":47054,"ratings":{"rtCritics":62,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.4,"tmdbVotes":155}},{"id":"tv-avatar-the-last-airbender-2024","title":"Avatar: The Last Airbender","year":2024,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Action & Adventure","Sci-Fi & Fantasy","Drama"],"overview":"A young boy known as the Avatar must master the four elemental powers to save a world at war \u2014 and fight a ruthless enemy bent on stopping him.","poster":"assets/tv-avatar-the-last-airbender-2024-poster.jpg","backdrop":"assets/tv-avatar-the-last-airbender-2024-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":82452,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.8,"tmdbVotes":1406}},{"id":"tv-avatar-seven-havens-2026","title":"Avatar: Seven Havens","year":2026,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"In a world shattered by a devastating cataclysm, a young Earthbender discovers she's the new Avatar after Korra. But in this dangerous era, that title marks her as humanity's destroyer, not its savior. Hunted by both\u2026","poster":"assets/tv-avatar-seven-havens-2026-poster.jpg","backdrop":"assets/tv-avatar-seven-havens-2026-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":284833,"ratings":{"rtCritics":100,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.9,"tmdbVotes":183}},{"id":"tv-batman-the-brave-and-the-bold-2008","title":"Batman: The Brave and the Bold","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Animation","Action & Adventure","Sci-Fi & Fantasy"],"overview":"The Caped Crusader is teamed up with Blue Beetle, Green Arrow, Aquaman and countless others in his quest to uphold justice.","poster":"assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","backdrop":"assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":15804,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.6,"tmdbVotes":295}},{"id":"tv-beat-bobby-flay-2013","title":"Beat Bobby Flay","year":2013,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Reality"],"overview":"Two talented chefs go head-to-head for the chance to Beat Bobby Flay. To get to Bobby the chefs must first face off against each other, creating a spectacular dish with a secret ingredient of Bobby's choice. Judges\u2026","poster":"assets/tv-beat-bobby-flay-2013-poster.jpg","backdrop":"assets/tv-beat-bobby-flay-2013-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":62481,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.5,"tmdbVotes":31}},{"id":"tv-benson-1979","title":"Benson","year":1979,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A butler deals with life at the governor's mansion.","poster":"assets/tv-benson-1979-poster.jpg","backdrop":"assets/tv-benson-1979-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1849,"ratings":{"rtCritics":null,"rtCriticsLabel":null,"rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7,"tmdbVotes":46}},{"id":"tv-black-rabbit-2025","title":"Black Rabbit","year":2025,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"A rising-star restaurateur is forced into New York's criminal underworld when his chaotic brother returns to town with loan sharks on his trail.","poster":"assets/tv-black-rabbit-2025-poster.jpg","backdrop":"assets/tv-black-rabbit-2025-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":249039,"ratings":{"rtCritics":66,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":6.983,"tmdbVotes":240}},{"id":"tv-black-ish-2014","title":"black-ish","year":2014,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Comedy"],"overview":"A family man struggles to gain a sense of cultural identity while raising his kids in a predominantly white, upper-middle-class neighborhood.","poster":"assets/tv-black-ish-2014-poster.jpg","backdrop":"assets/tv-black-ish-2014-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":61381,"ratings":{"rtCritics":92,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.1,"tmdbVotes":352}},{"id":"tv-breaking-bad-2008","title":"Breaking Bad","year":2008,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Drama","Crime"],"overview":"Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to\u2026","poster":"assets/tv-breaking-bad-2008-poster.jpg","backdrop":"assets/tv-breaking-bad-2008-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"center_crop","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":1396,"ratings":{"rtCritics":96,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":8.959,"tmdbVotes":19030}},{"id":"tv-the-capture-2019","title":"The Capture","year":2019,"type":"tv","runtimeSeconds":null,"seasonCount":null,"communityRating":null,"criticRating":null,"genres":["Crime","Drama","Mystery"],"overview":"When soldier Shaun Emery's conviction for a murder in Afghanistan is overturned due to flawed video evidence, he returns to life as a free man with his young daughter. But when damning CCTV footage from a night out\u2026","poster":"assets/tv-the-capture-2019-poster.jpg","backdrop":"assets/tv-the-capture-2019-backdrop.jpg","played":null,"playCount":null,"resumeSeconds":null,"progress":null,"addedAt":null,"lastPlayedAt":null,"availability":"not_here","availabilityEvidence":"retained_profile_membership_and_native_request_ledger","source":"materialized_personal_recommendation","rank":null,"watchStateKnown":false,"artwork":{"posterKind":"poster","backdropKind":"poster_crop"},"availabilityIsSnapshot":true,"tmdbId":93166,"ratings":{"rtCritics":97,"rtCriticsLabel":"Fresh","rtAudience":null,"rtAudienceLabel":null,"imdb":null,"imdbVotes":null,"tmdb":7.634,"tmdbVotes":361}}],"discoverySource":{"state":"materialized_snapshot","rankKnown":false,"selection":"First 18 alphabetically indexed movie stubs and first 18 series stubs, not ranked feed order","observedAt":"2026-11-04T20:42:12.030108+00:00"}}],"cast":{"movie:14069":[{"name":"Riisa Naka","character":"Makoto Konno (voice)","img":"tqFHQiJUycuob3ytvilaAWDlbQ8.jpg","order":0},{"name":"Takuya Ishida","character":"Chiaki Mamiya (voice)","img":"aHQk6XxqyJinfvrVRADEIg31nOk.jpg","order":1},{"name":"Mitsutaka Itakura","character":"Kousuke Tsuda (voice)","img":"mnKYU7HIsuRkx3YjdRO5FLXCdX5.jpg","order":2},{"name":"Ayami Kakiuchi","character":"Yuri Hayakawa (voice)","img":"lUhVV9u5VGMCNdCAVhGzg8GFSdo.jpg","order":3},{"name":"Mitsuki Tanimura","character":"Kaho Fujitani (voice)","img":"xV9UrP8kZdxOiTI2efDFhp3t4iA.jpg","order":4},{"name":"Yuki Sekido","character":"Miyuki Konno (voice)","img":"7LVYeFTgUpjYDrxnU7Wm5ZQUX9t.jpg","order":5},{"name":"Utawaka Katsura","character":"Makoto's Father (voice)","img":"j0GXzLelO1dOzI7d1WVIwQXhLz8.jpg","order":6},{"name":"Midori Ando","character":"Makoto's Mother (voice)","img":"26R1LBiSOkkP08saSsz6LmVTY16.jpg","order":7},{"name":"Fumihiko Tachiki","character":"Fukushima (voice)","img":null,"order":8},{"name":"Keiko Yamamoto","character":"Auntie (voice)","img":null,"order":9},{"name":"Shiori Yokohari","character":"Moriko Uesugi (voice)","img":null,"order":10},{"name":"Sonoka Matsuoka","character":"Sekimi Nowake (voice)","img":null,"order":11},{"name":"Takayuki Handa","character":"Kato (voice)","img":null,"order":12},{"name":"Maho Kurashima","character":"Female Newscaster A (voice)","img":null,"order":13},{"name":"Keiko Aizawa","character":"School Nurse / Answering Machine (voice)","img":null,"order":14}],"movie:210479":[{"name":"Tom Hardy","character":"Ivan Locke","img":"d81K0RH8UX7tZj49tZaQhZ9ewH.jpg","order":0},{"name":"Ruth Wilson","character":"Katrina (voice)","img":"fdFVatb9pt5dfT9tDK8tikKWafS.jpg","order":1},{"name":"Andrew Scott","character":"Donal (voice)","img":"4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg","order":2},{"name":"Olivia Colman","character":"Bethan (voice)","img":"lawJUdbLb1YW0niaYjiVGR9IBG8.jpg","order":3},{"name":"Tom Holland","character":"Eddie (voice)","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":4},{"name":"Ben Daniels","character":"Gareth (voice)","img":"phEIvq0Ay2KjI5ewmVvpQ1a29GJ.jpg","order":5},{"name":"Bill Milner","character":"Sean (voice)","img":"bLgmpSFgEiSFNshqmdl3EIZkRZm.jpg","order":6},{"name":"Alice Lowe","character":"Sister Margaret (voice)","img":"v4sRxLpRpHEd9vbk43wm9VL0v0K.jpg","order":7},{"name":"Danny Webb","character":"Cassidy (voice)","img":null,"order":8},{"name":"Lee Ross","character":"PC Davids (voice)","img":null,"order":9},{"name":"Silas Carson","character":"Dr. Gullu (voice)","img":null,"order":10},{"name":"Kirsty Dillon","character":"Gareth's Wife (voice)","img":null,"order":11}],"movie:318917":[{"name":"Oliver Masucci","character":"Adolf Hitler","img":"fjJc03z1cOlMxLZbC1qihsq3C37.jpg","order":0},{"name":"Fabian Busch","character":"Fabian Sawatzki","img":"lpEs9yDsMQlsbm0RVsvDIleCXcI.jpg","order":1},{"name":"Christoph Maria Herbst","character":"Christoph Sensenbrink","img":"8v3lDa0QCJlDtOx9flqlMNIA3uf.jpg","order":2},{"name":"Katja Riemann","character":"Bellini","img":"waayy0jpNbxLDQ8JO1uRDng8SLF.jpg","order":3},{"name":"Franziska Wulf","character":"Kr\u00f6meier","img":"yxGsAKnrJZs2g6q4unNgkfm7NAA.jpg","order":4},{"name":"Lars Rudolph","character":"Shop Owner","img":"qiWu2gphSg64igYl3Nx0hEIOQF3.jpg","order":5},{"name":"Michael Kessler","character":"Michael Witzigmann","img":"sh4ANkKoIHgmHtkKQbJvF8y0Fjq.jpg","order":6},{"name":"Thomas Thieme","character":"K\u00e4rrner","img":"omWXGbeRR32MWuYhpnLOSqJ6xkO.jpg","order":7},{"name":"Frank Plasberg","character":"Frank Plasberg","img":null,"order":8},{"name":"Thomas M. K\u00f6ppl","character":"Etiquette Coach","img":null,"order":9},{"name":"Marc-Marvin Israel","character":"Football Boy","img":null,"order":10},{"name":"David Gebigke","character":"Football Boy","img":null,"order":11},{"name":"Paul Busche","character":"Football Boy","img":null,"order":12},{"name":"Gerdy Zint","character":"Pantomime","img":null,"order":13},{"name":"Nancy Maria Br\u00fcning","character":"Mother with Stroller","img":null,"order":14}],"movie:42994":[{"name":"Tsutomu Isobe","character":"Heinz Beckner (voice)","img":"qP5vQMIbLNvSsSFkEMLZRVt4ZW5.jpg","order":0},{"name":"Koichi Yamadera","character":"Miguel Costrela (voice)","img":"biov5jENp2XYfehkcdnRvi796Wi.jpg","order":1},{"name":"Shozo Iizuka","character":"Ivanov (voice)","img":"msqe5A1q5YUUND0RhzB9dw1AEAO.jpg","order":2},{"name":"Shigeru Chiba","character":"Aoshima (voice)","img":"mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg","order":3},{"name":"Gara Takashima","character":"Eva Friedel (voice)","img":"5L0kzTVITmOQdQ94NhicKM8ecCX.jpg","order":4},{"name":"Ami Hasegawa","character":"Emily (voice)","img":null,"order":5},{"name":"Hideyuki Hori","character":"Nobuo Tanaka (voice)","img":"fbWWkhe7GVSlhBljY89rJd01OnA.jpg","order":6},{"name":"Kenichi Ogata","character":"Oomaeda (voice)","img":"iQLEawIP2593yrGy22GzLSpnfgl.jpg","order":7},{"name":"Osamu Saka","character":"Kamata (voice)","img":null,"order":8},{"name":"Hisako Kyoda","character":"Grandma (voice)","img":null,"order":9},{"name":"Michio Hazama","character":"Nirasaki (voice)","img":null,"order":10},{"name":"Kayoko Fujii","character":"Sakiko (voice)","img":null,"order":11},{"name":"Ryoichi Tanaka","character":"General Officer (voice)","img":null,"order":12},{"name":"Tetsuya Iwanaga","character":"Nobuo's Brother (voice)","img":null,"order":13},{"name":"Yuu Hayashi","character":"Boy (voice)","img":null,"order":14}],"movie:123431":[{"name":"Snoop Dogg","character":"Self","img":"rbxBK2m6fEq0oHeMoBUMVYkJEdJ.jpg","order":0},{"name":"Dr. Dre","character":"Self","img":"xm2pYFBggiXSkFBV2I612i3fDd3.jpg","order":1},{"name":"Daz Dillinger","character":"Self","img":"nAxS5l9vb1C8ltCp9yAa0f6lazu.jpg","order":2},{"name":"Angela Hunte","character":"Self","img":null,"order":3},{"name":"Ariel Rechtshaid","character":"Self","img":"lF06PTNYrlXeJ3DYApvOcPXJr87.jpg","order":4},{"name":"Dre Skull","character":"Self","img":null,"order":5},{"name":"Jahdan Blakkamoore","character":"Self","img":null,"order":6},{"name":"Bunny Wailer","character":"Self","img":"oMHMLhoV3ry8aOfdvs1kxyowhNq.jpg","order":7},{"name":"Damian Marley","character":"Self","img":null,"order":8},{"name":"Louis Farrakhan","character":"Self","img":null,"order":9},{"name":"Shante Broadus","character":"Self","img":null,"order":10},{"name":"Cori Broadus","character":"Self","img":null,"order":11},{"name":"Maxine Stowe","character":"Self","img":null,"order":12},{"name":"Stewart Copeland","character":"Self","img":null,"order":13},{"name":"Diplo","character":"Self","img":null,"order":14}],"movie:1715492":[{"name":"John Scimone","character":"Self","img":null,"order":0},{"name":"Meghan Scimone","character":"Self","img":null,"order":1},{"name":"Patricia Sandoval","character":"Self","img":null,"order":2},{"name":"Nicholas Taliaferro","character":"Self","img":null,"order":3},{"name":"Stefania Vincenzi","character":"Self","img":null,"order":4},{"name":"Manrico Giampedroni","character":"Self","img":null,"order":5},{"name":"Rose Metcalf","character":"Self","img":null,"order":6},{"name":"Francesco Schettino","character":"Self (Archive Footage)","img":null,"order":7},{"name":"Manoj Singh","character":"Self","img":null,"order":8},{"name":"Francesco Boaria","character":"Self","img":null,"order":9},{"name":"Pier Luigi Foschi","character":"Self","img":null,"order":10},{"name":"Alessandro Cantelli-Forti","character":"Self","img":null,"order":11},{"name":"Barbie Latza Nadeau","character":"Self","img":null,"order":12}],"tv:83867":[{"name":"Diego Luna","character":"Cassian Andor","img":"qwbjcyDN37OHiln4a6P4LP31o7F.jpg","order":0},{"name":"Anton Lesser","character":"Major Partagaz","img":"x0lmqgkmphvOCSTHk2oxsaxA9Ra.jpg","order":3},{"name":"Muhannad Bhaier","character":"Wilmon","img":"5hn7WeXi6wguwxTPTSmcmzHAs9.jpg","order":4},{"name":"Stellan Skarsg\u00e5rd","character":"Luthen Rael","img":"mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg","order":5},{"name":"Denise Gough","character":"Dedra Meero","img":"wWcby4ARYqr8d92ZdjyQcfaiJBW.jpg","order":6},{"name":"Kyle Soller","character":"Syril Karn","img":"rLzduQpYJnZJKkXv3jRc7MDHwoI.jpg","order":7},{"name":"Adria Arjona","character":"Bix Caleen","img":"hzu7OqTmNCT1xdCdmwBSf2yJvaO.jpg","order":8},{"name":"Genevieve O'Reilly","character":"Mon Mothma","img":"4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg","order":9},{"name":"Faye Marsay","character":"Vel Sartha","img":null,"order":10},{"name":"Elizabeth Dulau","character":"Kleya Marki","img":null,"order":11},{"name":"Alastair Mackenzie","character":"Perrin Fertha","img":null,"order":501},{"name":"Dave Chapman","character":"B2EMO (voice)","img":null,"order":509},{"name":"Robert Emms","character":"Supervisor Lonni Jung","img":null,"order":538},{"name":"Michael Jenn","character":"Supervisor Lagret","img":null,"order":539},{"name":"Jacob James Beswick","character":"Attendant Heert","img":null,"order":619}],"tv:76479":[{"name":"Karl Urban","character":"Billy Butcher","img":"7Y96dAfg0HcFrcLjlD5eD9N0uj4.jpg","order":0},{"name":"Jack Quaid","character":"Hugh 'Hughie' Campbell","img":"320qW5yEbxpmyxQ3evmClJbtKag.jpg","order":1},{"name":"Antony Starr","character":"Homelander","img":"xx3As5SWcE8vYOKZgtjDjqmT3jc.jpg","order":2},{"name":"Erin Moriarty","character":"Annie January / Starlight","img":"2tFblvjBRUYwcVT1V1i79Ppd6wG.jpg","order":3},{"name":"Jessie T. Usher","character":"Reggie Franklin / A-Train","img":null,"order":5},{"name":"Laz Alonso","character":"Marvin T. 'Mother's Milk' Milk / M.M.","img":"nmgOd3X2Xn3jIp9OLCRJzLExRWN.jpg","order":6},{"name":"Chace Crawford","character":"Kevin Moskowitz / The Deep","img":"yWZILsr2mBQBCG1LOcCYYshIbts.jpg","order":7},{"name":"Tomer Capone","character":"Serge / Frenchie","img":"r6s7dkYZUJpd00C7p9itT6Gp8XB.jpg","order":8},{"name":"Karen Fukuhara","character":"Kimiko Miyashiro","img":"sG6CWLdENCtzukNFkFYyPxqyRmS.jpg","order":9},{"name":"Nathan Mitchell","character":"Earving / Black Noir","img":null,"order":10},{"name":"Colby Minifie","character":"Ashley Barrett","img":null,"order":13},{"name":"Claudia Doumit","character":"Victoria 'Vic' Neuman","img":null,"order":14},{"name":"Cameron Crovetti","character":"Ryan","img":null,"order":16},{"name":"Dominique McElligott","character":"Maggie Shaw / Queen Maeve","img":null,"order":242},{"name":"Nneka Elliott","character":"Nneka Elliott","img":null,"order":894}],"tv:1215":[{"name":"David Duchovny","character":"Hank Moody","img":"fD8GuLHOOSSAG98VjwN3NhAmqAJ.jpg","order":0},{"name":"Natascha McElhone","character":"Karen van der Beek","img":"9pxaoasotR1pdCXYSfN1pkm0geO.jpg","order":1},{"name":"Evan Handler","character":"Charlie Runkle","img":"yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg","order":2},{"name":"Pamela Adlon","character":"Marcy Runkle","img":"5eIRHUbNOH7ibXa0IfUfFvj5I5L.jpg","order":3},{"name":"Rachel Miner","character":"Dani","img":null,"order":4},{"name":"Madeleine Martin","character":"Becca Moody","img":"hOntuFYAMwn9QaXT0AwLCPU79ce.jpg","order":5},{"name":"Madeline Zima","character":"Mia Lewis","img":"6hmUs5Q1EETapn32yKNt3c3i1sn.jpg","order":6},{"name":"Callum Keith Rennie","character":"Lew Ashby","img":null,"order":7},{"name":"Carla Gallo","character":"Daisy","img":null,"order":513},{"name":"Embeth Davidtz","character":"Felicia Koons","img":null,"order":514},{"name":"Kathleen Turner","character":"Sue Collini","img":null,"order":522},{"name":"Jason Beghe","character":"Richard Bates","img":"hrTwOPWek7nkPJgFhh6inFu7vSS.jpg","order":534},{"name":"Stephen Tobolowsky","character":"Stu Beggs","img":"uL8yfcm0dCFYFQt052kczVQVvac.jpg","order":671},{"name":"Michael Imperioli","character":"Rick Rath","img":null,"order":843},{"name":"Oliver Cooper","character":"Levon","img":null,"order":844}],"tv:1437":[{"name":"Nathan Fillion","character":"Mal Reynolds","img":"aW6vCxkUZtwb6iH2Wf88Uq0XNVv.jpg","order":0},{"name":"Gina Torres","character":"Zo\u00eb Washburne","img":"ukDNHgvEgdU95eaw6pngiAPucv4.jpg","order":1},{"name":"Alan Tudyk","character":"Hoban Washburne","img":"jUuUbPuMGonFT5E2pcs4alfqaCN.jpg","order":2},{"name":"Morena Baccarin","character":"Inara Serra","img":"4gyHyg6FJ1oFczOm5pmMkdEEo2J.jpg","order":3},{"name":"Adam Baldwin","character":"Jayne Cobb","img":"lfbyJWNWGbiYfU0Yr1tjn6Trd2V.jpg","order":4},{"name":"Jewel Staite","character":"Kaylee Frye","img":"ztxDIZvuPreUixO6ZPxwkCg2UlO.jpg","order":5},{"name":"Sean Maher","character":"Simon Tam","img":"o7splcOIVAdbHnwohJOUAvXWVlk.jpg","order":6},{"name":"Michael Fairman","character":"Adelai Niska","img":null,"order":7},{"name":"Summer Glau","character":"River Tam","img":"rDTxblyw0AwU5oSeksgP7GwMyrF.jpg","order":8},{"name":"Ron Glass","character":"Derrial Book","img":null,"order":9},{"name":"Gregory Itzin","character":"Magistrate Higgins","img":null,"order":10},{"name":"Isabella Hofmann","character":"Regan Tam","img":null,"order":11},{"name":"Jeff Ricketts","character":"Man","img":null,"order":540},{"name":"Dennis Cockrum","character":"Other Man","img":null,"order":541},{"name":"Mark Sheppard","character":"Badger","img":null,"order":613}],"tv:67070":[{"name":"Phoebe Waller-Bridge","character":"Fleabag","img":"5VbadQsYCKdiJysP01LJFPmYB8A.jpg","order":0},{"name":"Sian Clifford","character":"Claire","img":"rIqcwewXdkrDTMTwX56QPa7MNTC.jpg","order":1},{"name":"Jenny Rainsford","character":"Boo","img":"pVwR9hsEiHzVTo9giCjR3vIfa8W.jpg","order":2},{"name":"Andrew Scott","character":"The Priest","img":"4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg","order":3},{"name":"Ben Aldridge","character":"Arsehole Guy","img":null,"order":507},{"name":"Jo Martin","character":"Pam","img":null,"order":509},{"name":"Olivia Colman","character":"Godmother","img":"lawJUdbLb1YW0niaYjiVGR9IBG8.jpg","order":510},{"name":"Bill Paterson","character":"Dad","img":"mk5vpYLmER7asqOhEwVKXD3P3YE.jpg","order":511},{"name":"Jamie Demetriou","character":"Bus Rodent","img":null,"order":512},{"name":"Hugh Skinner","character":"Harry","img":"7MHdNxS1gRnx0ZQGPrfS5eRjMe3.jpg","order":513},{"name":"Hugh Dennis","character":"Bank Manager","img":null,"order":514},{"name":"Brett Gelman","character":"Martin","img":"ub2IuMWFNQGYghHTPq0lpmn2Ue0.jpg","order":515},{"name":"Anthony Welsh","character":"Jack","img":null,"order":516},{"name":"Kae Alexander","character":"Elaine","img":null,"order":523},{"name":"David Hargreaves","character":"Chatty Joe","img":null,"order":530}],"tv:40075":[{"name":"Jason Ritter","character":"Dipper Pines (voice)","img":null,"order":0},{"name":"Kristen Schaal","character":"Mabel Pines (voice)","img":null,"order":1},{"name":"Alex Hirsch","character":"Grunkle Stan / Soos Ramirez / Bill Cipher / Old Man McGucket (voice)","img":null,"order":2},{"name":"Linda Cardellini","character":"Wendy Corduroy (voice)","img":null,"order":3},{"name":"Will Forte","character":"Tyler Cutebiker (voice)","img":null,"order":13},{"name":"Gregg Turkington","character":"Young Toby Determined (voice)","img":null,"order":166},{"name":"Carl Faruolo","character":"Grenda (voice)","img":null,"order":170},{"name":"Jennifer Coolidge","character":"Lazy Susan (voice)","img":null,"order":175},{"name":"Dee Bradley Baker","character":"Waddles (voice) / Additional Voices (voice)","img":null,"order":399},{"name":"John DiMaggio","character":"Manly Dan / Pinball Cowboy Skeleton (voice)","img":null,"order":469},{"name":"Kevin Michael Richardson","character":"Sheriff Blubs / Ghost / Ancient Heads / Basketball Player / Manly Dan (voice)","img":null,"order":561},{"name":"Grey DeLisle","character":"Mrs. Gleeful (voice)","img":null,"order":582},{"name":"Matt Chapman","character":"Mermando / Additional Voices (voice)","img":null,"order":591},{"name":"Niki Yang","character":"Candy Chiu (voice)","img":null,"order":650},{"name":"T.J. Miller","character":"Robbie Valentino (voice)","img":null,"order":664}],"tv:95557":[{"name":"Steven Yeun","character":"Mark Grayson / Invincible (voice)","img":null,"order":0},{"name":"Sandra Oh","character":"Debbie Grayson (voice)","img":null,"order":1},{"name":"J.K. Simmons","character":"Nolan Grayson / Omni-Man (voice)","img":null,"order":2},{"name":"Dan Navarro","character":"Hazmat Responder / Additional Voices (voice)","img":null,"order":547},{"name":"Ami Shukla","character":"Additional Voices (voice)","img":null,"order":548},{"name":"Nyima Funk","character":"Additional Voices (voice)","img":null,"order":558},{"name":"Zazie Beetz","character":"Amber (voice)","img":null,"order":567},{"name":"Walton Goggins","character":"Cecil (voice)","img":null,"order":568},{"name":"Gillian Jacobs","character":"Eve (voice)","img":null,"order":574},{"name":"Jason Mantzoukas","character":"Rex / Zombie #2 (voice)","img":null,"order":588},{"name":"Chris Diamantopoulos","character":"Pete / Donald / Todd (voice)","img":null,"order":596},{"name":"Khary Payton","character":"Black Samson / Martian (voice)","img":null,"order":601},{"name":"Ross Marquand","character":"Immortal / Rudy / Kursk (voice)","img":null,"order":628},{"name":"Grey DeLisle","character":"Monster Girl / Thula / Woman (voice)","img":null,"order":656},{"name":"Bobby Kesselman","character":"Additional Voices (voice)","img":null,"order":734}],"tv:33880":[{"name":"Janet Varney","character":"Korra (voice)","img":null,"order":0},{"name":"David Faustino","character":"Mako (voice)","img":null,"order":1},{"name":"P.J. Byrne","character":"Bolin (voice)","img":null,"order":2},{"name":"Seychelle Gabriel","character":"Asami Sato (voice)","img":null,"order":3},{"name":"Mindy Sterling","character":"Lin Beifong (voice)","img":null,"order":6},{"name":"Adrian LaTourelle","character":"Unalaq (voice)","img":null,"order":9},{"name":"J.K. Simmons","character":"Mushi (voice)","img":null,"order":68},{"name":"Dee Bradley Baker","character":"Juicy (voice)","img":null,"order":324},{"name":"Kiernan Shipka","character":"Jinora (voice)","img":null,"order":500},{"name":"Darcy Rose Byrnes","character":"Ikki (voice)","img":null,"order":503},{"name":"Logan Wells","character":"Meelo (voice)","img":null,"order":504},{"name":"John Michael Higgins","character":"Varrick (voice)","img":null,"order":549},{"name":"Richard Riehle","character":"Bumi (voice)","img":null,"order":551},{"name":"Maria Bamford","character":"Pema (voice)","img":null,"order":556},{"name":"Jeff Bennett","character":"Shiro Shinobi / Fishmonger / Ring Announcer (voice)","img":null,"order":568}],"tv:86831":[{"name":"Josh Brener","character":"K-VRC (voice)","img":null,"order":237},{"name":"Matthew Yang King","character":"Host (voice)","img":null,"order":520},{"name":"Nolan North","character":"Vladimir (voice)","img":null,"order":555},{"name":"Ike Amadi","character":"Platoon Sergeant (voice)","img":null,"order":566},{"name":"Time Winters","character":"Dicko (voice)","img":null,"order":584},{"name":"Emily O'Brien","character":"Eve (voice)","img":null,"order":592},{"name":"Chris Cox","character":"Male Backgound (voice)","img":null,"order":592},{"name":"Noshir Dalal","character":"Beaumont (voice)","img":null,"order":599},{"name":"Michelle C. Bonilla","character":"Commander / Pilot (voice)","img":null,"order":599},{"name":"Stanton Lee","character":"Sergeant Howard (voice)","img":null,"order":602},{"name":"Fred Tatasciore","character":"Creature / TV Santa (voice)","img":null,"order":608},{"name":"Scott Whyte","character":"Trot (voice)","img":null,"order":618},{"name":"Caitlin McKenna","character":"Additional Voices (voice)","img":null,"order":622},{"name":"Piotr Michael","character":"Investor (voice)","img":null,"order":636},{"name":"Chris Parnell","character":"The Cat (voice)","img":null,"order":653}],"tv:68126":[{"name":"Trond Fausa Aurv\u00e5g","character":"Rufus","img":null,"order":0},{"name":"Jon \u00d8igarden","character":"Jarl Varg","img":null,"order":1},{"name":"K\u00e5re Conradi","character":"Orm","img":null,"order":2},{"name":"Silje Torp","character":"Fr\u00f8ya","img":null,"order":3},{"name":"Mikkel Bratt Silset","character":"Ragnar","img":null,"order":4},{"name":"Nils J\u00f8rgen Kaalstad","character":"Arvid","img":null,"order":5},{"name":"Kristine Riis","character":"Liv","img":null,"order":6},{"name":"Bj\u00f8rn Hallgeir Myrene","character":"Torstein Hund","img":null,"order":7},{"name":"Marian Saastad Ottesen","character":"Hildur","img":null,"order":8},{"name":"Nikis Theophilakis","character":"Olvar / Eigil","img":null,"order":9},{"name":"\u00d8ystein Martinsen","character":"Kark","img":null,"order":10},{"name":"Christian Skolmen","character":"H\u00e5kon","img":null,"order":11},{"name":"Erik Aleksander Schjerven","character":"Magnus","img":null,"order":12},{"name":"Ingar Helge Gimle","character":"Nomadeh\u00f8vding","img":null,"order":13},{"name":"Finn Schau","character":"Lovgiveren","img":null,"order":14}],"tv:60625":[{"name":"Justin Roiland","character":"Rick Sanchez / Morty Smith (voice)","img":null,"order":0},{"name":"Chris Parnell","character":"Jerry Smith (voice)","img":null,"order":1},{"name":"Spencer Grammer","character":"Summer Smith (voice)","img":null,"order":2},{"name":"Sarah Chalke","character":"Beth Smith (voice)","img":null,"order":3},{"name":"Ian Cardoni","character":"Rick Sanchez (voice)","img":null,"order":4},{"name":"Harry Belden","character":"Morty Smith (voice)","img":null,"order":6},{"name":"Dan Harmon","character":"Alejandro (voice)","img":null,"order":688},{"name":"Kari Wahlgren","character":"Roy's Wife (voice)","img":null,"order":715},{"name":"Maurice LaMarche","character":"Roy's Teacher / Gromflamite Fed (voice)","img":null,"order":743},{"name":"Tom Kenny","character":"Conroy / Federation Admiral / Additional Voices (voice)","img":null,"order":750},{"name":"Ryan Ridley","character":"Head Vampire (voice)","img":null,"order":752},{"name":"Brandon Johnson","character":"Mr. Goldenfold / Black Mailman (voice)","img":null,"order":755},{"name":"Nolan North","character":"Jerry's High School Friend (voice)","img":null,"order":757},{"name":"Rob Schrab","character":"Town Resident (voice)","img":null,"order":768},{"name":"Nick Reczynski","character":"Snake Humanoid (voice)","img":null,"order":815}],"tv:261579":[{"name":"Keanu Reeves","character":"Pilot (voice)","img":null,"order":500},{"name":"Parry Shen","character":"MC (young) (voice)","img":null,"order":501},{"name":"Ping Wu","character":"MC (Old) (voice)","img":null,"order":502},{"name":"Lydia Look","character":"Li (voice)","img":null,"order":503},{"name":"Nelson Lee","character":"Sean (voice)","img":null,"order":504},{"name":"Feodor Chin","character":"Thug (voice)","img":null,"order":505},{"name":"Rae Lim","character":"MC (Child) (voice)","img":null,"order":506},{"name":"Erin Yvette","character":"The Voice (voice)","img":null,"order":507},{"name":"Temuera Morrison","character":"Old Salt (voice)","img":null,"order":508},{"name":"Patrick Schwarzenegger","character":"The Kid (voice)","img":null,"order":509},{"name":"Steve Blum","character":"Dispatch / Mechanic (voice)","img":null,"order":510},{"name":"Noah Manzoor","character":"Solon (voice)","img":null,"order":511},{"name":"Madeleine Knight","character":"Mora (voice)","img":null,"order":512},{"name":"Aleks Le","character":"Yi Xing (voice)","img":null,"order":556},{"name":"Fred Tatasciore","character":"Helldiver (voice)","img":null,"order":558}],"tv:125988":[{"name":"Rebecca Ferguson","character":"Juliette Nichols","img":null,"order":0},{"name":"Common","character":"Robert Sims","img":null,"order":1},{"name":"Chinaza Uche","character":"Paul Billings","img":null,"order":2},{"name":"Shane McRae","character":"Knox","img":null,"order":4},{"name":"Remmie Milner","character":"Shirley Campbell","img":null,"order":6},{"name":"Alexandria Riley","character":"Camille Sims","img":null,"order":7},{"name":"Billy Postlethwaite","character":"Hank Murphy","img":null,"order":12},{"name":"Tim Robbins","character":"Bernard Holland","img":null,"order":15},{"name":"Clare Perkins","character":"Carla McLain","img":null,"order":17},{"name":"Harriet Walter","character":"Martha Walker","img":null,"order":18},{"name":"Avi Nash","character":"Lukas Kyle","img":null,"order":20},{"name":"Rick Gomez","character":"Patrick Kennedy","img":null,"order":21},{"name":"Olatunji Ayofe","character":"Teddy","img":null,"order":502},{"name":"Chipo Chung","character":"Sandy","img":null,"order":506},{"name":"Caitlin Zoz","character":"Kathleen Billings","img":null,"order":542}],"tv:2190":[{"name":"Isaac Hayes","character":"Chef (voice)","img":null,"order":2},{"name":"Mary Kay Bergman","character":"Liane Cartman / Wendy Testaburger / Sharon Marsh / Sheila Broflovski (voice)","img":null,"order":3},{"name":"Mona Marshall","character":"Sheila Broflovski (voice)","img":null,"order":4},{"name":"Sebastian Yu","character":"Ike Broflovski (voice)","img":null,"order":7},{"name":"Jessie Jo Thomas","character":"","img":null,"order":8},{"name":"Milan Agnone","character":"Ike Broflovski (voice)","img":null,"order":9},{"name":"Trey Parker","character":"Eric Cartman / Stan Marsh / Randy Marsh / Mr. Garrison (voice)","img":null,"order":10},{"name":"Matt Stone","character":"Kyle Broflovski / Kenny McCormick / Butters Stotch (voice)","img":null,"order":11},{"name":"Jennifer Howell","character":"Bebe Stevens (voice)","img":null,"order":11},{"name":"April Stewart","character":"Wendy Testaburger / Liane Cartman / Dora M\u00e1rquez (voice)","img":null,"order":65},{"name":"Eliza Schneider","character":"Liane Cartman (voice)","img":null,"order":79},{"name":"Kyle McCulloch","character":"Excalibur Hotel Employee (voice)","img":null,"order":512},{"name":"Jonathan Kimmel","character":"","img":null,"order":528},{"name":"John Hansen","character":"","img":null,"order":534},{"name":"Adrien Beard","character":"Tolkien Black (voice)","img":null,"order":554}],"tv:30984":[{"name":"Masakazu Morita","character":"Ichigo Kurosaki (voice)","img":null,"order":0},{"name":"Fumiko Orikasa","character":"Rukia Kuchiki (voice)","img":null,"order":1},{"name":"Hiroki Yasumoto","character":"Yasutora Sado (voice)","img":null,"order":2},{"name":"Yuki Matsuoka","character":"Orihime Inoue (voice)","img":null,"order":3},{"name":"Noriaki Sugiyama","character":"Uryu Ishida (voice)","img":null,"order":4},{"name":"Kentaro Ito","character":"Renji Abarai (voice)","img":null,"order":47},{"name":"Ryotaro Okiayu","character":"Byakuya Kuchki (voice)","img":null,"order":48},{"name":"Mitsuaki Madono","character":"Kon (voice)","img":null,"order":49},{"name":"Toshiyuki Morikawa","character":"Kaname T\u014dsen (voice)","img":null,"order":133},{"name":"Kiyoyuki Yanada","character":"Tessai Tsukabishi (voice)","img":null,"order":500},{"name":"Katsuyuki Konishi","character":"Shuhei Hisagi (voice)","img":null,"order":501},{"name":"Shin-ichiro Miki","character":"Kisuke Urahara (voice)","img":null,"order":504},{"name":"Masaya Onosaka","character":"Gejigeji mukade (voice)","img":null,"order":505},{"name":"Ryusei Nakao","character":"Mayuri Kurotsuchi (voice)","img":null,"order":509},{"name":"Takayuki Sugo","character":"Yhwach (voice)","img":null,"order":517}],"tv:114410":[{"name":"Kikunosuke Toya","character":"Denji (voice)","img":null,"order":0},{"name":"Tomori Kusunoki","character":"Makima (voice)","img":null,"order":1},{"name":"Shogo Sakata","character":"Aki Hayakawa (voice)","img":null,"order":2},{"name":"Fairouz Ai","character":"Power (voice)","img":null,"order":3},{"name":"Mariya Ise","character":"Himeno (voice)","img":null,"order":500},{"name":"Marina Inoue","character":"Child Denji (voice)","img":null,"order":507},{"name":"Kenjiro Tsuda","character":"Kishibe (voice)","img":null,"order":519},{"name":"Taku Yashiro","character":"Arai (voice)","img":null,"order":520},{"name":"Karin Takahashi","character":"Kobeni (voice)","img":null,"order":521},{"name":"Shiori Izawa","character":"Pochita (voice)","img":null,"order":522},{"name":"Mitsutoshi Shundo","character":"Announcer (voice)","img":null,"order":528},{"name":"Hironori Kondoh","character":"Eternity Devil (voice)","img":null,"order":538},{"name":"Noriyuki Tsuyuki","character":"Henchman D (voice)","img":null,"order":542},{"name":"Daiki Hamano","character":"Samurai Sword (voice)","img":null,"order":546},{"name":"You Taichi","character":"Sawatari (voice)","img":null,"order":547}],"tv:105248":[{"name":"KENN","character":"David Martinez (voice)","img":null,"order":0},{"name":"Koki Uchiyama","character":"D (voice)","img":null,"order":1},{"name":"Momoka Terasawa","character":"Roman (voice)","img":null,"order":2},{"name":"Akari Kito","character":"Talia (voice)","img":null,"order":3},{"name":"Kentaro Tone","character":"Weak (voice)","img":null,"order":4},{"name":"Kento Shiraishi","character":"Maelstrom (voice)","img":null,"order":531},{"name":"Kenjiro Tsuda","character":"Ripperdoc (voice)","img":null,"order":568},{"name":"Aoi Yuki","character":"Lucy (voice)","img":null,"order":571},{"name":"Kazuhiko Inoue","character":"Faraday (voice)","img":null,"order":573},{"name":"Tomoyo Kurosawa","character":"Rebecca (voice)","img":null,"order":574},{"name":"Takako Honda","character":"Kiwi (voice)","img":null,"order":575},{"name":"Ryuho Nagaoka","character":"Driver (voice)","img":null,"order":627},{"name":"Akemi Okamura","character":"Avida (voice)","img":null,"order":668},{"name":"Aoi Koga","character":"Capri (voice)","img":null,"order":669},{"name":"Yasunori Matsumoto","character":"Kiril (voice)","img":null,"order":670}],"tv:240411":[{"name":"Shion Wakayama","character":"Momo Ayase (voice)","img":null,"order":0},{"name":"Natsuki Hanae","character":"Ken 'Okarun' Takakura (voice)","img":null,"order":1},{"name":"Mayumi Tanaka","character":"Turbo Granny (voice)","img":null,"order":2},{"name":"Nana Mizuki","character":"Seiko (voice)","img":null,"order":3},{"name":"Kaori Maeda","character":"Miko (voice)","img":null,"order":500},{"name":"Miyu Tomita","character":"Muko (voice)","img":null,"order":501},{"name":"Ayane Sakura","character":"Aira Shiratori (voice)","img":null,"order":519},{"name":"Aya Yamane","character":"Tsubame (voice)","img":null,"order":520},{"name":"Natsu Yorita","character":"Anzu (voice)","img":null,"order":521},{"name":"Koji Seki","character":"Male Teacher (voice)","img":null,"order":527},{"name":"Koseki Kogure","character":"Student C (voice)","img":null,"order":527},{"name":"Yukiko Motoyoshi","character":"Female Student A (voice)","img":null,"order":528},{"name":"Kazuya Nakai","character":"Serpoian A / Serpoian B / Serpoian C (voice)","img":null,"order":533},{"name":"Tomokazu Seki","character":"Dover Demon (voice)","img":null,"order":536},{"name":"Kaito Ishikawa","character":"Jin 'Jiji' Enjouji (voice)","img":null,"order":541}],"tv:86031":[{"name":"Yusuke Kobayashi","character":"Senku Ishigami (voice)","img":null,"order":0},{"name":"Makoto Furukawa","character":"Taiju Oki (voice)","img":null,"order":1},{"name":"Kana Ichinose","character":"Yuzuriha Ogawa (voice)","img":null,"order":2},{"name":"Manami Numakura","character":"Kohaku (voice)","img":null,"order":3},{"name":"Yuichi Nakamura","character":"Tsukasa (voice)","img":null,"order":4},{"name":"Gen Sato","character":"Chrome (voice)","img":null,"order":5},{"name":"Ayumu Murase","character":"Ginro (voice)","img":null,"order":6},{"name":"Yuya Hirose","character":"Science Club Member (voice)","img":null,"order":501},{"name":"Aoi Ichikawa","character":"Science Club Member (voice)","img":null,"order":502},{"name":"Haruki Ishiya","character":"Science Club Member (voice)","img":null,"order":503},{"name":"Kouichi Souma","character":"Teacher (voice)","img":null,"order":504},{"name":"Keiji Fujiwara","character":"Byakuya Ishigami (voice)","img":null,"order":508},{"name":"Kengo Kawanishi","character":"Asagiri Gen (voice)","img":null,"order":526},{"name":"Ryota Suzuki","character":"Nanami Ryuusui (voice)","img":null,"order":527},{"name":"Karin Takahashi","character":"Suika (Voice)","img":null,"order":528}],"tv:209867":[{"name":"Atsumi Tanezaki","character":"Frieren (voice)","img":null,"order":0},{"name":"Kana Ichinose","character":"Fern (voice)","img":null,"order":1},{"name":"Chiaki Kobayashi","character":"Stark (voice)","img":null,"order":2},{"name":"Nobuhiko Okamoto","character":"Himmel (voice)","img":null,"order":3},{"name":"Hiroki Touchi","character":"\ud558\uc774\ud130 (voice)","img":null,"order":4},{"name":"Yoji Ueda","character":"Eisen (voice)","img":null,"order":5},{"name":"Kento Shiraishi","character":"Falsch (voice)","img":null,"order":550},{"name":"Azumi Waki","character":"Kanne (voice)","img":null,"order":565},{"name":"Sayumi Suzushiro","character":"Lawine (voice)","img":null,"order":566},{"name":"Ikumi Hasegawa","character":"\u00dcbel (voice)","img":null,"order":567},{"name":"Haruka Terui","character":"Sense (voice)","img":null,"order":568},{"name":"Tarusuke Shingaki","character":"Genau (voice)","img":null,"order":569},{"name":"Masafumi Kobatake","character":"Mining Worker (voice)","img":null,"order":572},{"name":"Jiro Saito","character":"Denken (voice)","img":null,"order":576},{"name":"Eiji Hanawa","character":"Richter (voice)","img":null,"order":577}],"tv:31911":[{"name":"Romi Park","character":"Edward Elric (voice)","img":null,"order":0},{"name":"Shin-ichiro Miki","character":"Roy Mustang (voice)","img":null,"order":2},{"name":"Fumiko Orikasa","character":"Riza Hawkeye (voice)","img":null,"order":3},{"name":"Megumi Takamoto","character":"Winry Rockbell (voice)","img":null,"order":4},{"name":"Rie Kugimiya","character":"Alphonse Elric (voice)","img":null,"order":341},{"name":"Hidekatsu Shibata","character":"King Bradley (voice)","img":null,"order":505},{"name":"Kenji Utsumi","character":"Louis Armstrong (voice)","img":null,"order":507},{"name":"Kenta Miyake","character":"Scar (voice)","img":null,"order":535},{"name":"Minami Takayama","character":"Envy (voice)","img":null,"order":541},{"name":"Iemasa Kayumi","character":"Father (voice)","img":null,"order":552},{"name":"Biichi Sato","character":"Doctor (voice)","img":null,"order":559},{"name":"Mamoru Miyano","character":"Ling Yao (voice)","img":null,"order":587},{"name":"Mai Gotou","character":"May Chang (voice)","img":null,"order":590},{"name":"Yuki Hayashi","character":"Military (voice)","img":null,"order":630},{"name":"Takashi Hikida","character":"Ishvalan A (voice)","img":null,"order":689}],"tv:95479":[{"name":"Takaya Kuroda","character":"Masamichi Yaga (voice)","img":null,"order":549},{"name":"Nobunaga Shimazaki","character":"Mahito (voice)","img":null,"order":600},{"name":"Tomokazu Seki","character":"Panda (voice)","img":null,"order":602},{"name":"Mikako Komatsu","character":"Maki Zen'in (voice)","img":null,"order":603},{"name":"Kenjiro Tsuda","character":"Kento Nanami (voice)","img":null,"order":605},{"name":"Junichi Suwabe","character":"Ryomen Sukuna (voice)","img":null,"order":612},{"name":"Shigeru Chiba","character":"Grandfather (voice)","img":"mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg","order":654},{"name":"Aya Endo","character":"Shoko Ieiri (voice)","img":null,"order":666},{"name":"Mitsuo Iwata","character":"Spectator (voice)","img":null,"order":685},{"name":"Yuichi Nakamura","character":"Satoru Gojo (voice)","img":null,"order":686},{"name":"Asami Seto","character":"Transfigured Human (voice)","img":null,"order":687},{"name":"Junya Enoki","character":"Yuji Itadori (voice)","img":null,"order":689},{"name":"Takahiro Sakurai","character":"Suguru Geto (voice)","img":null,"order":718},{"name":"Yoshiko Sakakibara","character":"Tengen (voice)","img":null,"order":739},{"name":"Yuma Uchida","character":"Megumi Fushiguro (voice)","img":null,"order":849}],"tv:207468":[{"name":"Masaya Fukunishi","character":"Kafka Hibino / Kaiju No. 8 (voice)","img":null,"order":0},{"name":"Asami Seto","character":"Mina Ashiro (voice)","img":null,"order":1},{"name":"Wataru Katoh","character":"Reno Ichikawa (voice)","img":null,"order":2},{"name":"Kengo Kawanishi","character":"Soshiro Hoshina (voice)","img":null,"order":4},{"name":"Fairouz Ai","character":"Kaiju No. 15 (voice)","img":null,"order":322},{"name":"Nene Hieda","character":"Akari Minase (voice)","img":null,"order":546},{"name":"Tomohiro Ono","character":"Shimoen (voice)","img":null,"order":551},{"name":"Genta Nakamura","character":"Operator (voice)","img":null,"order":569},{"name":"Takayuki Sugo","character":"Keiji Itami (voice)","img":null,"order":575},{"name":"Ryosuke Higa","character":"Ministry of Defense Staff Member (voice)","img":null,"order":577},{"name":"Shota Yamamoto","character":"Mebari (voice)","img":null,"order":612},{"name":"Yuuki Shin","character":"Iharu Furuhashi (voice)","img":null,"order":625},{"name":"Keisuke Komoto","character":"Haruichi Izumo (voice)","img":null,"order":626},{"name":"Shunsuke Takeuchi","character":"Aoi Kaguragi (voice)","img":null,"order":627},{"name":"Sayaka Senbongi","character":"Konomi Okonogi (voice)","img":null,"order":628}],"tv:65930":[{"name":"Daiki Yamashita","character":"Izuku Midoriya (voice)","img":null,"order":0},{"name":"Tarusuke Shingaki","character":"Mirio Togata (voice)","img":null,"order":1},{"name":"Nobuhiko Okamoto","character":"Katsuki Bakugo (voice)","img":null,"order":2},{"name":"Kenta Miyake","character":"All Might (voice)","img":null,"order":3},{"name":"Yuki Kaji","character":"Shoto Todoroki (voice)","img":null,"order":6},{"name":"Seiran Kobayashi","character":"Eri (voice)","img":null,"order":8},{"name":"Ayane Sakura","character":"Ochako Uraraka (voice)","img":null,"order":48},{"name":"Aoi Yuki","character":"Setsuna Tokage (voice)","img":null,"order":51},{"name":"Junichi Suwabe","character":"Jurota Shishida (voice)","img":null,"order":60},{"name":"Kaito Ishikawa","character":"Tenya Iida (voice)","img":null,"order":91},{"name":"Kenjiro Tsuda","character":"Overhaul (voice)","img":null,"order":505},{"name":"Koki Uchiyama","character":"Tomura Shigaraki (voice)","img":null,"order":546},{"name":"Hiro Shimono","character":"Dabi (voice)","img":null,"order":571},{"name":"Kiyotaka Furushima","character":"Kido (voice)","img":null,"order":583},{"name":"Tetsu Inada","character":"Endeavor (voice)","img":null,"order":585}],"tv:890":[{"name":"Megumi Ogata","character":"Shinji Ikari (voice)","img":null,"order":0},{"name":"Kotono Mitsuishi","character":"Misato Katsuragi (voice)","img":null,"order":1},{"name":"Megumi Hayashibara","character":"Rei Ayanami (voice)","img":null,"order":2},{"name":"Yuko Miyamura","character":"Asuka Langley Soryu (voice)","img":null,"order":3},{"name":"Yuriko Yamaguchi","character":"Dr. Ritsuko Akagi (voice)","img":null,"order":4},{"name":"Fumihiko Tachiki","character":"Gendo Ikari (voice)","img":null,"order":5},{"name":"Motomu Kiyokawa","character":"Kozo Fuyutsuki (voice)","img":null,"order":6},{"name":"Miki Nagasawa","character":"Maya Ibuki (voice)","img":null,"order":7},{"name":"Mugihito","character":"Keel Lorenz (voice)","img":null,"order":500},{"name":"Junko Iwao","character":"Hikari Horaki (voice)","img":null,"order":501},{"name":"Tetsuya Iwanaga","character":"Kensuke Aida (voice)","img":null,"order":503},{"name":"Tomokazu Seki","character":"Touji Suzuhara (voice)","img":null,"order":505},{"name":"Takehito Koyasu","character":"Shigeru Aoba (voice)","img":null,"order":505},{"name":"Koichi Yamadera","character":"Ryouji Kaji (voice)","img":"biov5jENp2XYfehkcdnRvi796Wi.jpg","order":506},{"name":"Hiro Yuuki","character":"Makoto Hyuuga (voice)","img":null,"order":508}],"tv:114868":[{"name":"Tomoyo Kurosawa","character":"G\u00f6ll (voice)","img":null,"order":1},{"name":"Wataru Takagi","character":"Zeus (voice)","img":null,"order":2},{"name":"Junichi Suwabe","character":"Hermes (voice)","img":null,"order":3},{"name":"Soma Saito","character":"Adam (voice)","img":null,"order":4},{"name":"Miyuki Sawashiro","character":"Narrator (voice)","img":null,"order":87},{"name":"Tatsuhisa Suzuki","character":"Shiva (voice)","img":null,"order":501},{"name":"Rie Tanaka","character":"Aphrodite (voice)","img":null,"order":502},{"name":"Yukihiro Nozuyama","character":"Heimdall (voice)","img":null,"order":516},{"name":"Koji Ishii","character":"Zhang Fei (voice)","img":null,"order":524},{"name":"Naoko Komatsu","character":"Eve (voice)","img":null,"order":525},{"name":"Taisuke Nakano","character":"Huginn (voice)","img":null,"order":531},{"name":"Daiki Hamano","character":"God (voice)","img":null,"order":532},{"name":"Chiko Mizumori","character":"Iori Miyamoto (voice)","img":null,"order":534},{"name":"Tomohiro Yamaguchi","character":"Man (voice)","img":null,"order":535},{"name":"Satoru Ito","character":"Spectator (voice)","img":null,"order":539}],"tv:207332":[{"name":"Tomokazu Sugita","character":"Taro Sakamoto (voice)","img":null,"order":0},{"name":"Nobunaga Shimazaki","character":"Shin Asakura (voice)","img":null,"order":1},{"name":"Ayane Sakura","character":"Lu Shaotang (voice)","img":null,"order":2},{"name":"Nao Toyama","character":"Aoi Sakamoto (voice)","img":null,"order":3},{"name":"Hina Kino","character":"Hana Sakamoto (voice)","img":null,"order":4},{"name":"Ryota Suzuki","character":"Heisuke Mashimo (voice)","img":null,"order":5},{"name":"Natsuki Hanae","character":"Yoichi Nagumo (voice)","img":null,"order":6},{"name":"Taku Yashiro","character":"Shishiba (voice)","img":null,"order":7},{"name":"Saori Hayami","character":"Osaragi (voice)","img":null,"order":8},{"name":"Hiroki Yasumoto","character":"Hyou (voice)","img":null,"order":9},{"name":"Hochu Otsuka","character":"Takamura (voice)","img":null,"order":10},{"name":"Daisuke Namikawa","character":"Kei Uzuki (voice)","img":null,"order":11},{"name":"Nobuhiko Okamoto","character":"Natsuki Seba (voice)","img":null,"order":500},{"name":"Yuhko Kaida","character":"Dump (voice)","img":null,"order":501},{"name":"Ryota Takeuchi","character":"Minimalist (voice)","img":null,"order":502}],"tv:249907":[{"name":"Yohei Azakami","character":"Xylo Forbartz (voice)","img":null,"order":0},{"name":"Mayu Iizuka","character":"Teoritta (voice)","img":null,"order":1},{"name":"Shizuka Ishigami","character":"Patouche Kivia (voice)","img":null,"order":2},{"name":"Shun Horie","character":"Dotta Luzlas (voice)","img":null,"order":3},{"name":"Shunichi Toki","character":"Venetim Leopold (voice)","img":null,"order":4},{"name":"Yoji Ueda","character":"Norgayu Senridge (voice)","img":null,"order":5},{"name":"Yoshitsugu Matsuoka","character":"Tatsuya (voice)","img":null,"order":6},{"name":"Jun Fukushima","character":"Tsav (voice)","img":null,"order":7},{"name":"Shoya Chiba","character":"Jace Pertilact (voice)","img":null,"order":8},{"name":"Yoko Hikasa","character":"Neely (voice)","img":null,"order":9}],"tv:120089":[{"name":"Takuya Eguchi","character":"Loid Forger (voice)","img":null,"order":0},{"name":"Atsumi Tanezaki","character":"Anya Forger (voice)","img":null,"order":1},{"name":"Saori Hayami","character":"Yor Forger (voice)","img":null,"order":2},{"name":"Kenichirou Matsuda","character":"Narration (voice)","img":null,"order":345},{"name":"Emiri Kato","character":"Agent M (voice)","img":null,"order":602},{"name":"Hiroki Goto","character":"Eden College Teacher (voice)","img":null,"order":611},{"name":"Hana Sato","character":"Onboard Announcement (voice)","img":null,"order":618},{"name":"Emiko Takeuchi","character":"Nurse (voice)","img":null,"order":643},{"name":"Mirei Kumagai","character":"Berlint General Hospital Doctor (voice)","img":null,"order":644},{"name":"Shoumaru Zouza","character":"Vides (voice)","img":null,"order":677},{"name":"Yuhko Kaida","character":"Sylvia Sherwood (voice)","img":null,"order":685},{"name":"Marie Ooi","character":"Gram Gretcher (voice)","img":null,"order":687},{"name":"Motoki Sakuma","character":"Wolson (voice)","img":null,"order":689},{"name":"Takaaki Torashima","character":"Secretary (voice)","img":null,"order":690},{"name":"Masafumi Kobatake","character":"Barkeeper (voice)","img":null,"order":694}],"tv:36243":[{"name":"Shintaro Asanuma","character":"'I' (Watashi) (voice)","img":null,"order":0},{"name":"Hiroyuki Yoshino","character":"Ozu (voice)","img":null,"order":1},{"name":"Keiji Fujiwara","character":"Higuchi (voice)","img":null,"order":2},{"name":"Yuhko Kaida","character":"Hanuki (voice)","img":null,"order":4},{"name":"Maaya Sakamoto","character":"Akashi (voice)","img":null,"order":5},{"name":"Junichi Suwabe","character":"Jogasaki (voice)","img":null,"order":6}],"tv:88803":[{"name":"Yuto Uemura","character":"Thorfinn (voice)","img":null,"order":0},{"name":"Naoya Uchida","character":"Askeladd (voice)","img":null,"order":1},{"name":"Shunsuke Takeuchi","character":"Einar (voice)","img":null,"order":2},{"name":"Kensho Ono","character":"Canute (voice)","img":null,"order":3},{"name":"Hiroki Yasumoto","character":"Bjorn (voice)","img":null,"order":4},{"name":"Akio Otsuka","character":"Thorkell (voice)","img":null,"order":5},{"name":"Yusuke Ohta","character":"Guard (voice)","img":null,"order":589},{"name":"Yoshiaki Hasegawa","character":"Scout of Thorkell's Troops (voice)","img":null,"order":596},{"name":"Takayuki Nakatsukasa","character":"Member of Askeladd's Mercenaries (voice)","img":null,"order":631},{"name":"Takuya Nakashima","character":"Retainer (voice)","img":null,"order":639},{"name":"Tsuguo Mogami","character":"Soldier of the English Army (voice)","img":null,"order":644},{"name":"Hiroki Goto","character":"Torgrim (voice)","img":null,"order":657},{"name":"Masato Niwa","character":"Wild Dog (voice)","img":null,"order":660},{"name":"Taito Ban","character":"Merchant (voice)","img":null,"order":662},{"name":"Shinya Takahashi","character":"Thegn (voice)","img":null,"order":678}],"tv:30669":[{"name":"Nozomu Sasaki","character":"Y\u016bsuke Urameshi (voice)","img":null,"order":1},{"name":"Shigeru Chiba","character":"Kazuma Kuwabara (voice)","img":"mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg","order":2},{"name":"Megumi Ogata","character":"Kurama/Sh\u016bichi Minamino (voice)","img":null,"order":3},{"name":"Nobuyuki Hiyama","character":"Hiei (voice)","img":null,"order":4},{"name":"Yuri Amano","character":"Keiko Yukimura (voice)","img":null,"order":5},{"name":"Hisako Kyoda","character":"Genkai (voice)","img":null,"order":6},{"name":"Mayumi Tanaka","character":"Koenma (voice)","img":null,"order":7},{"name":"Ai Orikasa","character":"Shizuru Kuwabara (voice)","img":null,"order":8},{"name":"Tomomichi Nishimura","character":"Narration (voice)","img":null,"order":10},{"name":"Sanae Miyuki","character":"Botan","img":null,"order":10},{"name":"Yuri Shiratori","character":"Yukina (voice)","img":null,"order":12},{"name":"Katsumi Suzuki","character":"Toguro (voice)","img":null,"order":13},{"name":"Rokuro Naya","character":"Sensui Shinobu (voice)","img":null,"order":14}],"movie:1930":[{"name":"Andrew Garfield","character":"Spider-Man / Peter Parker","img":null,"order":0},{"name":"Emma Stone","character":"Gwen Stacy","img":null,"order":1},{"name":"Rhys Ifans","character":"The Lizard / Dr. Curt Connors","img":null,"order":2},{"name":"Denis Leary","character":"Captain Stacy","img":null,"order":3},{"name":"Martin Sheen","character":"Uncle Ben","img":null,"order":4},{"name":"Sally Field","character":"Aunt May","img":null,"order":5},{"name":"Irrfan Khan","character":"Rajit Ratha","img":null,"order":6},{"name":"Campbell Scott","character":"Richard Parker","img":null,"order":7},{"name":"Embeth Davidtz","character":"Mary Parker","img":null,"order":8},{"name":"Chris Zylka","character":"Flash Thompson","img":null,"order":9},{"name":"Max Charles","character":"Peter Parker (Age 4)","img":null,"order":10},{"name":"C. Thomas Howell","character":"Jack's Father","img":null,"order":11},{"name":"Jake Keiffer","character":"Jack","img":null,"order":12},{"name":"Kari Coleman","character":"Helen Stacy","img":null,"order":13},{"name":"Michael Barra","character":"Store Clerk","img":null,"order":14}],"movie:102382":[{"name":"Andrew Garfield","character":"Spider-Man / Peter Parker","img":null,"order":0},{"name":"Emma Stone","character":"Gwen Stacy","img":null,"order":1},{"name":"Jamie Foxx","character":"Electro / Max Dillon","img":null,"order":2},{"name":"Dane DeHaan","character":"Green Goblin / Harry Osborn","img":null,"order":3},{"name":"Colm Feore","character":"Donald Menken","img":null,"order":4},{"name":"Felicity Jones","character":"Felicia","img":null,"order":5},{"name":"Paul Giamatti","character":"Aleksei Sytsevich","img":null,"order":6},{"name":"Sally Field","character":"Aunt May","img":null,"order":7},{"name":"Embeth Davidtz","character":"Mary Parker","img":null,"order":8},{"name":"Campbell Scott","character":"Richard Parker","img":null,"order":9},{"name":"Marton Csokas","character":"Dr. Ashley Kafka","img":null,"order":10},{"name":"Louis Cancelmi","character":"Man in Black Suit","img":null,"order":11},{"name":"Max Charles","character":"Young Peter Parker","img":null,"order":12},{"name":"B.J. Novak","character":"Allistar Smythe","img":null,"order":13},{"name":"Sarah Gadon","character":"Kari","img":null,"order":14}],"movie:55931":[{"name":"Keanu Reeves","character":"Neo (\"Kid's Story\") (voice)","img":null,"order":0},{"name":"Carrie-Anne Moss","character":"Trinity (\"Kid's Story\" and \"A Detective Story\") (voice)","img":null,"order":1},{"name":"Clayton Watson","character":"The Kid (\"Kid's Story\") (voice)","img":null,"order":2},{"name":"Pamela Adlon","character":"Jue (\"Final Flight of the Osiris\") / Manabu (\"Beyond\") (voice)","img":"5eIRHUbNOH7ibXa0IfUfFvj5I5L.jpg","order":3},{"name":"Melinda Clarke","character":"Alexa (\"Matriculated\") (voice)","img":null,"order":4},{"name":"John DiMaggio","character":"Crew Man (\"Final Flight of the Osiris\") / Kaiser (\"Program\") (voice)","img":null,"order":5},{"name":"Kevin Michael Richardson","character":"Thadeus (\"Final Flight of Osiris\") / Cop (\"Kid's Story\") / Agent #2 (\"World Record\")","img":null,"order":6},{"name":"Tom Kenny","character":"Operator (\"Final Flight of Osiris\")","img":null,"order":7},{"name":"Rick Gomez","character":"Pilot (\"Final Flight of Osiris\")","img":null,"order":8},{"name":"Tara Strong","character":"Crew Woman (\"Final Flight of Osiris\") / Nurse (\"World Record\") / Misha (\"Beyond\")","img":null,"order":9},{"name":"Bette Ford","character":"Old Woman (\"Final Flight of Osiris\")","img":null,"order":10},{"name":"Julia Fletcher","character":"The Instructor (\"The Second Renaissance Part I & II\") / Narrator (\"World Record\")","img":null,"order":11},{"name":"Dane A. Davis","character":"01 Versatran Spokesman (\"The Second Renaissance Part I & II\")","img":null,"order":12},{"name":"Debi Derryberry","character":"Kid (\"The Second Renaissance Part I & II\")","img":null,"order":13},{"name":"Jill Talley","character":"Mother (\"The Second Renaissance Part I & II\")","img":null,"order":14}],"movie:640146":[{"name":"Paul Rudd","character":"Scott Lang / Ant-Man","img":null,"order":0},{"name":"Evangeline Lilly","character":"Hope Van Dyne / The Wasp","img":null,"order":1},{"name":"Michael Douglas","character":"Dr. Hank Pym","img":null,"order":2},{"name":"Michelle Pfeiffer","character":"Janet Van Dyne","img":null,"order":3},{"name":"Jonathan Majors","character":"Kang the Conqueror","img":null,"order":4},{"name":"Kathryn Newton","character":"Cassie Lang","img":null,"order":5},{"name":"Corey Stoll","character":"M.O.D.O.K.","img":null,"order":6},{"name":"Bill Murray","character":"Lord Krylar","img":null,"order":7},{"name":"Katy O'Brian","character":"Jentorra","img":null,"order":8},{"name":"William Jackson Harper","character":"Quaz","img":null,"order":9},{"name":"Jamie Andrew Cutler","character":"Xolum","img":null,"order":10},{"name":"David Dastmalchian","character":"Veb (voice)","img":null,"order":11},{"name":"Randall Park","character":"Jimmy Woo","img":null,"order":12},{"name":"Mark Weinman","character":"Patrol Police Officer / On-Set M.O.D.O.K.","img":null,"order":13},{"name":"Ross Mullan","character":"Axia Restaurant Maitre D'","img":null,"order":14}],"movie:269650":[{"name":"Yuka Komatsu","character":"Deunan Knute (voice)","img":null,"order":0},{"name":"Junichi Suwabe","character":"Briareos Hecatonchires (voice)","img":null,"order":1},{"name":"Aoi Yuki","character":"Iris (voice)","img":null,"order":2},{"name":"Hiroki Takahashi","character":"Olson (voice)","img":null,"order":3},{"name":"Hiroki Touchi","character":"Talos (voice)","img":null,"order":4},{"name":"Kaori Nazuka","character":"Nyx (voice)","img":null,"order":5},{"name":"Tessyo Genda","character":"Two Horns (voice)","img":null,"order":6},{"name":"Katsunosuke Hori","character":"Mathews (voice)","img":null,"order":7},{"name":"Hisanori Yoshida","character":"(voice)","img":null,"order":8},{"name":"Shinichiro Kamio","character":"(voice)","img":null,"order":9},{"name":"Yoshiki Nakajima","character":"(voice)","img":null,"order":10},{"name":"Yuki Yonai","character":"(voice)","img":null,"order":11},{"name":"Mao Satonaka","character":"(voice)","img":null,"order":12}],"movie:572802":[{"name":"Jason Momoa","character":"Arthur","img":null,"order":0},{"name":"Patrick Wilson","character":"Orm","img":null,"order":1},{"name":"Yahya Abdul-Mateen II","character":"Black Manta","img":null,"order":2},{"name":"Randall Park","character":"Dr. Shin","img":null,"order":3},{"name":"Amber Heard","character":"Mera","img":null,"order":4},{"name":"Nicole Kidman","character":"Atlanna","img":null,"order":5},{"name":"Dolph Lundgren","character":"King Nereus","img":null,"order":6},{"name":"Temuera Morrison","character":"Tom","img":null,"order":7},{"name":"Lillah Indya Moore","character":"Karshon","img":null,"order":8},{"name":"Pilou Asb\u00e6k","character":"Kordax","img":null,"order":9},{"name":"Jani Zhao","character":"Stingray","img":null,"order":10},{"name":"Vincent Regan","character":"Atlan","img":null,"order":11},{"name":"Martin Short","character":"Kingfish (voice)","img":null,"order":12},{"name":"John Rhys-Davies","character":"Brine King (voice)","img":null,"order":13},{"name":"Natalia Safran","character":"Council Lady","img":null,"order":14}],"movie:24428":[{"name":"Robert Downey Jr.","character":"Tony Stark / Iron Man","img":null,"order":0},{"name":"Chris Evans","character":"Steve Rogers / Captain America","img":null,"order":1},{"name":"Mark Ruffalo","character":"Bruce Banner / The Hulk","img":null,"order":2},{"name":"Chris Hemsworth","character":"Thor","img":null,"order":3},{"name":"Scarlett Johansson","character":"Natasha Romanoff / Black Widow","img":null,"order":4},{"name":"Jeremy Renner","character":"Clint Barton / Hawkeye","img":null,"order":5},{"name":"Tom Hiddleston","character":"Loki","img":null,"order":6},{"name":"Clark Gregg","character":"Agent Phil Coulson","img":null,"order":7},{"name":"Cobie Smulders","character":"Agent Maria Hill","img":null,"order":8},{"name":"Stellan Skarsg\u00e5rd","character":"Selvig","img":"mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg","order":9},{"name":"Samuel L. Jackson","character":"Nick Fury","img":null,"order":10},{"name":"Gwyneth Paltrow","character":"Pepper Potts","img":null,"order":11},{"name":"Paul Bettany","character":"Jarvis (voice)","img":null,"order":12},{"name":"Alexis Denisof","character":"The Other","img":null,"order":13},{"name":"Tina Benko","character":"NASA Scientist","img":null,"order":14}],"movie:257346":[{"name":"Miyuki Sawashiro","character":"Natasha Romanoff / Black Widow (voice)","img":null,"order":0},{"name":"Tessyo Genda","character":"Frank Castle / The Punisher (voice)","img":null,"order":1},{"name":"Hideaki Tezuka","character":"Nick Fury (voice)","img":null,"order":2},{"name":"Hiroki Touchi","character":"Elihas Starr (voice)","img":null,"order":3},{"name":"Ryuuzaburou Ootomo","character":"Cain (voice)","img":null,"order":4},{"name":"Junko Minagawa","character":"Maria Hill (voice)","img":null,"order":5},{"name":"Daisuke Namikawa","character":"Amadeus Cho (voice)","img":null,"order":6},{"name":"Masashi Sugawara","character":"Orion (voice)","img":null,"order":7},{"name":"Yuichi Karasuma","character":"The Hulk (voice)","img":null,"order":8},{"name":"Hisashi Izumi","character":"Ren (voice)","img":null,"order":9},{"name":"Keiji Fujiwara","character":"Tony Stark / Iron Man (voice)","img":null,"order":10},{"name":"Shuhei Sakaguchi","character":"Clint Barton / Hawkeye (voice)","img":null,"order":11}],"movie:99861":[{"name":"Robert Downey Jr.","character":"Tony Stark / Iron Man","img":null,"order":0},{"name":"Chris Hemsworth","character":"Thor","img":null,"order":1},{"name":"Mark Ruffalo","character":"Bruce Banner / Hulk","img":null,"order":2},{"name":"Chris Evans","character":"Steve Rogers / Captain America","img":null,"order":3},{"name":"Scarlett Johansson","character":"Natasha Romanoff / Black Widow","img":null,"order":4},{"name":"Jeremy Renner","character":"Clint Barton / Hawkeye","img":null,"order":5},{"name":"James Spader","character":"Ultron (voice)","img":null,"order":6},{"name":"Samuel L. Jackson","character":"Nick Fury","img":null,"order":7},{"name":"Don Cheadle","character":"James Rhodes / War Machine","img":null,"order":8},{"name":"Aaron Taylor-Johnson","character":"Pietro Maximoff / Quicksilver","img":null,"order":9},{"name":"Elizabeth Olsen","character":"Wanda Maximoff / Scarlet Witch","img":null,"order":10},{"name":"Paul Bettany","character":"Jarvis / Vision","img":null,"order":11},{"name":"Cobie Smulders","character":"Maria Hill","img":null,"order":12},{"name":"Anthony Mackie","character":"Sam Wilson / The Falcon","img":null,"order":13},{"name":"Hayley Atwell","character":"Peggy Carter","img":null,"order":14}],"movie:299536":[{"name":"Robert Downey Jr.","character":"Tony Stark / Iron Man","img":null,"order":0},{"name":"Chris Evans","character":"Steve Rogers / Captain America","img":null,"order":1},{"name":"Chris Hemsworth","character":"Thor","img":null,"order":2},{"name":"Josh Brolin","character":"Thanos","img":null,"order":3},{"name":"Mark Ruffalo","character":"Bruce Banner / Hulk","img":null,"order":4},{"name":"Scarlett Johansson","character":"Natasha Romanoff / Black Widow","img":null,"order":5},{"name":"Don Cheadle","character":"James Rhodes / War Machine","img":null,"order":6},{"name":"Benedict Cumberbatch","character":"Doctor Strange","img":null,"order":7},{"name":"Tom Holland","character":"Peter Parker / Spider-Man","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":8},{"name":"Chadwick Boseman","character":"T'Challa / Black Panther","img":null,"order":9},{"name":"Zoe Salda\u00f1a","character":"Gamora","img":null,"order":10},{"name":"Karen Gillan","character":"Nebula","img":null,"order":11},{"name":"Tom Hiddleston","character":"Loki","img":null,"order":12},{"name":"Paul Bettany","character":"Vision","img":null,"order":13},{"name":"Elizabeth Olsen","character":"Wanda Maximoff / Scarlet Witch","img":null,"order":14}],"movie:485942":[{"name":"Koichi Yamadera","character":"Batman / Bruce Wayne (voice)","img":"biov5jENp2XYfehkcdnRvi796Wi.jpg","order":0},{"name":"Wataru Takagi","character":"The Joker (voice)","img":null,"order":1},{"name":"Rie Kugimiya","character":"Harley Quinn / Harleen Quinzel (voice)","img":null,"order":2},{"name":"Ai Kakuma","character":"Catwoman / Selina Kyle (voice)","img":null,"order":3},{"name":"Hochu Otsuka","character":"Alfred Pennyworth (voice)","img":null,"order":4},{"name":"Daisuke Ono","character":"Nightwing / Dick Grayson (voice)","img":null,"order":5},{"name":"Akira Ishida","character":"Red Hood / Jason Todd (voice)","img":null,"order":6},{"name":"Kengo Kawanishi","character":"Red Robin / Tim Drake (voice)","img":null,"order":7},{"name":"Yuki Kaji","character":"Robin / Damian Wayne (voice)","img":null,"order":8},{"name":"Junichi Suwabe","character":"Deathstroke / Slade Wilson (voice)","img":null,"order":9},{"name":"Cho","character":"The Penguin / Oswald Cobblepot (voice)","img":null,"order":10},{"name":"Takehito Koyasu","character":"Gorilla Grodd (voice)","img":null,"order":11},{"name":"Toshiyuki Morikawa","character":"Two-Face / Harvey Dent (voice)","img":null,"order":12},{"name":"Atsuko Tanaka","character":"Poison Ivy / Pamela Isley (voice)","img":null,"order":13},{"name":"Kenta Miyake","character":"Bane (voice)","img":null,"order":14}],"movie:209112":[{"name":"Ben Affleck","character":"Bruce Wayne / Batman","img":null,"order":0},{"name":"Henry Cavill","character":"Clark Kent / Superman","img":null,"order":1},{"name":"Jesse Eisenberg","character":"Lex Luthor","img":null,"order":2},{"name":"Gal Gadot","character":"Diana Prince / Wonder Woman","img":null,"order":3},{"name":"Amy Adams","character":"Lois","img":null,"order":4},{"name":"Diane Lane","character":"Martha Kent","img":null,"order":5},{"name":"Laurence Fishburne","character":"Perry White","img":null,"order":6},{"name":"Jeremy Irons","character":"Alfred","img":null,"order":7},{"name":"Holly Hunter","character":"Senator Finch","img":null,"order":8},{"name":"Scoot McNairy","character":"Wallace Keefe","img":null,"order":9},{"name":"Callan Mulvey","character":"Anatoli Knyazev","img":null,"order":10},{"name":"Tao Okamoto","character":"Mercy Graves","img":null,"order":11},{"name":"Brandon Spink","character":"Young Bruce Wayne","img":null,"order":12},{"name":"Lauren Cohan","character":"Martha Wayne","img":null,"order":13},{"name":"Alan D. Purwin","character":"Wayne Industries Pilot","img":null,"order":14}],"movie:123025":[{"name":"Peter Weller","character":"Batman / Bruce Wayne (voice)","img":null,"order":0},{"name":"Ariel Winter","character":"Robin / Carrie Kelley (voice)","img":null,"order":1},{"name":"David Selby","character":"James Gordon (voice)","img":null,"order":2},{"name":"Wade Williams","character":"Harvey Dent / Two-Face (voice)","img":null,"order":3},{"name":"Michael McKean","character":"Dr. Bartholomew Wolper (voice)","img":null,"order":4},{"name":"Rob Paulsen","character":"Jeff Adams (voice)","img":null,"order":5},{"name":"Gary Anthony Williams","character":"Mutant Leader / Anchor Bill (voice)","img":null,"order":6},{"name":"Gary Anthony Sturgis","character":"Silk / Other Spike / Young Mutant (voice)","img":null,"order":7},{"name":"Paget Brewster","character":"Lana Lang (voice)","img":null,"order":8},{"name":"Carlos Alazraqui","character":"Hernando / TV Host (voice)","img":null,"order":9},{"name":"Dee Bradley Baker","character":"Don / Fat Mutant / Additional Voices (voice)","img":null,"order":10},{"name":"Maria Canals-Barrera","character":"Yindel (voice)","img":null,"order":11},{"name":"Cathy Cavadini","character":"Joanie / Woman with Hot Dog (voice)","img":null,"order":12},{"name":"Townsend Coleman","character":"Morrie / Civilian (voice)","img":null,"order":13},{"name":"Grey DeLisle","character":"Anchor Carla / Female Mutant (voice)","img":null,"order":14}],"movie:436270":[{"name":"Dwayne Johnson","character":"Black Adam / Teth Adam","img":null,"order":0},{"name":"Aldis Hodge","character":"Hawkman / Carter Hall","img":null,"order":1},{"name":"Noah Centineo","character":"Atom Smasher / Al Rothstein","img":null,"order":2},{"name":"Sarah Shahi","character":"Adrianna Tomaz","img":null,"order":3},{"name":"Quintessa Swindell","character":"Cyclone / Maxine Hunkel","img":null,"order":4},{"name":"Marwan Kenzari","character":"Ishmael / Sabbac / King Ahk-Ton","img":null,"order":5},{"name":"Mo Amer","character":"Karim","img":null,"order":6},{"name":"Bodhi Sabongui","character":"Amon Tomaz","img":null,"order":7},{"name":"Pierce Brosnan","character":"Dr. Fate / Kent Nelson","img":null,"order":8},{"name":"James Cusati-Moyer","character":"Samir","img":null,"order":9},{"name":"Jalon Christian","character":"Hurut","img":null,"order":10},{"name":"Benjamin Patterson","character":"Skinny Teth Adam","img":null,"order":11},{"name":"Odelya Halevi","character":"Shiruta","img":null,"order":12},{"name":"Uli Latukefu","character":"The Champion","img":null,"order":13},{"name":"Jennifer Holland","character":"Emilia Harcourt","img":null,"order":14}],"movie:284054":[{"name":"Chadwick Boseman","character":"T'Challa / Black Panther","img":null,"order":0},{"name":"Michael B. Jordan","character":"Erik Killmonger","img":null,"order":1},{"name":"Lupita Nyong'o","character":"Nakia","img":null,"order":2},{"name":"Danai Gurira","character":"Okoye","img":null,"order":3},{"name":"Martin Freeman","character":"Everett K. Ross","img":null,"order":4},{"name":"Daniel Kaluuya","character":"W'Kabi","img":null,"order":5},{"name":"Letitia Wright","character":"Shuri","img":null,"order":6},{"name":"Winston Duke","character":"M'Baku","img":null,"order":7},{"name":"Sterling K. Brown","character":"N'Jobu","img":null,"order":8},{"name":"Angela Bassett","character":"Ramonda","img":null,"order":9},{"name":"Forest Whitaker","character":"Zuri","img":null,"order":10},{"name":"Andy Serkis","character":"Ulysses Klaue","img":null,"order":11},{"name":"Florence Kasumba","character":"Ayo","img":null,"order":12},{"name":"John Kani","character":"T'Chaka","img":null,"order":13},{"name":"David S. Lee","character":"Limbani","img":null,"order":14}],"movie:505642":[{"name":"Letitia Wright","character":"Shuri","img":null,"order":0},{"name":"Tenoch Huerta Mej\u00eda","character":"Namor","img":null,"order":1},{"name":"Lupita Nyong'o","character":"Nakia","img":null,"order":2},{"name":"Danai Gurira","character":"Okoye","img":null,"order":3},{"name":"Winston Duke","character":"M'Baku","img":null,"order":4},{"name":"Angela Bassett","character":"Ramonda","img":null,"order":5},{"name":"Martin Freeman","character":"Everett Ross","img":null,"order":6},{"name":"Julia Louis-Dreyfus","character":"Valentina Allegra de Fontaine","img":null,"order":7},{"name":"Dominique Thorne","character":"Riri","img":null,"order":8},{"name":"Florence Kasumba","character":"Ayo","img":null,"order":9},{"name":"Michaela Coel","character":"Aneka","img":null,"order":10},{"name":"Alex Livinalli","character":"Attuma","img":null,"order":11},{"name":"Mabel Cadena","character":"Namora","img":null,"order":12},{"name":"Michael B. Jordan","character":"Killmonger","img":null,"order":13},{"name":"Isaach de Bankol\u00e9","character":"River Tribe Elder","img":null,"order":14}],"movie:409421":[{"name":"Takahiro Sakurai","character":"Killy (voice)","img":null,"order":0},{"name":"Kana Hanazawa","character":"Cibo (voice)","img":null,"order":1},{"name":"Sora Amamiya","character":"Zuru (voice)","img":null,"order":2},{"name":"Mamoru Miyano","character":"Sutezo (voice)","img":null,"order":3},{"name":"Aya Suzaki","character":"Tae (voice)","img":null,"order":4},{"name":"Nobunaga Shimazaki","character":"Fusata (voice)","img":null,"order":5},{"name":"Nanako Mori","character":"Fuku (voice)","img":null,"order":6},{"name":"Kazuhiro Yamaji","character":"Pop (voice)","img":null,"order":7}],"movie:17165":[{"name":"Masakazu Morita","character":"Ichigo Kurosaki (voice)","img":null,"order":0},{"name":"Fumiko Orikasa","character":"Rukia Kuchiki (voice)","img":null,"order":1},{"name":"Kentaro Ito","character":"Renji Abarai (voice)","img":null,"order":2},{"name":"Aya Hirano","character":"Homura (voice)","img":null,"order":3},{"name":"Hiroshi Kamiya","character":"Shizuku (voice)","img":null,"order":4},{"name":"Mitsuaki Madono","character":"Kon (voice)","img":null,"order":5},{"name":"Jun Fukuyama","character":"Yumichika Ayasegawa (voice)","img":null,"order":6},{"name":"Taro Yamaguchi","character":"Sasakibe Choujiro (voice)","img":null,"order":7},{"name":"Takako Honda","character":"Jinta Hanakari (voice)","img":null,"order":8},{"name":"Katsuyuki Konishi","character":"Shuuhei Hisagi (voice)","img":null,"order":9},{"name":"Rintaro Nishi","character":"Tetsuzaemon Iba (voice)","img":null,"order":10},{"name":"Tetsu Inada","character":"Sajin Komamura (voice)","img":null,"order":11},{"name":"Fumiko Orikasa","character":"Hisana Kuchiki (voice)","img":null,"order":12},{"name":"Rie Kugimiya","character":"Nemu Kurotsuchi (voice)","img":null,"order":13},{"name":"Hisayo Mochizuki","character":"Yachiru Kusajishi (voice)","img":null,"order":14}],"tv:73319":[{"name":"Ed Brown","character":"Self","img":null,"order":0},{"name":"Rosemarie Vega","character":"Self","img":null,"order":1},{"name":"Angela Deem","character":"Self","img":null,"order":2},{"name":"Michael Ilesanmi","character":"Self","img":null,"order":3}],"tv:90046":[],"tv:48866":[{"name":"Eliza Taylor","character":"Clarke Griffin","img":null,"order":0},{"name":"Bob Morley","character":"Bellamy Blake","img":null,"order":1},{"name":"Lindsey Morgan","character":"Raven Reyes","img":null,"order":2},{"name":"Marie Avgeropoulos","character":"Octavia Blake","img":null,"order":3},{"name":"Richard Harmon","character":"John Murphy","img":null,"order":5},{"name":"Luisa d'Oliveira","character":"Emori","img":null,"order":7},{"name":"Christopher Larkin","character":"Monty Green","img":null,"order":9},{"name":"Tati Gabrielle","character":"Gaia","img":null,"order":11},{"name":"Devon Bostick","character":"Jasper Jordan","img":null,"order":13},{"name":"Isaiah Washington","character":"Thelonious Jaha","img":null,"order":15},{"name":"Henry Ian Cusick","character":"Marcus Kane","img":null,"order":16},{"name":"Adina Porter","character":"Indra","img":null,"order":20},{"name":"Jarod Joseph","character":"Nathan Miller","img":null,"order":21},{"name":"Ricky Whittle","character":"Lincoln","img":null,"order":25},{"name":"Paige Turco","character":"Judge","img":null,"order":102}],"tv:11821":[{"name":"Bud Abbott","character":"Bud Abbott","img":null,"order":0},{"name":"Lou Costello","character":"Lou Costello","img":null,"order":1},{"name":"Joe Besser","character":"Stinky Davis","img":null,"order":500},{"name":"Hillary Brooke","character":"Hillary Brooke","img":null,"order":539},{"name":"Gordon Jones","character":"Mike Kelly","img":null,"order":540},{"name":"Sid Fields","character":"Sid Fields","img":null,"order":561},{"name":"Joe Kirk","character":"Mr. Bacciagalupe","img":null,"order":567},{"name":"Murray Leonard","character":"Doctor","img":null,"order":587},{"name":"Bobby Barber","character":"Tout","img":null,"order":614},{"name":"Joan Shawlee","character":"Marriage License Bureau Clerk","img":null,"order":634},{"name":"Dorothy Granger","character":"Arguing Wife","img":null,"order":642},{"name":"Milt Bronson","character":"Sam Speedy","img":null,"order":646},{"name":"Ray Walker","character":"Police Lt. Smith","img":null,"order":653},{"name":"Lucien Littlefield","character":"First Man","img":null,"order":658},{"name":"Veda Ann Borg","character":"Crazy Lady","img":null,"order":663}],"tv:43167":[{"name":"Yuki Kaji","character":"Arita Haruyuki","img":null,"order":0},{"name":"Sachika Misawa","character":"Kuroyuki-hime","img":null,"order":1},{"name":"Ayako Kawasumi","character":"Kakei Mihaya","img":null,"order":2},{"name":"Kenichi Suzumura","character":"Ash Roller","img":null,"order":3},{"name":"Aki Toyosaki","character":"Kurashima Chiyuri","img":null,"order":4},{"name":"Shintaro Asanuma","character":"Mayuzumi Takumu","img":null,"order":5},{"name":"Sanae Kobayashi","character":"Noumi Seiji","img":null,"order":6},{"name":"Rina Hidaka","character":"Kouzuki Yuniko","img":null,"order":7},{"name":"Haruka Tomatsu","character":"Wakamiya Megumi","img":null,"order":8}],"tv:114461":[{"name":"Rosario Dawson","character":"Ahsoka Tano","img":null,"order":0},{"name":"Natasha Liu Bordizzo","character":"Sabine Wren","img":null,"order":1},{"name":"Ivanna Sakhno","character":"Shin Hati","img":null,"order":2},{"name":"Ray Stevenson","character":"Baylan Skoll","img":null,"order":2},{"name":"Mary Elizabeth Winstead","character":"Hera Syndulla","img":null,"order":3},{"name":"Lars Mikkelsen","character":"Grand Admiral Thrawn","img":null,"order":4},{"name":"Eman Esfandi","character":"Ezra Bridger","img":null,"order":5},{"name":"Diana Lee Inosanto","character":"Morgan Elsbeth","img":null,"order":5},{"name":"Rory McCann","character":"Baylan Skoll","img":null,"order":6},{"name":"David Tennant","character":"Huyang (voice)","img":null,"order":6},{"name":"Hayden Christensen","character":"Anakin Skywalker","img":null,"order":7},{"name":"Paul Darnell","character":"Marrok","img":null,"order":512},{"name":"Chris Bartlett","character":"Home One Protocol Droid","img":null,"order":519},{"name":"Nican Robinson","character":"First Officer Vic Hawkins","img":null,"order":526},{"name":"Dawn Dininger","character":"Lieutenant Beyta","img":null,"order":546}],"tv:1433":[{"name":"Seth MacFarlane","character":"Stan Smith / Roger (voice)","img":null,"order":0},{"name":"Wendy Schaal","character":"Francine Smith (voice)","img":null,"order":1},{"name":"Rachael MacFarlane","character":"Hayley Smith (voice)","img":null,"order":2},{"name":"Scott Grimes","character":"Steve Smith (voice)","img":null,"order":3},{"name":"Dee Bradley Baker","character":"Klaus (voice)","img":null,"order":4},{"name":"Jeff Fischer","character":"Kidnapper (voice)","img":null,"order":204},{"name":"Mike Barker","character":"Terry Bates / Researcher (voice)","img":null,"order":1590},{"name":"Kevin Michael Richardson","character":"Rerun (voice)","img":null,"order":1824},{"name":"Mike Henry","character":"Jackson / Maitre D' (voice)","img":null,"order":1927},{"name":"Eddie Kaye Thomas","character":"Barry Robinson / Spike (voice)","img":null,"order":1967},{"name":"Patrick Stewart","character":"Avery Bullock as B (voice)","img":null,"order":2007},{"name":"Curtis Armstrong","character":"","img":null,"order":2028},{"name":"David Koechner","character":"Dick Reynolds (voice)","img":null,"order":2052},{"name":"Daisuke Suzuki","character":"Toshi Yoshida (voice)","img":null,"order":2054},{"name":"Richard Kind","character":"Car Salesman (voice)","img":null,"order":2057}],"tv:46639":[{"name":"Ricky Whittle","character":"Shadow Moon","img":null,"order":0},{"name":"Ian McShane","character":"Mr. Wednesday / God Odin","img":null,"order":1},{"name":"Emily Browning","character":"Laura Moon","img":null,"order":2},{"name":"Pablo Schreiber","character":"Mad Sweeney","img":null,"order":3},{"name":"Yetide Badaki","character":"Bilquis","img":null,"order":4},{"name":"Crispin Glover","character":"Mr. World","img":null,"order":5},{"name":"Bruce Langley","character":"Technical Boy","img":null,"order":6},{"name":"Omid Abtahi","character":"Salim","img":null,"order":7},{"name":"Kenzie Delo","character":"Herman","img":null,"order":9},{"name":"Demore Barnes","character":"Mr. Ibis","img":null,"order":611},{"name":"Herizen F. Guardiola","character":"Oshun","img":null,"order":612},{"name":"Peter Stormare","character":"Czernobog","img":null,"order":619},{"name":"Julia Sweeney","character":"Ann-Marie Hinzelmann","img":null,"order":620},{"name":"Eric Johnson","character":"Chad Mulligan","img":null,"order":621},{"name":"Lela Loren","character":"Marguerite Olsen","img":null,"order":624}],"tv:2985":[{"name":"Kevin Sorbo","character":"Dylan Hunt","img":null,"order":0},{"name":"Lisa Ryder","character":"Beka Valentine","img":null,"order":1},{"name":"Laura Bertram","character":"Trance Gemini","img":null,"order":2},{"name":"Gordon Michael Woolvett","character":"Seamus Harper","img":null,"order":3},{"name":"Lexa Doig","character":"Andromeda Ascendant","img":null,"order":4},{"name":"Brandy Ledford","character":"Doyle","img":null,"order":5},{"name":"Keith Hamilton Cobb","character":"Tyr Anasazi","img":null,"order":6},{"name":"Brent Stait","character":"Rev Bem","img":null,"order":7},{"name":"Steve Bacic","character":"Gaheris Rhade","img":null,"order":424},{"name":"Rik Kiviaho","character":"High Guard Captain Perim","img":null,"order":666},{"name":"Janyse Jaud","character":"Cavava (voice)","img":null,"order":677},{"name":"Robert Saunders","character":"Spirit of the Abyss","img":null,"order":689},{"name":"Jim Byrnes","character":"Virgil Vox","img":null,"order":775},{"name":"Nicholas Lea","character":"Tri-Lorn","img":null,"order":777},{"name":"Carmen Moore","character":"Tri-Jema","img":null,"order":813}],"tv:251":[{"name":"Dana Snyder","character":"Master Shake (voice)","img":null,"order":0},{"name":"Carey Means","character":"Frylock (voice)","img":null,"order":1},{"name":"Dave Willis","character":"Meatwad / Carl (voice)","img":null,"order":3},{"name":"Mike Schatz","character":"Emory (voice)","img":null,"order":6},{"name":"Andy Merrill","character":"Oglethorpe / Merle (voice)","img":null,"order":145},{"name":"Schoolly D","character":"Boost Mobile Phone (voice)","img":null,"order":333},{"name":"Matt Maiellaro","character":"Err / Cybernetic Ghost of Christmas Past (voice)","img":null,"order":396},{"name":"C. Martin Croker","character":"Birds (voice)","img":null,"order":455},{"name":"H. Jon Benjamin","character":"Don Shake","img":null,"order":576},{"name":"MC Chris","character":"Little Brittle (voice)","img":null,"order":617},{"name":"George Lowe","character":"George Lowe (voice)","img":null,"order":642},{"name":"Scott Hilley","character":"George Washington (voice)","img":null,"order":642},{"name":"Eugene Mirman","character":"Dr. Eugene Mirman (voice)","img":null,"order":660},{"name":"Edward Hastings","character":"Panda Furry (voice)","img":null,"order":661},{"name":"Nick Ingkatanuwat","character":"Tour Guide (voice)","img":null,"order":670}],"tv:86034":[{"name":"Toshinari Fukamachi","character":"Hajime Nagumo (voice)","img":null,"order":0},{"name":"Yuuki Kuwahara","character":"Yue (voice)","img":null,"order":1},{"name":"Minami Takahashi","character":"Shea Haulia (voice)","img":null,"order":2},{"name":"Yoko Hikasa","character":"Tio Klarus (voice)","img":null,"order":3},{"name":"Saori Onishi","character":"Kaori Shirasaki (voice)","img":null,"order":4},{"name":"Yumiri Hanamori","character":"Shizuku Yaegashi (voice)","img":null,"order":5},{"name":"Ai Kakuma","character":"Aiko Hatayama (voice)","img":null,"order":6},{"name":"Yui Ogura","character":"Myu (voice)","img":null,"order":7},{"name":"Noriko Shibasaki","character":"Liliana S. B. Heiligh (voice)","img":null,"order":8},{"name":"Katsuyuki Konishi","character":"Freid Bagwa (voice)","img":null,"order":10},{"name":"Makoto Ishii","character":"Mikhail (voice)","img":null,"order":11},{"name":"Hiroshi Watanabe","character":"Haulia Yao (voice)","img":null,"order":12},{"name":"Rina Sato","character":"Nun (voice)","img":null,"order":269},{"name":"Tetsuya Kakihara","character":"K\u014dki Amanogawa (voice)","img":null,"order":563},{"name":"Yasuhiro Mamiya","character":"Meld Loggins (voice)","img":null,"order":564}],"tv:1412":[{"name":"Stephen Amell","character":"Oliver Queen / Green Arrow","img":null,"order":0},{"name":"David Ramsey","character":"John Diggle / Spartan","img":null,"order":1},{"name":"Rick Gonzalez","character":"Rene Ramirez / Wild Dog","img":null,"order":4},{"name":"Susanna Thompson","character":"Moira Queen","img":null,"order":5},{"name":"Emily Bett Rickards","character":"Felicity Smoak","img":null,"order":13},{"name":"Paul Blackthorne","character":"Quentin Lance","img":null,"order":14},{"name":"Colton Haynes","character":"Roy Harper / Arsenal","img":null,"order":19},{"name":"John Barrowman","character":"Malcolm Merlyn","img":null,"order":26},{"name":"Echo Kellum","character":"Curtis Holt","img":null,"order":27},{"name":"Katie Cassidy","character":"Black Siren","img":null,"order":34},{"name":"Willa Holland","character":"Thea Queen / Speedy","img":null,"order":96},{"name":"Juliana Harkavy","character":"Dinah Drake / Black Canary","img":null,"order":223},{"name":"Manu Bennett","character":"Slade Wilson / Deathstroke","img":null,"order":279},{"name":"Audrey Marie Anderson","character":"Lyla Michaels","img":null,"order":1444},{"name":"Keri Adams","character":"Bethany Snow","img":null,"order":1589}],"tv:82452":[{"name":"Gordon Cormier","character":"Aang","img":null,"order":0},{"name":"Kiawentiio","character":"Katara","img":null,"order":1},{"name":"Ian Ousley","character":"Sokka","img":null,"order":2},{"name":"Dallas Liu","character":"Zuko","img":null,"order":3},{"name":"Paul Sun-Hyung Lee","character":"Uncle Iroh","img":null,"order":4},{"name":"Miyako","character":"Toph Beifong","img":null,"order":5},{"name":"Ken Leung","character":"Commander Zhao","img":null,"order":6},{"name":"Daniel Dae Kim","character":"Fire Lord Ozai","img":null,"order":7},{"name":"Ruy Iskandar","character":"Lieutenant Jee","img":null,"order":504},{"name":"Maria Zhang","character":"Suki","img":null,"order":523},{"name":"Thalia Tran","character":"Mai","img":null,"order":532},{"name":"Momona Tamada","character":"Ty Lee","img":null,"order":533},{"name":"Matthew Yang King","character":"Appa/Momo(voice)","img":null,"order":568},{"name":"Elizabeth Yu","character":"Azula","img":null,"order":575},{"name":"Dee Bradley Baker","character":"Appa/Momo(voice)","img":null,"order":629}],"tv:284833":[{"name":"Saheli Khan","character":"Pavi (voice)","img":null,"order":0},{"name":"Aishwarya 'Aishu' Devan","character":"Nisha (voice)","img":null,"order":1},{"name":"Major Curda","character":"Jae (voice)","img":null,"order":2},{"name":"Akshay Khanna","character":"Karthik (voice)","img":null,"order":3},{"name":"Sakina Jaffrey","character":"Agam (voice)","img":null,"order":4},{"name":"Eric Bauza","character":"Jin-woo / Pich / One Guy in the Crowd / Crowd / Airbenders / Firebenders (voice)","img":null,"order":502},{"name":"Tobit Raphael","character":"Ulan / Bending Cadet #1 / Bending Cadets (voice)","img":null,"order":509},{"name":"Dee Bradley Baker","character":"Geet / Ruhi (voice)","img":null,"order":510},{"name":"Darren Barnet","character":"Daemin (voice)","img":null,"order":511},{"name":"Clyde Kusatsu","character":"Commander Bountan / Crowd / Airbenders / Firebenders (voice)","img":null,"order":513},{"name":"Sarah Anne","character":"Fang / Bending Cadet #5 / Elloran Girl (voice)","img":null,"order":514},{"name":"Brycen Taylor Hall","character":"Ping (voice)","img":null,"order":515},{"name":"Xanthe Huynh","character":"Lan / Elloran Citizens (voice)","img":null,"order":516},{"name":"Kimberly Woods","character":"Lucky Cadet / Bending Cadet #2 / Bending Cadets (voice)","img":null,"order":519},{"name":"Zehra Fazal","character":"Bending Cadet #3 / Bending Cadet #4 / Masked Elloran Woman (voice)","img":null,"order":519}],"tv:15804":[{"name":"Diedrich Bader","character":"Batman (voice)","img":null,"order":0},{"name":"James Arnold Taylor","character":"Green Arrow / Jace (voice)","img":null,"order":523},{"name":"John DiMaggio","character":"Aquaman / Mr. Freeze (voice)","img":null,"order":549},{"name":"Jeff Bennett","character":"Buddy / OMAC (voice)","img":null,"order":553},{"name":"Tara Strong","character":"Mary/Billy Batson (voice)","img":null,"order":606},{"name":"Jason Marsden","character":"Scooby-Doo Robin (voice)","img":null,"order":628},{"name":"Tom Kenny","character":"Babyface / Plastic Man (voice)","img":null,"order":648},{"name":"Dee Bradley Baker","character":"Felix Faust / Fluke (voice)","img":null,"order":665},{"name":"Kevin Michael Richardson","character":"Lex Luthor (voice)","img":null,"order":667},{"name":"Greg Ellis","character":"Cavalier / Computer Voice / Nuclear Engineer (voice)","img":null,"order":673},{"name":"Corey Burton","character":"Silver Cyclone (voice)","img":null,"order":682},{"name":"Nicholas Guest","character":"Question (voice)","img":null,"order":722},{"name":"Will Friedle","character":"Blue Beetle (voice)","img":null,"order":733},{"name":"Grey DeLisle","character":"Daphne (voice)","img":null,"order":736},{"name":"Tom Everett Scott","character":"Booster Gold (voice)","img":null,"order":759}],"tv:89247":[{"name":"Ruby Rose","character":"Kate Kane / Batwoman","img":null,"order":1},{"name":"Javicia Leslie","character":"Ryan Wilder / Batwoman","img":null,"order":2},{"name":"Rachel Skarsten","character":"Alice / Beth Kane","img":null,"order":3},{"name":"Meagan Tandy","character":"Sophie Moore","img":null,"order":4},{"name":"Nicole Kang","character":"Mary Hamilton","img":null,"order":5},{"name":"Camrus Johnson","character":"Luke Fox","img":null,"order":6},{"name":"Dougray Scott","character":"Jacob Kane","img":null,"order":7},{"name":"Nick Creegan","character":"Marquis Jet","img":null,"order":8},{"name":"Robin Givens","character":"Jada Jet","img":null,"order":9},{"name":"Elizabeth Anweis","character":"Catherine Hamilton-Kane","img":null,"order":10},{"name":"Christina Wolfe","character":"Julia Pennyworth","img":null,"order":679},{"name":"Rachel Maddow","character":"Vesper Fairchild (voice)","img":null,"order":836},{"name":"Sam Littlefield","character":"Mouse","img":null,"order":841},{"name":"Nathan Owens","character":"Ocean","img":null,"order":842},{"name":"Allison Riley","character":"Dana Dewitt","img":null,"order":859}],"tv:74204":[{"name":"Nick Kroll","character":"Nick Birch / Maury the Hormone Monster / Coach Steve / Lola Skumpy / Rick the Hormone Monster (voice)","img":null,"order":0},{"name":"John Mulaney","character":"Andrew Glouberman (voice)","img":null,"order":1},{"name":"Jessi Klein","character":"Jessi Glaser (voice)","img":null,"order":2},{"name":"Jason Mantzoukas","character":"Jay Bilzerian (voice)","img":null,"order":4},{"name":"Maya Rudolph","character":"Diane Birch / Connie the Hormone Monstress (voice)","img":null,"order":5},{"name":"Fred Armisen","character":"Elliot Birch (voice)","img":null,"order":6},{"name":"Jordan Peele","character":"Ghost of Duke Ellington (voice)","img":null,"order":7},{"name":"Ayo Edebiri","character":"Missy Foreman-Greenwald (voice)","img":null,"order":91},{"name":"Andrew Rannells","character":"Leander / Matthew MacDell (voice)","img":null,"order":136},{"name":"Jenny Slate","character":"Pharmacist / Caitlin (voice)","img":null,"order":149},{"name":"Joe Wengert","character":"Caleb / Go-Gurt Thief / Teacher (voice)","img":null,"order":578},{"name":"Paula Pell","character":"(voice)","img":null,"order":622},{"name":"Neil Casey","character":"Detective Dumont (voice)","img":null,"order":624},{"name":"June Diane Raphael","character":"","img":null,"order":639},{"name":"Richard Kind","character":"","img":null,"order":674}],"tv:71663":[{"name":"Cress Williams","character":"Jefferson Pierce / Black Lightning","img":null,"order":0},{"name":"Nafessa Williams","character":"Anissa Pierce / Thunder / Blackbird","img":null,"order":1},{"name":"China Anne McClain","character":"Jennifer Pierce / Lightning","img":null,"order":2},{"name":"Christine Adams","character":"Lynn Pierce","img":null,"order":3},{"name":"Marvin Jones III","character":"Tobias Whale","img":null,"order":4},{"name":"James Remar","character":"Peter Gambi","img":null,"order":5},{"name":"Jordan Calloway","character":"Khalil Payne / Painkiller","img":null,"order":6},{"name":"Chantal Thuy","character":"Grace Choi","img":null,"order":7},{"name":"Damon Gupton","character":"Bill Henderson (voice)","img":null,"order":211},{"name":"Clifton Powell","character":"Reverend Jeremiah Holt","img":null,"order":730},{"name":"Bill Duke","character":"Agent Percy Odell","img":null,"order":801},{"name":"Jennifer Riker","character":"Dr. Helga Jace","img":null,"order":802},{"name":"Christopher Ammanuel","character":"Baron / TC","img":null,"order":818},{"name":"William Catlett","character":"Latavious 'Lala' Johnson","img":null,"order":853},{"name":"Jahking Guillory","character":"Brandon","img":null,"order":855}],"movie:1574":[{"name":"Ren\u00e9e Zellweger","character":"Roxie Hart","img":null,"order":0},{"name":"Catherine Zeta-Jones","character":"Velma Kelly","img":null,"order":1},{"name":"Richard Gere","character":"Billy Flynn","img":null,"order":2},{"name":"Queen Latifah","character":"Matron Mama Morton","img":null,"order":3},{"name":"John C. Reilly","character":"Amos Hart","img":null,"order":4},{"name":"Colm Feore","character":"Harrison","img":null,"order":5},{"name":"Dominic West","character":"Fred Casely","img":null,"order":6},{"name":"Christine Baranski","character":"Mary Sunshine","img":null,"order":7},{"name":"Ekaterina Chtchelkanova","character":"Katalin Helinski \"Hunyak\"","img":null,"order":8},{"name":"Lucy Liu","character":"Kitty Baxter","img":null,"order":9},{"name":"Taye Diggs","character":"Bandleader","img":null,"order":10},{"name":"Mya","character":"Mona","img":null,"order":11},{"name":"Deidre Goodwin","character":"June","img":null,"order":12},{"name":"Denise Faye","character":"Annie","img":null,"order":13},{"name":"Susan Misner","character":"Liz","img":null,"order":14}],"movie:220289":[{"name":"Emily Baldoni","character":"Em","img":null,"order":0},{"name":"Maury Sterling","character":"Kevin","img":null,"order":1},{"name":"Nicholas Brendon","character":"Mike","img":null,"order":2},{"name":"Lauren Maher","character":"Laurie","img":null,"order":3},{"name":"Elizabeth Gracen","character":"Beth","img":null,"order":4},{"name":"Hugo Armstrong","character":"Hugh","img":null,"order":5},{"name":"Lorene Scafaria","character":"Lee","img":null,"order":6},{"name":"Alex Manugian","character":"Amir","img":null,"order":7},{"name":"Kelly Donovan","character":"Extra","img":null,"order":8},{"name":"Aqueela Zoll","character":"Extra","img":null,"order":9},{"name":"Mark Ballou","character":"Extra","img":null,"order":10},{"name":"Alexis Boozer Sterling","character":"Extra","img":null,"order":11}],"movie:399057":[{"name":"Colin Farrell","character":"Steven Murphy","img":null,"order":0},{"name":"Nicole Kidman","character":"Anna Murphy","img":null,"order":1},{"name":"Barry Keoghan","character":"Martin Lang","img":null,"order":2},{"name":"Raffey Cassidy","character":"Kim Murphy","img":null,"order":3},{"name":"Sunny Suljic","character":"Bob Murphy","img":null,"order":4},{"name":"Bill Camp","character":"Matthew Williams","img":null,"order":5},{"name":"Alicia Silverstone","character":"Martin's Mother","img":null,"order":6},{"name":"Herb Caillouet","character":"Ed Thompson (Hospital Director)","img":null,"order":7},{"name":"Barry G. Bernson","character":"Dr. Larry Banks","img":null,"order":8},{"name":"Denise Dal Vera","character":"Mary Williams","img":null,"order":9},{"name":"Drew Logan","character":"Principal","img":null,"order":10},{"name":"Michael Trester","character":"Elderly Man","img":null,"order":11},{"name":"Anita Farmer Bergman","character":"Nurse (uncredited)","img":null,"order":12},{"name":"Lea Hutton Beasmore","character":"Conference Guest (uncredited)","img":null,"order":13},{"name":"Dylan Keith Adams","character":"Doctor at Conference and Event Dinner (uncredited)","img":null,"order":14}],"movie:430214":[{"name":"Gen Hoshino","character":"Senpai (voice)","img":null,"order":0},{"name":"Kana Hanazawa","character":"The Girl with Black Hair (voice)","img":null,"order":1},{"name":"Ami Koshimizu","character":"Naoko (voice)","img":null,"order":2},{"name":"Aoi Yuki","character":"Princess Daruma (voice)","img":null,"order":3},{"name":"Hiroshi Kamiya","character":"Director of School Festival Operations (voice)","img":null,"order":4},{"name":"Chikara Honda","character":"Bookstore Shopkeeper Gabi (voice)","img":null,"order":5},{"name":"Hiroyuki Yoshino","character":"God of the Old Books Market (voice)","img":null,"order":6},{"name":"Junichi Suwabe","character":"Nise-J\u014dgasaki (voice)","img":null,"order":7},{"name":"Kazuhiro Yamaji","character":"Todo (voice)","img":null,"order":8},{"name":"Kazuya Nakai","character":"Seitar\u014d Higuchi (voice)","img":null,"order":9},{"name":"Mugihito","character":"Ri Haku (voice)","img":null,"order":10},{"name":"Nobuyuki Hiyama","character":"Johnny (voice)","img":null,"order":11},{"name":"Ryuji Akiyama","character":"Don Underwear (voice)","img":null,"order":12},{"name":"Seiko Niizuma","character":"Kiko (voice)","img":null,"order":13},{"name":"S\u014dichi Nakaoka","character":"K\u014dsaka (voice)","img":null,"order":14}],"movie:1541":[{"name":"Susan Sarandon","character":"Louise","img":null,"order":0},{"name":"Geena Davis","character":"Thelma","img":null,"order":1},{"name":"Harvey Keitel","character":"Hal","img":null,"order":2},{"name":"Michael Madsen","character":"Jimmy","img":null,"order":3},{"name":"Christopher McDonald","character":"Darryl","img":null,"order":4},{"name":"Stephen Tobolowsky","character":"Max","img":"uL8yfcm0dCFYFQt052kczVQVvac.jpg","order":5},{"name":"Brad Pitt","character":"J.D.","img":null,"order":6},{"name":"Timothy Carhart","character":"Harlan","img":null,"order":7},{"name":"Lucinda Jenney","character":"Lena, the Waitress","img":null,"order":8},{"name":"Jason Beghe","character":"State Trooper","img":"hrTwOPWek7nkPJgFhh6inFu7vSS.jpg","order":9},{"name":"Sonny Carl Davis","character":"Albert","img":null,"order":10},{"name":"Ken Swofford","character":"Major","img":null,"order":11},{"name":"Shelly Desai","character":"East Indian Motel Clerk","img":null,"order":12},{"name":"Carol Mansell","character":"Waitress","img":null,"order":13},{"name":"Stephen Polk","character":"Surveilance Man","img":null,"order":14}],"movie:5038":[{"name":"Scarlett Johansson","character":"Cristina","img":null,"order":0},{"name":"Rebecca Hall","character":"Vicky","img":null,"order":1},{"name":"Javier Bardem","character":"Juan Antonio Gonzalo","img":null,"order":2},{"name":"Pen\u00e9lope Cruz","character":"Mar\u00eda Elena","img":null,"order":3},{"name":"Christopher Evan Welch","character":"Narrator (voice)","img":null,"order":4},{"name":"Chris Messina","character":"Doug","img":null,"order":5},{"name":"Patricia Clarkson","character":"Judy Nash","img":null,"order":6},{"name":"Kevin Dunn","character":"Mark Nash","img":null,"order":7},{"name":"Julio Perill\u00e1n","character":"Charles","img":null,"order":8},{"name":"Ricard Salom","character":"Art Gallery Guest","img":null,"order":9},{"name":"Maurice Sonnenberg","character":"Art Gallery Guest","img":null,"order":10},{"name":"Manel Barcel\u00f3","character":"Doctor","img":null,"order":11},{"name":"Josep Maria Dom\u00e8nech","character":"Julio","img":null,"order":12},{"name":"Emilio de Benito","character":"Guitarist in Asturias","img":null,"order":13},{"name":"Jaume Montan\u00e9","character":"Juan Antonio's Friend","img":null,"order":14}],"tv:108255":[{"name":"Nicholas Ralph","character":"James Herriot","img":null,"order":0},{"name":"Samuel West","character":"Siegfried Farnon","img":null,"order":1},{"name":"Anna Madeley","character":"Mrs Hall","img":null,"order":2},{"name":"Callum Woodhouse","character":"Tristan Farnon","img":null,"order":3},{"name":"Rachel Shenton","character":"Helen Herriot","img":null,"order":4},{"name":"James Anthony-Rose","character":"Richard Carmody","img":null,"order":500},{"name":"Mollie Winnard","character":"Maggie","img":null,"order":528},{"name":"Maimie McCoy","character":"Dorothy","img":null,"order":530},{"name":"Drew Cain","character":"James Herriot Senior","img":null,"order":535},{"name":"Will Thorp","character":"Gerald Hammond","img":null,"order":539},{"name":"Matthew Lewis","character":"Hugh Hulton","img":null,"order":552},{"name":"Patricia Hodge","character":"Mrs. Pumphrey","img":null,"order":553},{"name":"Imogen Clawson","character":"Jenny Alderson","img":null,"order":556},{"name":"Tony Pitts","character":"Richard Alderson","img":null,"order":558},{"name":"Gaia Wise","character":"Charlotte Beauvoir","img":null,"order":582}],"tv:88510":[{"name":"Daisy Haggard","character":"Miri Matteson","img":null,"order":0},{"name":"Geraldine James","character":"Caroline Matteson","img":null,"order":1},{"name":"Richard Durden","character":"Oscar Matteson","img":null,"order":2},{"name":"Jo Martin","character":"Janice","img":null,"order":3},{"name":"Adeel Akhtar","character":"Billy","img":null,"order":4},{"name":"Jamie Michie","character":"Dom","img":null,"order":5},{"name":"Christine Bottomley","character":"Mandy","img":null,"order":6},{"name":"Frank Feys","character":"Samuel","img":null,"order":502},{"name":"Liam Williams","character":"Nathan","img":null,"order":504},{"name":"Imogen Gurney","character":"Lara","img":null,"order":505},{"name":"Juliet Cowan","character":"Tina","img":null,"order":510},{"name":"Lizzy McInnerny","character":"Norah Boback","img":null,"order":515},{"name":"Adrian Edmondson","character":"John Boback","img":null,"order":516},{"name":"Keir Charles","character":"Mark","img":null,"order":520},{"name":"Hebe Bartlett","character":"Cerys","img":null,"order":522}],"tv:202851":[{"name":"Jack Rooke","character":"Narrator (voice)","img":null,"order":0},{"name":"Dylan Llewellyn","character":"Jack","img":null,"order":1},{"name":"Camille Coduri","character":"Peggy","img":null,"order":2},{"name":"Jon Pointing","character":"Danny","img":null,"order":3},{"name":"Katy Wix","character":"Jules","img":null,"order":4},{"name":"Izuka Hoyle","character":"Corinne","img":null,"order":5},{"name":"Olisa Odele","character":"Yemi","img":null,"order":6},{"name":"Harriet Webb","character":"Shannon","img":null,"order":7},{"name":"Annette Badland","character":"Nanny Bingo","img":null,"order":8},{"name":"Robert Gilbert","character":"Tim","img":null,"order":501},{"name":"Sheila Reid","character":"Iris","img":null,"order":502},{"name":"Lucia Keskin","character":"Kelly","img":null,"order":503},{"name":"Shane Zaza","character":"Tariq","img":null,"order":504},{"name":"Callum Mardy","character":"Ash","img":null,"order":505},{"name":"Ian Burfield","character":"Lance (voice)","img":null,"order":506}],"tv:39793":[{"name":"Vanessa Redgrave","character":"Jennifer Worth (voice)","img":null,"order":0},{"name":"Jenny Agutter","character":"Sister Julienne","img":null,"order":2},{"name":"Judy Parfitt","character":"Sister Monica Joan","img":null,"order":3},{"name":"Linda Bassett","character":"Phyllis Crane","img":null,"order":12},{"name":"Stephen McGann","character":"Dr Patrick Turner","img":null,"order":14},{"name":"Alice Brown","character":"Angela Turner","img":null,"order":23},{"name":"Laura Main","character":"Nurse Shelagh Turner","img":null,"order":24},{"name":"Cliff Parisi","character":"Fred Buckle","img":null,"order":26},{"name":"Georgie Glen","character":"Miss Millicent Higgins","img":null,"order":28},{"name":"Victoria Yeates","character":"Sister Winifred","img":null,"order":35},{"name":"Annabelle Apsion","character":"Violet Buckle","img":null,"order":87},{"name":"Max Macmillan","character":"Timothy Turner","img":null,"order":94},{"name":"Helen George","character":"Nurse Trixie Aylward","img":null,"order":102},{"name":"Zephryn Taitte","character":"Cyril Robinson","img":null,"order":104},{"name":"April Rae Hoang","character":"May Tang","img":null,"order":237}],"tv:76148":[{"name":"Saoirse-Monica Jackson","character":"Erin Quinn","img":null,"order":0},{"name":"Louisa Harland","character":"Orla McCool","img":null,"order":1},{"name":"Nicola Coughlan","character":"Clare Devlin","img":null,"order":2},{"name":"Jamie Lee O'Donnell","character":"Michelle Mallon","img":null,"order":3},{"name":"Dylan Llewellyn","character":"James Maguire","img":null,"order":4},{"name":"Tara Lynne O'Neill","character":"Ma Mary Quinn","img":null,"order":5},{"name":"Tommy Tiernan","character":"Da Gerry Quinn","img":null,"order":6},{"name":"Kathy Kiera Clarke","character":"Aunt Sarah","img":null,"order":7},{"name":"Ian McElhinney","character":"Granda Joe","img":null,"order":8},{"name":"Siobh\u00e1n McSweeney","character":"Sister Michael","img":null,"order":9},{"name":"Leah O'Rourke","character":"Jenny Joyce","img":null,"order":10},{"name":"Donna Traynor","character":"Donna Traynor","img":null,"order":555},{"name":"Beccy Henderson","character":"Aisling","img":null,"order":558},{"name":"Claire Rafferty","character":"Miss Mooney","img":null,"order":559},{"name":"Phillipa Dunne","character":"Geraldine Devlin","img":null,"order":572}],"tv:693":[{"name":"Teri Hatcher","character":"Susan Mayer","img":null,"order":0},{"name":"Felicity Huffman","character":"Lynette Scavo","img":null,"order":1},{"name":"Marcia Cross","character":"Bree Van de Kamp","img":null,"order":2},{"name":"Eva Longoria","character":"Gabrielle Solis","img":null,"order":3},{"name":"Ricardo Chavira","character":"Carlos Solis","img":null,"order":4},{"name":"Doug Savant","character":"Tom Scavo","img":null,"order":5},{"name":"Nicollette Sheridan","character":"Edie Britt","img":null,"order":6},{"name":"Mark Moses","character":"Paul Young","img":null,"order":7},{"name":"James Denton","character":"Mike Delfino","img":null,"order":8},{"name":"Brenda Strong","character":"Mary Alice Young","img":null,"order":10},{"name":"Kyle MacLachlan","character":"Orson Hodge","img":null,"order":14},{"name":"Shawn Pyfrom","character":"Andrew Van de Kamp","img":null,"order":19},{"name":"Andrea Bowen","character":"Julie Mayer","img":null,"order":21},{"name":"Kathryn Joosten","character":"Karen McCluskey","img":null,"order":25},{"name":"Joy Jorgensen","character":"Danielle Van de Kamp","img":null,"order":717}],"tv:203857":[{"name":"Keri Russell","character":"Ambassador Kate Wyler","img":null,"order":0},{"name":"Rufus Sewell","character":"Hal Wyler","img":null,"order":1},{"name":"David Gyasi","character":"Foreign Secretary Austin Dennison","img":null,"order":2},{"name":"Ali Ahn","character":"Eidra Park","img":null,"order":3},{"name":"Rory Kinnear","character":"Prime Minister Nicol Trowbridge","img":null,"order":4},{"name":"Ato Essandoh","character":"Stuart Hayford","img":null,"order":5},{"name":"Allison Janney","character":"Grace Penn","img":null,"order":7},{"name":"Nana Mensah","character":"Billie Appiah","img":null,"order":8},{"name":"Celia Imrie","character":"Meg Roylin","img":null,"order":500},{"name":"Penny Downie","character":"Frances Munning","img":null,"order":505},{"name":"Adam Silver","character":"Howard","img":null,"order":506},{"name":"Tim Delap","character":"Byron","img":null,"order":507},{"name":"Kenichiro Thomson","character":"Martin","img":null,"order":508},{"name":"Georgie Henley","character":"Pensy","img":null,"order":509},{"name":"Graham Miller","character":"Neil Barrow","img":null,"order":572}],"tv:66134":[{"name":"Keeley Hawes","character":"Louisa Durrell","img":null,"order":0},{"name":"Josh O'Connor","character":"Lawrence Durrell","img":null,"order":1},{"name":"Milo Parker","character":"Gerald Durrell","img":null,"order":2},{"name":"Daisy Waterstone","character":"Margo Durrell","img":null,"order":3},{"name":"Callum Woodhouse","character":"Leslie Durrell","img":null,"order":4},{"name":"Alexis Georgoulis","character":"Spiros Halikiopoulos","img":null,"order":5},{"name":"Yorgos Karamihos","character":"Theo Stephanides","img":null,"order":6},{"name":"Anna Savva","character":"Lugaretzia","img":null,"order":8},{"name":"Lucy Black","character":"Florence Petridis","img":null,"order":79},{"name":"Ben Hall","character":"Donald","img":null,"order":525},{"name":"Nikorestis Chaniotakis","character":"Pavlos the Monk","img":null,"order":553},{"name":"Elli Tringou","character":"Daphne Likourgou","img":null,"order":553},{"name":"Ulric von der Esch","character":"Sven","img":null,"order":578},{"name":"Alexis Conran","character":"Dr. Petridis","img":null,"order":579},{"name":"Miles Jupp","character":"Basil","img":null,"order":584}],"tv:225383":[{"name":"Luke Kirby","character":"Jack McMillan","img":null,"order":0},{"name":"Charlotte Gainsbourg","character":"Genevi\u00e8ve Lavigne","img":null,"order":1},{"name":"Lou de La\u00e2ge","character":"Cheyenne Toussaint","img":null,"order":2},{"name":"Gideon Glick","character":"Tobias Bell","img":null,"order":3},{"name":"David Alvarez","character":"Gael Rodriguez","img":null,"order":4},{"name":"Ivan du Pontavice","character":"Gabin","img":null,"order":5},{"name":"Ta\u00efs Vinolo","character":"Mishi Duplessis","img":null,"order":6},{"name":"David Haig","character":"Nicholas","img":null,"order":7},{"name":"LaMay Zhang","character":"Susu","img":null,"order":8},{"name":"Simon Callow","character":"Crispin Shamblee","img":null,"order":9},{"name":"Yanic Truesdale","character":"Rapha\u00ebl Marchand","img":null,"order":10},{"name":"Etai Benson","character":"Curt","img":null,"order":500},{"name":"Steve Belanger","character":"Al","img":null,"order":501}],"tv:4586":[{"name":"Lauren Graham","character":"Lorelai Gilmore","img":null,"order":0},{"name":"Alexis Bledel","character":"Rory Gilmore","img":null,"order":1},{"name":"Scott Patterson","character":"Luke Danes","img":null,"order":2},{"name":"Melissa McCarthy","character":"Sookie St. James","img":null,"order":3},{"name":"Keiko Agena","character":"Lane Kim","img":null,"order":4},{"name":"Yanic Truesdale","character":"Michel Gerard","img":null,"order":5},{"name":"Liza Weil","character":"Paris Geller","img":null,"order":6},{"name":"Jared Padalecki","character":"Dean Forester","img":null,"order":8},{"name":"Matt Czuchry","character":"Logan Huntzberger","img":null,"order":9},{"name":"Kelly Bishop","character":"Emily Gilmore","img":null,"order":10},{"name":"Edward Herrmann","character":"Richard Gilmore","img":null,"order":12},{"name":"Sean Gunn","character":"Kirk Gleason","img":null,"order":60},{"name":"Liz Torres","character":"Miss Patty","img":null,"order":692},{"name":"Michael Winters","character":"Taylor Doose","img":null,"order":697},{"name":"Jackson Douglas","character":"Jackson Belleville","img":null,"order":746}],"tv:117581":[{"name":"Antonia Gentry","character":"Virginia 'Ginny' Miller","img":null,"order":0},{"name":"Brianne Howey","character":"Georgia Miller","img":null,"order":1},{"name":"Diesel La Torraca","character":"Austin Miller","img":null,"order":2},{"name":"Jennifer Robertson","character":"Ellen Baker","img":null,"order":3},{"name":"Felix Mallard","character":"Marcus Baker","img":null,"order":4},{"name":"Sara Waisglass","character":"Maxine Baker","img":null,"order":5},{"name":"Scott Porter","character":"Mayor Paul Randolph","img":null,"order":6},{"name":"Raymond Ablack","character":"Joe","img":null,"order":7},{"name":"Mason Temple","character":"Hunter Chen","img":null,"order":8},{"name":"Katie Douglas","character":"Abigail 'Abby' Littman","img":null,"order":9},{"name":"Chelsea Clark","character":"Norah","img":null,"order":10},{"name":"Nikki Roumel","character":"Young Georgia","img":null,"order":205},{"name":"Sabrina Grdevich","character":"Cynthia Fuller","img":null,"order":502},{"name":"Colton Gobbo","character":"Jordan","img":null,"order":503},{"name":"Tyssen Smith","character":"Brodie","img":null,"order":504}],"tv:32504":[{"name":"Aly Michalka","character":"Marti Perkins","img":null,"order":0},{"name":"Ashley French","character":"Savannah Monroe","img":null,"order":1},{"name":"Heather Hemmens","character":"Alice Verdura","img":null,"order":2},{"name":"Robbie Jones","character":"Lewis Flynn","img":null,"order":3},{"name":"Matt Barr","character":"Dan Patch","img":null,"order":4},{"name":"Gail O'Grady","character":"Wanda Perkins","img":null,"order":5},{"name":"Sharon Leal","character":"Vanessa Lodge","img":null,"order":6},{"name":"Aaron Douglas","character":"Bill Marsh","img":null,"order":500},{"name":"Jeff Hephner","character":"Red Raymond","img":null,"order":501},{"name":"D. B. Woodside","character":"Derrick Altman","img":null,"order":502},{"name":"Elena Esovolova","character":"Patty Wedgerman","img":null,"order":503},{"name":"Emma Lahana","character":"Charlotte Monroe","img":null,"order":504},{"name":"Teryl Rothery","character":"Layne Monroe","img":null,"order":505},{"name":"Lauren Watson","character":"Judge (uncredited)","img":null,"order":506},{"name":"John Pyper-Ferguson","character":"Rex Perkins","img":null,"order":507}],"tv:226637":[{"name":"Kaitlin Olson","character":"Morgan Gillory","img":null,"order":0},{"name":"Daniel Sunjata","character":"Adam Karadec","img":null,"order":1},{"name":"Judy Reyes","character":"Selena Soto","img":null,"order":2},{"name":"Javicia Leslie","character":"Daphne Forrester","img":null,"order":3},{"name":"Deniz Akdeniz","character":"Lev 'Oz' Ozdil","img":null,"order":4},{"name":"Amirah J","character":"Ava Gillory","img":null,"order":5},{"name":"Matthew Lamb","character":"Elliot Radovic","img":null,"order":6},{"name":"Taran Killam","character":"Ludo Radovic","img":null,"order":500},{"name":"Garret Dillahunt","character":"Lieutenant Melon","img":null,"order":520},{"name":"TJ Lee","character":"Officer Lin","img":null,"order":522},{"name":"J. D. Pardo","character":"Tom","img":null,"order":570},{"name":"Mekhi Phifer","character":"Arthur Ellis","img":null,"order":686},{"name":"Steve Howey","character":"Nick Wagner","img":null,"order":719},{"name":"John Pyper-Ferguson","character":"Eric Hayworth","img":null,"order":754},{"name":"Susan Kelechi Watson","character":"Lucia","img":null,"order":774}],"tv:233079":[{"name":"Roisin Gallagher","character":"Saoirse","img":null,"order":0},{"name":"Sin\u00e9ad Keenan","character":"Robyn","img":null,"order":1},{"name":"Caoilfhionn Dunne","character":"Dara","img":null,"order":2},{"name":"Darragh Hand","character":"Liam","img":null,"order":3},{"name":"Natasha O'Keeffe","character":"Greta","img":null,"order":500},{"name":"Bronagh Gallagher","character":"Booker","img":null,"order":501},{"name":"Emmett J. Scanlan","character":"Owen","img":null,"order":502},{"name":"Emma Canning","character":"Young Greta","img":null,"order":503},{"name":"Michelle Fairley","character":"Margo","img":null,"order":504},{"name":"Jenn Murray","character":"Jamie","img":null,"order":510},{"name":"Eleanor Methven","character":"Mrs. Friel","img":null,"order":511},{"name":"Ardal O'Hanlon","character":"S\u00e9amus","img":null,"order":512},{"name":"Tom Basden","character":"Seb","img":null,"order":513},{"name":"Matilda Freeman","character":"Maria","img":null,"order":515},{"name":"Josh Finan","character":"Andrew Sampson","img":null,"order":520}],"tv:244447":[{"name":"Brittany Snow","character":"Sophie O\u2019Neill","img":null,"order":0},{"name":"Malin Akerman","character":"Margo Banks","img":null,"order":1},{"name":"Dermot Mulroney","character":"Jed Banks","img":null,"order":2},{"name":"Katie Lowes","character":"Jill Thompson","img":null,"order":3},{"name":"Jaime Ray Newman","character":"Callie","img":null,"order":4},{"name":"George Ferrier","character":"Brad Thompson","img":null,"order":4},{"name":"Chrissy Metz","character":"Starr Jackson","img":null,"order":4},{"name":"Evan Jonigkeit","character":"Graham O\u2019Neill","img":null,"order":5},{"name":"Alex Fitzalan","character":"Lincoln Trout","img":null,"order":6},{"name":"Hunter Emery","character":"Deputy Walter Flynn","img":null,"order":8},{"name":"Steve D'Avria","character":"Sheriff's Deputy","img":null,"order":9},{"name":"Karen Rodriguez","character":"Deputy Wanda Salazar","img":null,"order":10},{"name":"Madison Wolfe","character":"Abby Jackson","img":null,"order":503},{"name":"Alexandria DeBerry","character":"Taylor Tucker","img":null,"order":504},{"name":"Joyce Glenn","character":"Monae Roberts","img":null,"order":505}],"tv:125816":[{"name":"Sigourney Weaver","character":"June Hart","img":null,"order":0},{"name":"Alycia Debnam-Carey","character":"Alice Hart","img":null,"order":1},{"name":"Alyla Browne","character":"Young Alice Hart","img":null,"order":2},{"name":"Leah Purcell","character":"Twig","img":null,"order":3},{"name":"Frankie Adams","character":"Candy","img":null,"order":4},{"name":"Tilda Cobham-Hervey","character":"Agnes Hart","img":null,"order":5},{"name":"Asher Keddie","character":"Sally Morgan","img":null,"order":6},{"name":"Alexander England","character":"John Morgan","img":null,"order":7},{"name":"Charlie Vickers","character":"Clem Hart","img":null,"order":8},{"name":"Sebasti\u00e1n Zurita","character":"Dylan","img":null,"order":9},{"name":"Maggie Dence","character":"Boo","img":null,"order":502},{"name":"Dalara Williams","character":"Myf","img":null,"order":504},{"name":"Amy Kersey","character":"Rosie","img":null,"order":506},{"name":"Vivienne Awosoga","character":"Lulu","img":null,"order":514},{"name":"Shareena Clanton","character":"Ruby","img":null,"order":515}],"tv:99353":[{"name":"Nick Lachey","character":"Self - Host","img":null,"order":0},{"name":"Vanessa Lachey","character":"Self - Host","img":null,"order":1},{"name":"Cole Barnett","character":"Self - Contestant","img":null,"order":511},{"name":"Paul Peden","character":"Self - Contestant","img":null,"order":545},{"name":"Colleen Reed","character":"Self - Contestant","img":null,"order":581},{"name":"Matt Bolton","character":"Self - Contestant","img":null,"order":583},{"name":"Micah Lussier","character":"Self - Video Message","img":null,"order":594},{"name":"Alexa Alfia","character":"Self - Contestant","img":null,"order":602},{"name":"Brennon Lemieux","character":"Self - Video Message","img":null,"order":605},{"name":"Tiffany Pennywell","character":"Self - Contestant","img":null,"order":606},{"name":"Brett Brown","character":"Self - Video Message","img":null,"order":607},{"name":"Chelsea Griffin Appiah","character":"Self - Contestant","img":null,"order":607},{"name":"Zack Goytowski","character":"Self - Video Message","img":null,"order":608},{"name":"Kwame Appiah","character":"Self - Video Message","img":null,"order":610},{"name":"Bliss Poureetezadi","character":"Self - Video Message","img":null,"order":616}],"tv:285404":[{"name":"Tatiana Maslany","character":"Paula Sanders","img":null,"order":0},{"name":"Jake Johnson","character":"Karl Hendricks","img":null,"order":1},{"name":"Dolly de Leon","character":"Detective Sofia Gonzalez","img":null,"order":2},{"name":"Jessy Hodges","character":"Mallory","img":null,"order":3},{"name":"Jon Michael Hill","character":"Detective Baxter","img":null,"order":4},{"name":"Charlie Hall","character":"Rudy","img":null,"order":5},{"name":"Kiarra Hamagami Goldberg","character":"Geri","img":null,"order":6},{"name":"Nola Wallace","character":"Hazel","img":null,"order":7},{"name":"Brandon Flynn","character":"Trevor","img":null,"order":500},{"name":"Nina Bloomgarden","character":"Ash","img":null,"order":502},{"name":"Daniel Dale","character":"Sky","img":null,"order":504},{"name":"Remy Auberjonois","character":"Doug","img":null,"order":505},{"name":"Grace Porter","character":"Gwynn","img":null,"order":506},{"name":"Raymond Lee","character":"Steve","img":null,"order":507},{"name":"Murray Bartlett","character":"Dennis","img":null,"order":509}],"tv:118303":[{"name":"Ophelia Lovibond","character":"Joyce","img":null,"order":0},{"name":"Jake Johnson","character":"Doug","img":null,"order":1},{"name":"Michael Angarano","character":"Glenn","img":null,"order":2},{"name":"Jessica Lowe","character":"Bambi","img":null,"order":3},{"name":"Oscar Montoya","character":"Richie","img":null,"order":4},{"name":"Lennon Parham","character":"Shelly","img":null,"order":5},{"name":"Idara Victor","character":"Tina","img":null,"order":6},{"name":"Sofia Gonzalez","character":"Rita","img":null,"order":554},{"name":"David Lee Hess","character":"Phil","img":null,"order":557},{"name":"Jaeden Bettencourt","character":"Tommy","img":null,"order":572},{"name":"Rich Sommer","character":"Lenny","img":null,"order":576},{"name":"Elizabeth Perkins","character":"Constance","img":null,"order":579},{"name":"Lincoln A. Castellanos","character":"Daniel","img":null,"order":597},{"name":"Liora Belle","character":"Millie","img":null,"order":744},{"name":"Vera Myers","character":"Bottom Dollar Playmate (uncredited)","img":null,"order":766}],"tv:33155":[{"name":"Don Hany","character":"","img":null,"order":0},{"name":"Asher Keddie","character":"Nina Proudman","img":null,"order":1},{"name":"Deborah Mailman","character":"","img":null,"order":2},{"name":"Kat Stewart","character":"Billie Proudman","img":null,"order":3},{"name":"Richard Davies","character":"Jimmy Proudman","img":null,"order":4},{"name":"Linda Cropper","character":"Geraldine Proudman","img":null,"order":5},{"name":"Jane Harber","character":"Zara Perkich","img":null,"order":6},{"name":"Alicia Gardiner","character":"Kim Akerholt","img":null,"order":7},{"name":"Eddie Perfect","character":"Mick Holland","img":null,"order":8},{"name":"John Waters","character":"","img":null,"order":9},{"name":"Matthew Le Nevez","character":"","img":null,"order":10},{"name":"Lachy Hulme","character":"","img":null,"order":11},{"name":"Clare Bowditch","character":"Rosanna Harding","img":null,"order":12},{"name":"Kate Atkinson","character":"Renee","img":null,"order":508},{"name":"Shannon Berry","character":"Brody Jordan","img":null,"order":511}],"tv:107113":[{"name":"Steve Martin","character":"Charles-Haden Savage","img":null,"order":0},{"name":"Martin Short","character":"Oliver Putnam","img":null,"order":1},{"name":"Selena Gomez","character":"Mabel Mora","img":null,"order":2},{"name":"Amy Ryan","character":"Jan Bellows","img":null,"order":3},{"name":"Michael Cyril Creighton","character":"Howard Morris","img":null,"order":5},{"name":"Jackie Hoffman","character":"Uma Heller","img":null,"order":542},{"name":"Da'Vine Joy Randolph","character":"Donna Williams","img":null,"order":567},{"name":"Vanessa Aspillaga","character":"Ursula","img":null,"order":576},{"name":"Teddy Coluca","character":"Lester","img":null,"order":577},{"name":"Russell G. Jones","character":"Grover Stanley","img":null,"order":583},{"name":"Jayne Houdyshell","character":"Bunny Folger","img":null,"order":586},{"name":"Ryan Broussard","character":"Will Putnam","img":null,"order":588},{"name":"Meryl Streep","character":"Loretta Durkin","img":null,"order":598},{"name":"Paul Rudd","character":"Ben Glenroy","img":null,"order":644},{"name":"Richard Kind","character":"Vince Fish","img":null,"order":659}],"tv:207484":[{"name":"Harriet Slater","character":"Ellen MacKenzie","img":null,"order":0},{"name":"Jamie Roy","character":"Brian Fraser","img":null,"order":1},{"name":"Hermione Corfield","character":"Julia Moriston Beauchamp","img":null,"order":2},{"name":"Jeremy Irvine","character":"Henry Beauchamp","img":null,"order":3},{"name":"Tony Curran","character":"Lord Lovat (Simon Fraser)","img":null,"order":4},{"name":"S\u00e9amus McLean Ross","character":"Colum MacKenzie","img":null,"order":5},{"name":"Sam Retford","character":"Dougal MacKenzie","img":null,"order":6},{"name":"Rory Alexander","character":"Murtagh Fitzgibbons Fraser","img":null,"order":7},{"name":"Conor MacNeill","character":"Ned Gowan","img":null,"order":8},{"name":"Sara Vickers","character":"Davina Porter","img":null,"order":9},{"name":"Sadhbh Malin","character":"Jocasta MacKenzie Cameron","img":null,"order":502},{"name":"Jhon Lumsden","character":"Malcolm Grant","img":null,"order":503},{"name":"Sally Messham","character":"Mrs. Fitzgibbons","img":null,"order":506},{"name":"Terence Rae","character":"Arch Bug","img":null,"order":507},{"name":"Chick Allan","character":"Balloch","img":null,"order":514}],"tv:208921":[{"name":"David Tennant","character":"Tony Baddingham","img":null,"order":0},{"name":"Aidan Turner","character":"Declan O'Hara","img":null,"order":1},{"name":"Danny Dyer","character":"Freddie Jones","img":null,"order":2},{"name":"Alex Hassell","character":"Rupert Campbell-Black","img":null,"order":3},{"name":"Nafessa Williams","character":"Cameron Cook","img":null,"order":4},{"name":"Bella Maclean","character":"Taggie O'Hara","img":null,"order":5},{"name":"Katherine Parkinson","character":"Lizzie Vereker","img":null,"order":6},{"name":"Victoria Smurfit","character":"Maud O'Hara","img":null,"order":7},{"name":"Claire Rushbrook","character":"Lady Monica Baddingham","img":null,"order":8},{"name":"Oliver Chris","character":"James Vereker","img":null,"order":9},{"name":"Lisa McGrillis","character":"Valerie Jones","img":null,"order":10},{"name":"Emily Atack","character":"Sarah Stratton","img":null,"order":11},{"name":"Rufus Jones","character":"Paul Stratton","img":null,"order":12},{"name":"Luke Pasqualino","character":"Basil 'Bas' Baddingham","img":null,"order":13},{"name":"Catriona Chandler","character":"Caitlin O'Hara","img":null,"order":14}],"tv:254953":[{"name":"Steve Carell","character":"Greg Russo","img":null,"order":0},{"name":"Charly Clive","character":"Katie Russo","img":null,"order":1},{"name":"Danielle Deadwyler","character":"Dylan Shepard","img":null,"order":2},{"name":"Phil Dunster","character":"Archie Bates","img":null,"order":3},{"name":"John C. McGinley","character":"Walter Mann","img":null,"order":4},{"name":"Lauren Tsai","character":"Sunny Salewski","img":null,"order":5},{"name":"Annie Mumolo","character":"Cristle","img":null,"order":500},{"name":"Rory Scovel","character":"Officer Donnie Mullins","img":null,"order":501},{"name":"Cece Abbey","character":"Maya","img":null,"order":503},{"name":"Xavier Beloved","character":"George Heitman","img":null,"order":504},{"name":"Noah Grismer","character":"J.D.","img":null,"order":507},{"name":"Madison Hu","character":"Eva","img":null,"order":508},{"name":"Evan Jachelski","character":"Spooner","img":null,"order":509},{"name":"Maximo Salas","character":"Tommy Newton","img":null,"order":515},{"name":"Robby Hoffman","character":"Mo","img":null,"order":518}],"tv:105":[{"name":"Sarah Jessica Parker","character":"Carrie Bradshaw","img":null,"order":0},{"name":"Kim Cattrall","character":"Samantha Jones","img":null,"order":1},{"name":"Cynthia Nixon","character":"Miranda Hobbes","img":null,"order":2},{"name":"Kristin Davis","character":"Charlotte York","img":null,"order":3},{"name":"Lynn Cohen","character":"Magda","img":null,"order":826},{"name":"Willie Garson","character":"Stanford Blatch","img":null,"order":962},{"name":"Mario Cantone","character":"Anthony Marantino","img":null,"order":964},{"name":"Chris Noth","character":"Mr. Big","img":null,"order":982},{"name":"Kyle MacLachlan","character":"Trey MacDougal","img":null,"order":1001},{"name":"David Eigenberg","character":"Steve Brady","img":null,"order":1022},{"name":"Frances Sternhagen","character":"Bunny MacDougal","img":null,"order":1023},{"name":"John Corbett","character":"Aidan Shaw","img":null,"order":1026},{"name":"James Remar","character":"Richard Wright","img":null,"order":1040},{"name":"Jason Lewis","character":"Jerry 'Smith' Jerrod","img":null,"order":1062},{"name":"Evan Handler","character":"Harry Goldenblatt","img":"yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg","order":1096}],"tv:133908":[{"name":"Natasha Calis","character":"Hayley Roberts","img":null,"order":0},{"name":"Morgan Holmstrom","character":"Crystal Highway","img":null,"order":1},{"name":"Aaron Ashmore","character":"William \"Wheezer\" Heaseman","img":null,"order":2},{"name":"Mercedes Morris","character":"Lexi Martine","img":null,"order":3},{"name":"Praneet Akilla","character":"Jay \"Chopper\" Chopra","img":null,"order":4},{"name":"Aason Nadjiwan","character":"Austen Bodie","img":null,"order":5},{"name":"Alexander Eling","character":"Wyatt Ellis","img":null,"order":6},{"name":"Thomas Elms","character":"Milosz Nowak","img":null,"order":8},{"name":"Kheon Clarke","character":"Tristan Green","img":null,"order":10},{"name":"Rebecca Kwan","character":"Emma Lin","img":null,"order":12},{"name":"Braeden Clarke","character":"Jeremy Wood","img":null,"order":14},{"name":"Emilia McCarthy","character":"Madison Van Camp","img":null,"order":15},{"name":"Patrick Kwok-Choon","character":"Trevor Sung","img":null,"order":16},{"name":"Sydney Kuhne","character":"Stef","img":null,"order":17},{"name":"Anthony Grant","character":"TJ","img":null,"order":18}],"tv:40231":[{"name":"EliZe","character":"Self - Presenter","img":null,"order":0},{"name":"Dan Karaty","character":"Self - Jury","img":null,"order":1},{"name":"Euvgenia Parakhina","character":"Self - Jury","img":null,"order":2},{"name":"Jaakko Toivonen","character":"Self - Jury","img":null,"order":3},{"name":"Nicky Vernieuwe","character":"Self - Jury","img":null,"order":4},{"name":"Marco Gerris","character":"Self - Jury","img":null,"order":5},{"name":"Min Hee Bervoets","character":"Self - Jury","img":null,"order":6},{"name":"Roberto da Costa","character":"Guest Jury","img":null,"order":7},{"name":"Brahim Attaeb","character":"Guest Jury","img":null,"order":8},{"name":"Eszteca Noya","character":"Guest Jury","img":null,"order":9},{"name":"Dennis Weening","character":"Self - Presenter","img":null,"order":10},{"name":"Ish A\u00eft Hamou","character":"Self - Jury","img":null,"order":11},{"name":"Eline De Munck","character":"Self - Presenter","img":null,"order":12},{"name":"Jan Kooijman","character":"Self - Jury","img":null,"order":13},{"name":"Wendy van Dijk","character":"Self - Jury","img":null,"order":257}],"tv:111803":[{"name":"Leslie Bibb","character":"Kate Bohr","img":null,"order":0},{"name":"Jennifer Coolidge","character":"Tanya McQuoid","img":null,"order":1},{"name":"Carrie Coon","character":"Laurie Duffy","img":null,"order":1},{"name":"Walton Goggins","character":"Rick Hatchett","img":null,"order":2},{"name":"Sarah Catherine Hook","character":"Piper Ratliff","img":null,"order":3},{"name":"Jason Isaacs","character":"Timothy Ratliff","img":null,"order":4},{"name":"Jon Gries","character":"Greg Hunt","img":null,"order":5},{"name":"Parker Posey","character":"Victoria Ratliff","img":null,"order":5},{"name":"Patrick Schwarzenegger","character":"Saxon Ratliff","img":null,"order":6},{"name":"Tayme Thapthimthong","character":"Gaitok","img":null,"order":7},{"name":"Aimee Lou Wood","character":"Chelsea","img":null,"order":8},{"name":"LISA","character":"Mook","img":null,"order":9},{"name":"Michelle Monaghan","character":"Jaclyn Lemon","img":null,"order":10},{"name":"Natasha Rothwell","character":"Belinda Lindsey","img":null,"order":11},{"name":"Sam Nivola","character":"Lochlan Ratliff","img":null,"order":12}],"tv:117488":[{"name":"Melanie Lynskey","character":"Shauna Sadecki","img":null,"order":0},{"name":"Tawny Cypress","character":"Taissa Turner","img":null,"order":1},{"name":"Sophie N\u00e9lisse","character":"Teen Shauna Shipman","img":null,"order":3},{"name":"Jasmin Savoy Brown","character":"Teen Taissa Turner","img":null,"order":4},{"name":"Sophie Thatcher","character":"Teen Natalie Scatorccio","img":null,"order":5},{"name":"Sammi Hanratty","character":"Teen Misty Quigley","img":null,"order":6},{"name":"Steven Krueger","character":"Ben Scott","img":null,"order":7},{"name":"Warren Kole","character":"Jeff Sadecki","img":null,"order":8},{"name":"Courtney Eaton","character":"Teen Lottie Matthews","img":null,"order":9},{"name":"Lauren Ambrose","character":"Van Palmer","img":null,"order":10},{"name":"Christina Ricci","character":"Misty Quigley","img":null,"order":11},{"name":"Kevin Alves","character":"Teen Travis","img":null,"order":14},{"name":"Liv Hewson","character":"Teen Van Palmer","img":null,"order":15},{"name":"Sarah Desjardins","character":"Callie Sadecki","img":null,"order":16},{"name":"Alexa Barajas","character":"Teen Mari Ibarra","img":null,"order":18}],"movie:21469":[{"name":"Mike Vogel","character":"Julian","img":null,"order":0},{"name":"Brittany Murphy","character":"June","img":null,"order":1},{"name":"Danny Pino","character":"Terry","img":null,"order":2},{"name":"Brad Greenquist","character":"The Porter","img":null,"order":3},{"name":"Arie Verveen","character":"Lucas","img":null,"order":4},{"name":"Natalie Smyka","character":"Anna","img":null,"order":5},{"name":"Guillermo D\u00edaz","character":"The Cook","img":null,"order":6},{"name":"Dov Davidoff","character":"The Bellhop","img":null,"order":7},{"name":"William Stanford Davis","character":"Custodian","img":null,"order":8},{"name":"Erika Ringor","character":"Detective","img":null,"order":9},{"name":"Mark Engelhardt","character":"Officer","img":null,"order":10},{"name":"Chase Kim","character":"Police Officer","img":null,"order":11},{"name":"Erika Seifred","character":"Dominatrix","img":null,"order":12},{"name":"Cameron Cash","character":"Georgie (uncredited)","img":null,"order":13}],"movie:235271":[{"name":"Sharon Leal","character":"Zoe Reynard","img":null,"order":0},{"name":"Boris Kodjoe","character":"Jason Reynard","img":null,"order":1},{"name":"John Newberg","character":"Balthazar Crayne","img":null,"order":2},{"name":"Brandon Gonzales","character":"Tony","img":null,"order":3},{"name":"Cameron Mills","character":"10-year-old Zoe","img":null,"order":4},{"name":"Emayatzy Corinealdi","character":"Brina","img":null,"order":5},{"name":"Garrett Hines","character":"Benny","img":null,"order":6},{"name":"Hunter Burke","character":"Shane","img":null,"order":7},{"name":"Kat Graham","character":"Diamond","img":null,"order":8},{"name":"Maria Howell","character":"Nina","img":null,"order":9},{"name":"Omer Mughal","character":"Handsome Man","img":null,"order":10},{"name":"Tasha Smith","character":"Marcella","img":null,"order":11},{"name":"Tyson Beckford","character":"Corey","img":null,"order":12},{"name":"William Levy","character":"Quentin Matthews","img":null,"order":13},{"name":"Mikayla Donlon","character":"Girl on Playground","img":null,"order":14}],"movie:376004":[{"name":"Sean Faris","character":"Samuel Deuprey","img":null,"order":0},{"name":"Danielle Savre","character":"Ashley","img":null,"order":1},{"name":"Mehcad Brooks","character":"Damien","img":null,"order":2},{"name":"Stephanie Charles","character":"Lola","img":null,"order":3},{"name":"Steffinnie Phrommany","character":"Jasmine","img":null,"order":4},{"name":"Rebecca Reaney","character":"Rebecca Scott","img":null,"order":5},{"name":"Phillip Brock","character":"Jimmy","img":null,"order":6},{"name":"Robert Eric Wise","character":"Tyrone","img":null,"order":7}],"movie:18206":[{"name":"Colin Hanks","character":"Doug","img":null,"order":0},{"name":"Ana Claudia Talanc\u00f3n","character":"Amy","img":null,"order":1},{"name":"Jordana Spiro","character":"Jen","img":null,"order":2},{"name":"Jonathon Trent","character":"Matt","img":null,"order":3},{"name":"Alex Boling","character":"Barrista","img":null,"order":4},{"name":"Tony Armatrading","character":"Cop","img":null,"order":5}],"movie:13483":[{"name":"Hayden Christensen","character":"Clay Beresford","img":null,"order":0},{"name":"Jessica Alba","character":"Sam Lockwood","img":null,"order":1},{"name":"Terrence Howard","character":"Dr. Jack Harper","img":null,"order":2},{"name":"Lena Olin","character":"Lilith Beresford","img":null,"order":3},{"name":"Christopher McDonald","character":"Dr. Larry Lupin","img":null,"order":4},{"name":"Sam Robards","character":"Clayton Beresford Sr.","img":null,"order":5},{"name":"Arliss Howard","character":"Dr. Jonathan Neyer","img":null,"order":6},{"name":"Fisher Stevens","character":"Dr. Puttnam","img":null,"order":7},{"name":"Georgina Chapman","character":"Penny Carver","img":null,"order":8},{"name":"David Harbour","character":"Dracula","img":null,"order":9},{"name":"Steven Hinkle","character":"Young Clay","img":null,"order":10},{"name":"Denis O'Hare","character":"Financial News Analyst","img":null,"order":11},{"name":"Charlie Hewson","character":"Brian the Orderly","img":null,"order":12},{"name":"Court Young","character":"Officer Doherty","img":null,"order":13},{"name":"Poorna Jagannathan","character":"Dr. Neyer's Nurse","img":null,"order":14}],"movie:624788":[{"name":"Aubrey Plaza","character":"Allison","img":null,"order":0},{"name":"Christopher Abbott","character":"Gabe","img":null,"order":1},{"name":"Sarah Gadon","character":"Blair","img":null,"order":2},{"name":"Paola L\u00e1zaro","character":"Cahya","img":null,"order":3},{"name":"Grantham Coleman","character":"Baako","img":null,"order":4},{"name":"Lindsay Burdge","character":"Maude","img":null,"order":5},{"name":"Lou Gonzalez","character":"Chris","img":null,"order":6},{"name":"Shannon O'Neill","character":"Simone","img":null,"order":7},{"name":"Alexander Koch","character":"Mike","img":null,"order":8},{"name":"Jennifer Kim","character":"Nora","img":null,"order":9},{"name":"Kevin Barker","character":"John the Gaffer","img":null,"order":10},{"name":"Mary Borrello","character":"1st AC","img":null,"order":11},{"name":"Rob Turbovsky","character":"2nd AC","img":null,"order":12},{"name":"Haitao Zeng","character":"PA","img":null,"order":13}],"movie:13250":[{"name":"Pierce Brosnan","character":"Tom Ryan","img":null,"order":0},{"name":"Maria Bello","character":"Abby Randall","img":null,"order":1},{"name":"Gerard Butler","character":"Neil Randall","img":null,"order":2},{"name":"Emma Karwandy","character":"Sophie Randall","img":null,"order":3},{"name":"Claudette Mink","character":"Judy Ryan","img":null,"order":4},{"name":"Desiree Zurowski","character":"Helen Schriver","img":null,"order":5},{"name":"Nicholas Lea","character":"Jerry Crane","img":null,"order":6},{"name":"Peter Keleghan","character":"Karl Granger","img":null,"order":7},{"name":"Samantha Ferris","character":"Diane","img":null,"order":8},{"name":"Malcolm Stewart","character":"Dave Carver","img":null,"order":9},{"name":"Callum Keith Rennie","character":"Det. McGill","img":null,"order":10},{"name":"Dustin Milligan","character":"Matt Ryan","img":null,"order":11},{"name":"Julie Patzwald","character":"Female Teller","img":null,"order":12},{"name":"Jason McKinnon","character":"French Waiter","img":null,"order":13},{"name":"Kurt Evans","character":"Maitre D'","img":null,"order":14}],"movie:317744":[{"name":"Nick Jonas","character":"Doug Martin","img":null,"order":0},{"name":"Isabel Lucas","character":"Lena Harper","img":null,"order":1},{"name":"Dermot Mulroney","character":"Eliott Harper","img":null,"order":2},{"name":"Kandyse McClure","character":"Angie Alvarez","img":null,"order":3},{"name":"Graham Rogers","character":"Carson","img":null,"order":4},{"name":"Marc Macaulay","character":"Gordon","img":null,"order":5},{"name":"David Sherrill","character":"Brian Martin","img":null,"order":6},{"name":"Kevin J. O'Connor","character":"Pier/Patron du bar","img":null,"order":7},{"name":"Paul Sorvino","character":"Sheriff Big Jack","img":null,"order":8},{"name":"Alex ter Avest","character":"Emma Shalloway","img":null,"order":9},{"name":"John Driskell Hopkins","character":"Gus","img":null,"order":10},{"name":"Ross Gallo","character":"Lena's Lawyer","img":null,"order":11},{"name":"Kiki Harris","character":"Emily/Doug's mother","img":null,"order":12}],"movie:1920":[{"name":"Barbara Stanwyck","character":"Kathy Ferguson","img":null,"order":0},{"name":"Sterling Hayden","character":"Lt. William Doyle","img":null,"order":1},{"name":"Raymond Burr","character":"Inspector Tony Pope","img":null,"order":2},{"name":"Fay Wray","character":"Alice Pope","img":null,"order":3},{"name":"Virginia Grey","character":"Sara Alidas","img":null,"order":4},{"name":"Royal Dano","character":"Capt. Charlie Alidas","img":null,"order":5},{"name":"Robert Griffin","character":"Police Sgt. James","img":null,"order":6},{"name":"Dennis Cross","character":"Police Sgt. Jules","img":null,"order":7},{"name":"Jay Adler","character":"Mr. Nalence","img":null,"order":8},{"name":"Stuart Whitman","character":"Laboratory Technician","img":null,"order":9},{"name":"Malcolm Atterbury","character":"Police Officer Spitz","img":null,"order":10},{"name":"Robert Quarry","character":"Sam, Reporter","img":null,"order":11},{"name":"Gail Bonney","character":"Mrs. London","img":null,"order":12},{"name":"Joe Conley","character":"Delivery Boy","img":null,"order":13},{"name":"Ralph Brooks","character":"Reporter in Newspaper Office (Uncredited)","img":null,"order":14}],"movie:619979":[{"name":"Ben Affleck","character":"Vic Van Allen","img":null,"order":0},{"name":"Ana de Armas","character":"Melinda Van Allen","img":null,"order":1},{"name":"Tracy Letts","character":"Don Wilson","img":null,"order":2},{"name":"Lil Rel Howery","character":"Grant","img":null,"order":3},{"name":"Dash Mihok","character":"Jonas Fernandez","img":null,"order":4},{"name":"Finn Wittrock","character":"Tony Cameron","img":null,"order":5},{"name":"Kristen Connolly","character":"Kelly Wilson","img":null,"order":6},{"name":"Jacob Elordi","character":"Charlie De Lisle","img":null,"order":7},{"name":"Rachel Blanchard","character":"Kristin Peterson","img":null,"order":8},{"name":"Michael Braun","character":"Jeff Peterson","img":null,"order":9},{"name":"Jade Fernandez","character":"Jen Fernandez","img":null,"order":10},{"name":"Grace Jenkins","character":"Trixie","img":null,"order":11},{"name":"Brendan Miller","character":"Joel Dash","img":null,"order":12},{"name":"Devyn A. Tyler","character":"Mary Washington","img":null,"order":13},{"name":"Jeff Pope","character":"Chief Nichols","img":null,"order":14}],"movie:8999":[{"name":"Clive Owen","character":"Charles Schine","img":null,"order":0},{"name":"Jennifer Aniston","character":"Lucinda Harris","img":null,"order":1},{"name":"Vincent Cassel","character":"LaRoche","img":null,"order":2},{"name":"Melissa George","character":"Deanna Schine","img":null,"order":3},{"name":"Giancarlo Esposito","character":"Detective Church","img":null,"order":4},{"name":"David Morrissey","character":"Sam Griffin","img":null,"order":5},{"name":"Georgina Chapman","character":"Candy","img":null,"order":6},{"name":"Denis O'Hare","character":"Jerry","img":null,"order":7},{"name":"Tom Conti","character":"Eliot Firth","img":null,"order":8},{"name":"Addison Timlin","character":"Amy Schine","img":null,"order":9},{"name":"RZA","character":"Winston Boyko","img":null,"order":10},{"name":"Xzibit","character":"Dexter","img":null,"order":11},{"name":"Sandra Bee","character":"Train Conductor","img":null,"order":12},{"name":"William Armstrong","character":"Accountant","img":null,"order":13},{"name":"Rachael Blake","character":"Susan Davis","img":null,"order":14}],"movie:1033148":[{"name":"Jennifer Lawrence","character":"Grace","img":null,"order":0},{"name":"Robert Pattinson","character":"Jackson","img":null,"order":1},{"name":"Sissy Spacek","character":"Pam","img":null,"order":2},{"name":"LaKeith Stanfield","character":"Karl","img":null,"order":3},{"name":"Nick Nolte","character":"Harry","img":null,"order":4},{"name":"Gabrielle Rose","character":"Jen","img":null,"order":5},{"name":"Clare Coulter","character":"Courtney","img":null,"order":6},{"name":"Sarah Lind","character":"Cheryl","img":null,"order":7},{"name":"Luke Camilleri","character":"Greg","img":null,"order":8},{"name":"Victor Zinck Jr.","character":"Tom","img":null,"order":9},{"name":"Debs Howard","character":"Marsha","img":null,"order":10},{"name":"Phillip Forest Lewitski","character":"Charlie","img":null,"order":11},{"name":"Georgina Lightning","character":"Kathleen the Orderly","img":null,"order":12},{"name":"Darren Moore","character":"Gerry","img":null,"order":13},{"name":"Lauren Viau","character":"Bessie","img":null,"order":14}],"movie:69668":[{"name":"Daniel Craig","character":"Will Atenton","img":null,"order":0},{"name":"Naomi Watts","character":"Ann Patterson","img":null,"order":1},{"name":"Rachel Weisz","character":"Libby Atenton","img":null,"order":2},{"name":"Marton Csokas","character":"Jack Patterson","img":null,"order":3},{"name":"Elias Koteas","character":"Boyce","img":null,"order":4},{"name":"Taylor Geare","character":"Trish","img":null,"order":5},{"name":"Claire Geare","character":"Dee Dee","img":null,"order":6},{"name":"Rachel Fox","character":"Chloe Patterson","img":null,"order":7},{"name":"Jane Alexander","character":"Dr. Greeley","img":null,"order":8},{"name":"Brian Murray","character":"Dr. Medlin","img":null,"order":9},{"name":"Bernadette Quigley","character":"Heather Keeler","img":null,"order":10},{"name":"Sarah Gadon","character":"Cindi","img":null,"order":11},{"name":"Gregory Smith","character":"Artie","img":null,"order":12},{"name":"David Huband","character":"Officer Nelson","img":null,"order":13},{"name":"Martin Roach","character":"Tommy","img":null,"order":14}],"movie:18190":[{"name":"James Spader","character":"Ray Reardon","img":null,"order":0},{"name":"M\u00e4dchen Amick","character":"Lena Mathers","img":null,"order":1},{"name":"Fredric Lehne","character":"Larry","img":null,"order":2},{"name":"Bess Armstrong","character":"Elaine","img":null,"order":3},{"name":"Larry Miller","character":"Norman","img":null,"order":4},{"name":"Kathleen York","character":"Martha","img":null,"order":5},{"name":"Kate Williamson","character":"Mrs. Sneeder","img":null,"order":6},{"name":"Tom Lillard","character":"Hank Sneeder","img":null,"order":7},{"name":"William Shockley","character":"Buddy","img":null,"order":8},{"name":"Carl Sundstrom","character":"Officer","img":null,"order":9},{"name":"Irwin Keyes","character":"Officer","img":null,"order":10},{"name":"Joel McKinnon Miller","character":"Minister","img":null,"order":11},{"name":"Joseph Scoren","character":"Martha's Lawyer (as Joseph Scorsiani)","img":null,"order":12},{"name":"Archie Lang","character":"Judge #1","img":null,"order":13},{"name":"Clyde Kusatsu","character":"Judge Kurita","img":null,"order":14}],"movie:1249213":[{"name":"Meghann Fahy","character":"Violet","img":null,"order":0},{"name":"Brandon Sklenar","character":"Henry","img":null,"order":1},{"name":"Reed Diamond","character":"Richard","img":null,"order":2},{"name":"Violett Beane","character":"Jen","img":null,"order":3},{"name":"Jeffery Self","character":"Matt","img":null,"order":4},{"name":"Jacob Robinson","character":"Toby","img":null,"order":5},{"name":"Gabrielle Ryan","character":"Cara","img":null,"order":6},{"name":"Ed Weeks","character":"Phil","img":null,"order":7},{"name":"Travis Nelson","character":"Connor","img":null,"order":8},{"name":"Sarah McCormack","character":"Hostess","img":null,"order":9},{"name":"Ben Pelletier","character":"Bearded Man / Masked Man","img":null,"order":10},{"name":"Saoirse Hayden","character":"Green Prom Dress Girl","img":null,"order":11},{"name":"Fiona Browne","character":"Diane","img":null,"order":12},{"name":"Stephanie Karam","character":"Young Woman (Zoom)","img":null,"order":13},{"name":"Michael Shea","character":"Blake","img":null,"order":14}],"movie:2252":[{"name":"Viggo Mortensen","character":"Nikolai Luzhin","img":null,"order":0},{"name":"Naomi Watts","character":"Anna Ivanovna Khitrova","img":null,"order":1},{"name":"Vincent Cassel","character":"Kirill","img":null,"order":2},{"name":"Armin Mueller-Stahl","character":"Semyon","img":null,"order":3},{"name":"Sin\u00e9ad Cusack","character":"Helen","img":null,"order":4},{"name":"Donald Sumpter","character":"Yuri","img":null,"order":5},{"name":"Jerzy Skolimowski","character":"Stepan","img":null,"order":6},{"name":"Josef Altin","character":"Ekrem","img":null,"order":7},{"name":"Aleksandar Mikic","character":"Soyka","img":null,"order":8},{"name":"Sarah-Jeanne Labrosse","character":"Tatiana","img":null,"order":9},{"name":"Lalita Ahmed","character":"Customer","img":null,"order":10},{"name":"Tatiana Maslany","character":"Tatiana (voice)","img":null,"order":11},{"name":"Tereza Srbova","character":"Kirilenko","img":null,"order":12},{"name":"David Papava","character":"Chechen","img":null,"order":13},{"name":"Tamer Hassan","character":"Chechen","img":null,"order":14}],"movie:753":[{"name":"John Marley","character":"Richard Forst","img":null,"order":0},{"name":"Gena Rowlands","character":"Jeannie Rapp","img":null,"order":1},{"name":"Lynn Carlin","character":"Maria Forst","img":null,"order":2},{"name":"Fred Draper","character":"Freddie Draper","img":null,"order":3},{"name":"Seymour Cassel","character":"Chet","img":null,"order":4},{"name":"Val Avery","character":"Jim McCarthy","img":null,"order":5},{"name":"Dorothy Gulliver","character":"Florence","img":null,"order":6},{"name":"Joanne Moore Jordan","character":"Louise Draper","img":null,"order":7},{"name":"Darlene Conley","character":"Billy Mae","img":null,"order":8},{"name":"Gene Darfler","character":"Joe Jackson","img":null,"order":9},{"name":"Elizabeth Deering","character":"Stella","img":null,"order":10},{"name":"George Dunn","character":"Comedian (as George Dunne)","img":null,"order":11},{"name":"Laurie Mock","character":"Barmaid","img":null,"order":12},{"name":"Christina Crawford","character":"Woman Scattering Coins on Bar","img":null,"order":13},{"name":"George Sims","character":"Bartender","img":null,"order":14}],"movie:549514":[{"name":"Hilary Swank","character":"Det. Valerie Quinlan","img":null,"order":0},{"name":"Michael Ealy","character":"Derrick Tyler","img":null,"order":1},{"name":"Mike Colter","character":"Rafe Grimes","img":null,"order":2},{"name":"Damaris Lewis","character":"Tracie Tyler","img":null,"order":3},{"name":"Tyrin Turner","character":"Tyrin Abenathy","img":null,"order":4},{"name":"Danny Pino","character":"Carter Heywood","img":null,"order":5},{"name":"Geoffrey Owens","character":"Bill Cranepool","img":null,"order":6},{"name":"David Hoflin","character":"Officer Lowe","img":null,"order":7},{"name":"Sam Daly","character":"Officer Stallman","img":null,"order":8},{"name":"Lance Stephenson","character":"Sports Star Cameo","img":null,"order":9},{"name":"Chic Daniel","character":"Investigator","img":null,"order":10},{"name":"Stephen O'Mahoney","character":"Uniformed Police Officer","img":null,"order":11},{"name":"Lexa Gluck","character":"Carter's Trophy Wife","img":null,"order":12},{"name":"Oakley Bull","character":"Haley Older / Haley Younger","img":null,"order":13},{"name":"Ian Stanley","character":"First Officer","img":null,"order":14}],"tv:241372":[{"name":"Dove Cameron","character":"Ciara Wyse","img":null,"order":0},{"name":"Avan Jogia","character":"Oliver Kennedy","img":null,"order":1},{"name":"Dorian Missick","character":"Karl Connelly","img":null,"order":2},{"name":"Karla Souza","character":"Lee Reardon","img":null,"order":3},{"name":"Kingston Rumi Southwick","character":"Young Oliver","img":null,"order":4},{"name":"Patch Darragh","character":"Dan Troxler","img":null,"order":5},{"name":"Kira Guloien","character":"Jane Miller","img":null,"order":6},{"name":"Megan Peta Hill","character":"Shyla","img":null,"order":7}],"tv:4493":[{"name":"Jennifer Saunders","character":"Edina Monsoon","img":null,"order":0},{"name":"Joanna Lumley","character":"Patsy Stone","img":null,"order":1},{"name":"Julia Sawalha","character":"Saffron 'Saffy' Monsoon","img":null,"order":2},{"name":"June Whitfield","character":"Mother","img":null,"order":3},{"name":"Jane Horrocks","character":"Bubble / Katy Grin","img":null,"order":118},{"name":"Naoko Mori","character":"Sarah","img":null,"order":505},{"name":"Christopher Malcolm","character":"Justin","img":null,"order":509},{"name":"Kathy Burke","character":"Magda","img":null,"order":510},{"name":"Harriet Thorpe","character":"Fleur","img":null,"order":512},{"name":"Christopher Ryan","character":"Marshall","img":null,"order":513},{"name":"Mo Gaffney","character":"Bo","img":null,"order":514},{"name":"Lulu","character":"Self","img":null,"order":514},{"name":"Helen Lederer","character":"Catriona","img":null,"order":518},{"name":"Tilly Blackwood","character":"Lady Candida De Denison-Bender","img":null,"order":624},{"name":"Llewella Gideon","character":"Nurse","img":null,"order":641}],"tv:106454":[{"name":"Tiffany Haddish","character":"Detective Danner","img":null,"order":0},{"name":"Sam Richardson","character":"Aniq","img":null,"order":1},{"name":"Zo\u00eb Chao","character":"Zoe","img":null,"order":2},{"name":"Ben Schwartz","character":"Yasper","img":null,"order":3},{"name":"Jamie Demetriou","character":"Walt","img":null,"order":7},{"name":"John Early","character":"Detective Culp","img":null,"order":8},{"name":"John Cho","character":"Ulysses","img":null,"order":10},{"name":"Paul Walter Hauser","character":"Travis","img":null,"order":11},{"name":"Ken Jeong","character":"Feng","img":null,"order":12},{"name":"Anna Konkle","character":"Hannah","img":null,"order":13},{"name":"Poppy Liu","character":"Grace","img":null,"order":14},{"name":"Elizabeth Perkins","character":"Isabel","img":null,"order":15},{"name":"Jack Whitehall","character":"Sebastian","img":null,"order":16},{"name":"Zach Woods","character":"Edgar","img":null,"order":17},{"name":"Vivian Wu","character":"Vivian","img":null,"order":18}],"tv:6127":[{"name":"Julia McKenzie","character":"Miss Marple","img":null,"order":0},{"name":"Geraldine McEwan","character":"Miss Marple","img":null,"order":1},{"name":"Emilia Fox","character":"Joanna Burton","img":null,"order":2},{"name":"Sylvia Syms","character":"Lavinia Pinkerton","img":null,"order":500},{"name":"Russell Tovey","character":"PC Terence Reed","img":null,"order":501},{"name":"Hugo Speer","character":"James Abbot","img":null,"order":502},{"name":"Shirley Henderson","character":"Honoria Waynflete","img":null,"order":503},{"name":"Margo Stilley","character":"Bridget Conway","img":null,"order":504},{"name":"Steve Pemberton","character":"Henry Wake","img":null,"order":505},{"name":"Benedict Cumberbatch","character":"Luke Fitzwilliam","img":null,"order":506},{"name":"Anna Chancellor","character":"Lydia Horton","img":null,"order":507},{"name":"Jemma Redgrave","character":"Jessie Humbleby","img":null,"order":508},{"name":"Lyndsey Marshal","character":"Amy Gibbs","img":null,"order":509},{"name":"David Haig","character":"Major Hugh Horton","img":null,"order":510},{"name":"Joanna Lumley","character":"Dolly Bantry","img":null,"order":679}],"tv:153723":[{"name":"Geffri Maya","character":"Simone Hicks","img":null,"order":0},{"name":"Peyton Alex Smith","character":"Damon Sims","img":null,"order":1},{"name":"Kelly Jenrette","character":"Amara Patterson","img":null,"order":2},{"name":"Cory Hardrict","character":"Marcus Turner","img":null,"order":3},{"name":"Sylvester Powell","character":"Jessie 'JR' Raymond","img":null,"order":4},{"name":"Camille Hyde","character":"Thea Mays","img":null,"order":5},{"name":"Mitchell Edwards","character":"Cam Watkins","img":null,"order":6},{"name":"Netta Walker","character":"Keisha McCalla","img":null,"order":7},{"name":"Rhoyle Ivy King","character":"Nathaniel Hardin","img":null,"order":8},{"name":"Leonard Roberts","character":"Zeke Allen","img":null,"order":505},{"name":"Derek Rivera","character":"Santiago Reyes","img":null,"order":510},{"name":"Joe Holt","character":"Jesse","img":null,"order":529},{"name":"Heather Lynn Harris","character":"Gabrielle","img":null,"order":554},{"name":"Martin Bobb-Semple","character":"Orlando 'Lando' Johnson","img":null,"order":555},{"name":"Taylor Polidore","character":"Tootie","img":null,"order":579}],"tv:116450":[{"name":"Sarah Jessica Parker","character":"Carrie Bradshaw","img":null,"order":0},{"name":"Cynthia Nixon","character":"Miranda Hobbes","img":null,"order":1},{"name":"Kristin Davis","character":"Charlotte York","img":null,"order":2},{"name":"Sara Ramirez","character":"Che Diaz","img":null,"order":3},{"name":"Cathy Ang","character":"Lily Goldenblatt","img":null,"order":4},{"name":"Mario Cantone","character":"Anthony Marentino","img":null,"order":5},{"name":"Sarita Choudhury","character":"Seema Patel","img":null,"order":6},{"name":"Evan Handler","character":"Harry Goldenblatt","img":"yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg","order":7},{"name":"Christopher Jackson","character":"Herbert Wexley","img":null,"order":8},{"name":"Nicole Ari Parker","character":"Lisa Todd Wexley","img":null,"order":9},{"name":"Alexa Swinton","character":"Rose 'Rock' Goldenblatt","img":null,"order":10},{"name":"Karen Pittman","character":"Dr. Nya Wallace","img":null,"order":11},{"name":"Niall Cunningham","character":"Brady Hobbes","img":null,"order":12},{"name":"Alexander Bello","character":"Henry Wexley","img":null,"order":515},{"name":"Sebastiano Pigazzi","character":"Giuseppe","img":null,"order":620}],"tv:70785":[{"name":"Amybeth McNulty","character":"Anne Shirley","img":null,"order":0},{"name":"Geraldine James","character":"Marilla Cuthbert","img":null,"order":1},{"name":"R.H. Thomson","character":"Matthew Cuthbert","img":null,"order":2},{"name":"Dalila Bela","character":"Diana Barry","img":null,"order":3},{"name":"Lucas Jade Zumann","character":"Gilbert Blythe","img":null,"order":4},{"name":"Corrine Koslo","character":"Rachel Lynde","img":null,"order":5},{"name":"Jonathan Holmes","character":"Mr. William Barry","img":null,"order":539},{"name":"Helen Johns","character":"Mrs. Eliza Barry","img":null,"order":543},{"name":"Lia Pappas-Kemps","character":"Jane Andrews","img":null,"order":567},{"name":"Kyla Matthews","character":"Ruby Gillis","img":null,"order":569},{"name":"Miranda McKeon","character":"Josie Pye","img":null,"order":570},{"name":"Jacob Horsley","character":"Charlie Sloane","img":null,"order":572},{"name":"Jacob Ursomarzo","character":"Moody Spurgeon","img":null,"order":573},{"name":"Dalmar Abuzeid","character":"Sebastian 'Bash' Lacroix","img":null,"order":574},{"name":"Aymeric Jett Montaz","character":"Jerry Baynard","img":null,"order":579}],"tv:39859":[{"name":"Ashley Rickards","character":"Jenna Hamilton","img":null,"order":0},{"name":"Beau Mirchoff","character":"Matty McKibben","img":null,"order":1},{"name":"Nikki DeLoach","character":"Lacey Hamilton","img":null,"order":2},{"name":"Jillian Rose Reed","character":"Tamara Kaplan","img":null,"order":4},{"name":"Brett Davern","character":"Jake Rosati","img":null,"order":5},{"name":"Molly Tarlov","character":"Sadie Saxton","img":null,"order":6},{"name":"Desi Lydic","character":"Valerie Marks","img":null,"order":7},{"name":"Jessica Lu","character":"Ming Huang","img":null,"order":8},{"name":"Greer Grammer","character":"Lissa Miller","img":null,"order":9},{"name":"Meaghan Jette Martin","character":"Julie #2","img":null,"order":10},{"name":"Wesam Keesh","character":"Kyle Cohen","img":null,"order":522},{"name":"Barret Swatek","character":"Ally Saxton","img":null,"order":617},{"name":"Monty Geer","character":"Cole","img":null,"order":642},{"name":"Evan Crooks","character":"Theo","img":null,"order":643},{"name":"Niko Pepaj","character":"Sergio","img":null,"order":694}],"tv:100412":[{"name":"Sophie Grace","character":"Kristy Thomas","img":null,"order":0},{"name":"Momona Tamada","character":"Claudia Kishi","img":null,"order":1},{"name":"Shay Rudolph","character":"Stacey McGill","img":null,"order":2},{"name":"Malia Baker","character":"Mary Anne Spier","img":null,"order":3},{"name":"Alicia Silverstone","character":"Elizabeth Thomas-Brewer","img":null,"order":500},{"name":"Mark Feuerstein","character":"Watson Brewer","img":null,"order":501},{"name":"Takayo Fischer","character":"Mimi Yamamoto","img":null,"order":502},{"name":"Benjamin Goas","character":"David Michael Thomas","img":null,"order":505},{"name":"Sophia Reid-Gantzert","character":"Karen Brewer","img":null,"order":509},{"name":"Marc Evan Jackson","character":"Richard Spier","img":null,"order":515},{"name":"Xochitl Gomez","character":"Dawn Schafer","img":null,"order":536},{"name":"Jessica Elaina Eason","character":"Sharon Porter","img":null,"order":537},{"name":"Vivian Watson","character":"Mallory Pike","img":null,"order":562},{"name":"Anais Lee","character":"Jessi Ramsey","img":null,"order":577},{"name":"Kyndra Sanchez","character":"Dawn Schafer","img":null,"order":587}],"tv:62704":[{"name":"Dwayne Johnson","character":"Spencer Strassmore","img":null,"order":0},{"name":"Rob Corddry","character":"Joe Krutel","img":null,"order":1},{"name":"John David Washington","character":"Ricky Jerret","img":null,"order":2},{"name":"Omar Benson Miller","character":"Charles Greane","img":null,"order":3},{"name":"Troy Garity","character":"Jason Antolotti","img":null,"order":4},{"name":"London Brown","character":"Reggie","img":null,"order":5},{"name":"Donovan W. Carter","character":"Vernon Littlefield","img":null,"order":6},{"name":"Arielle Kebbel","character":"Tracy Legette","img":null,"order":7},{"name":"Jazmyn Simon","character":"Julie Greane","img":null,"order":9},{"name":"Brittany S. Hall","character":"Amber","img":null,"order":10},{"name":"Dul\u00e9 Hill","character":"Larry Siefert","img":null,"order":501},{"name":"Richard Schiff","character":"Brett Anderson","img":null,"order":503},{"name":"Jay Glazer","character":"Jay Glazer","img":null,"order":647},{"name":"Carl McDowell","character":"TTD","img":null,"order":673},{"name":"Robert Wisdom","character":"Dennis","img":null,"order":674}],"tv:3216":[{"name":"Robert Blake","character":"Det. Tony Baretta","img":null,"order":0},{"name":"Dana Elcar","character":"Inspector Shiller","img":null,"order":1},{"name":"Sharon Cintron","character":"","img":null,"order":2},{"name":"Burt Young","character":"Solomon","img":null,"order":368},{"name":"Michael D. Roberts","character":"","img":null,"order":500},{"name":"Val Avery","character":"","img":null,"order":501},{"name":"Joshua Shelley","character":"","img":null,"order":502},{"name":"Timothy Carey","character":"","img":null,"order":554},{"name":"Sharon Cintron","character":"","img":null,"order":565},{"name":"Art Metrano","character":"","img":null,"order":649},{"name":"Tom Ewell","character":"","img":null,"order":667},{"name":"Strother Martin","character":"","img":null,"order":678},{"name":"Ron Thompson","character":"","img":null,"order":689},{"name":"Elisha Cook Jr.","character":"","img":null,"order":700},{"name":"Angelo Rossitto","character":"Little Moe (uncredited)","img":null,"order":708}],"tv:197373":[{"name":"Kaley Cuoco","character":"Ava Bartlett","img":null,"order":0},{"name":"Chris Messina","character":"Nathan Bartlett","img":null,"order":1},{"name":"Tom Bateman","character":"Matt Pierce","img":null,"order":2},{"name":"Priscilla Quintana","character":"Ruby Gale","img":null,"order":3},{"name":"Liana Liberato","character":"Tory Thompson","img":null,"order":4},{"name":"Li Jun Li","character":"Michelle","img":null,"order":502},{"name":"Annabelle Dexter-Jones","character":"Serena","img":null,"order":503},{"name":"Aisha Alfa","character":"Carolyn","img":null,"order":504},{"name":"Alex Alomar Akpobome","character":"Ryan","img":null,"order":505},{"name":"June Diane Raphael","character":"Romy Lipinski","img":null,"order":506},{"name":"Brandon Keener","character":"Paul","img":null,"order":507},{"name":"Miles Mussenden","character":"Detective Quincy Burrell","img":null,"order":508},{"name":"Jessica St. Clair","character":"Rochelle Lipinski","img":null,"order":519},{"name":"Sebastian Quinn","character":"Carlos","img":null,"order":521},{"name":"Melissa Fumero","character":"Drew","img":null,"order":559}],"tv:250504":[{"name":"Claire Danes","character":"Agatha 'Aggie' Wiggs","img":null,"order":0},{"name":"Matthew Rhys","character":"Nile Jarvis","img":null,"order":1},{"name":"Brittany Snow","character":"Nina Jarvis","img":null,"order":2},{"name":"Natalie Morales","character":"Shelley Morris","img":null,"order":3},{"name":"Jonathan Banks","character":"Martin Jarvis","img":null,"order":500},{"name":"Tim Guinee","character":"Rick 'Wrecking Ball' Jarvis","img":null,"order":501},{"name":"David Lyons","character":"Brian Abbott","img":null,"order":502},{"name":"Bubba Weiler","character":"Teddy Fenig","img":null,"order":503},{"name":"Deirdre O'Connell","character":"Carol McGiddish","img":null,"order":504},{"name":"Leonard Gerome","character":"Cooper Morris-Wiggs","img":null,"order":508},{"name":"Hettienne Park","character":"Erika Breton","img":null,"order":514},{"name":"Aleyse Shannon","character":"Olivia Benitez","img":null,"order":515},{"name":"Julie Ann Emery","character":"Lila Jarvis","img":null,"order":519},{"name":"Amir Arison","character":"Frank Breton","img":null,"order":520},{"name":"Peter Grosz","character":"Elijah Garrity","img":null,"order":526}],"tv:80587":[{"name":"Chris Houghton","character":"Cricket Green (voice)","img":null,"order":0},{"name":"Marieve Herington","character":"Tilly Green (voice)","img":null,"order":1},{"name":"Bob Joles","character":"Bill Green (voice)","img":null,"order":2},{"name":"Artemis Pebdani","character":"Alice 'Gramma' Green (voice)","img":null,"order":3},{"name":"Zeno Robinson","character":"Remy (voice)","img":null,"order":4},{"name":"Wendi McLendon-Covey","character":"Nancy Green (voice)","img":null,"order":5},{"name":"Raven-Symon\u00e9","character":"Maria Media (voice)","img":null,"order":503},{"name":"Anna Akana","character":"Gloria (voice)","img":null,"order":512},{"name":"Danny Trejo","character":"Vasquez (voice)","img":null,"order":513},{"name":"Andy Daly","character":"Officer Keys (voice)","img":null,"order":514},{"name":"Paul Scheer","character":"Chip Whistler (voice)","img":null,"order":516},{"name":"Vladimir Caama\u00f1o","character":"Juan Pablo (voice)","img":null,"order":517},{"name":"Jenna Ortega","character":"Gabriella Espinosa (voice)","img":null,"order":521},{"name":"Paul F. Tompkins","character":"Tooth Fairy (voice)","img":null,"order":523},{"name":"Shane Houghton","character":"Driver Construction Worker (voice)","img":null,"order":541}],"tv:69630":[{"name":"Katie Stevens","character":"Jane Sloan","img":null,"order":0},{"name":"Aisha Dee","character":"Kat Edison","img":null,"order":1},{"name":"Meghann Fahy","character":"Sutton Brady","img":null,"order":2},{"name":"Melora Hardin","character":"Jacqueline Carlyle","img":null,"order":3},{"name":"Stephen Conrad Moore","character":"Oliver Grayson","img":null,"order":4},{"name":"Matt Ward","character":"Alex Crawford","img":null,"order":5},{"name":"Sam Page","character":"Richard 'Dick' Mott","img":null,"order":6},{"name":"Nikohl Boosheri","character":"Aden 'Adena' El Amin","img":null,"order":7},{"name":"Luca James Lee","character":"Ben Chau","img":null,"order":625},{"name":"Alexis Floyd","character":"Tia Clayton","img":null,"order":699},{"name":"Peter Vack","character":"Patrick Duchand","img":null,"order":711},{"name":"Adam Capriolo","character":"Andrew Morrison","img":null,"order":856},{"name":"Stephanie Costa","character":"Sage Aiello","img":null,"order":857},{"name":"Dan Jeannotte","character":"Ryan Decker","img":null,"order":859},{"name":"Gildart Jackson","character":"Ian Carlyle","img":null,"order":860}],"tv:92621":[{"name":"Joseph Gilgun","character":"Vincent \"Vinnie\" O'Neill","img":null,"order":0},{"name":"Michelle Keegan","character":"Erin Croft","img":null,"order":1},{"name":"Steve Evets","character":"Jim","img":null,"order":2},{"name":"Damien Molony","character":"Dylan Golding","img":null,"order":3},{"name":"Bronagh Gallagher","character":"Carol Dennings","img":null,"order":4},{"name":"Tom Hanson","character":"Leslie \"Cardi\" Titt","img":null,"order":5},{"name":"Aaron Heffernan","character":"Ashley \"Ash\" Dennings","img":null,"order":6},{"name":"Joanna Higson","character":"Mary \"Sugar\" Jones","img":null,"order":7},{"name":"Parth Thakerar","character":"Jehan \"JJ\" Jovani","img":null,"order":8},{"name":"Ryan Sampson","character":"Thomas \"Tommo\" Schaffner","img":null,"order":9},{"name":"Dominic West","character":"Dr. Chris Coxley","img":null,"order":10},{"name":"Ramon Tikaram","character":"Terence McCann","img":null,"order":11},{"name":"Tadhg Murphy","character":"Gary Cullen","img":null,"order":14},{"name":"Neil Ashton","character":"David \"Davey\" MacDonagh","img":null,"order":16},{"name":"Jude Riordan","character":"Tyler Croft","img":null,"order":501}],"movie:4982":[{"name":"Denzel Washington","character":"Frank Lucas","img":null,"order":0},{"name":"Russell Crowe","character":"Richie Roberts","img":null,"order":1},{"name":"Josh Brolin","character":"Detective Trupo","img":null,"order":2},{"name":"Chiwetel Ejiofor","character":"Huey Lucas","img":null,"order":3},{"name":"Cuba Gooding Jr.","character":"Nicky Barnes","img":null,"order":4},{"name":"Lymari Nadal","character":"Eva","img":null,"order":5},{"name":"Ted Levine","character":"Lou Toback","img":null,"order":6},{"name":"Armand Assante","character":"Dominic Cattano","img":null,"order":7},{"name":"John Ortiz","character":"Javier J. Rivera","img":null,"order":8},{"name":"John Hawkes","character":"Freddie Spearman","img":null,"order":9},{"name":"RZA","character":"Moses Jones","img":null,"order":10},{"name":"Yul Vazquez","character":"Alfonso Abruzzo","img":null,"order":11},{"name":"Ruby Dee","character":"Mama Lucas","img":null,"order":12},{"name":"Idris Elba","character":"Tango","img":null,"order":13},{"name":"Carla Gugino","character":"Laurie Roberts","img":null,"order":14}],"movie:703":[{"name":"Diane Keaton","character":"Annie Hall","img":null,"order":0},{"name":"Woody Allen","character":"Alvy Singer","img":null,"order":1},{"name":"Tony Roberts","character":"Rob","img":null,"order":2},{"name":"Carol Kane","character":"Allison","img":null,"order":3},{"name":"Paul Simon","character":"Tony Lacey","img":null,"order":4},{"name":"Shelley Duvall","character":"Pam","img":null,"order":5},{"name":"Janet Margolin","character":"Robin","img":null,"order":6},{"name":"Colleen Dewhurst","character":"Mrs. Hall","img":null,"order":7},{"name":"Christopher Walken","character":"Duane Hall","img":null,"order":8},{"name":"Donald Symington","character":"Dad Hall","img":null,"order":9},{"name":"Helen Ludlam","character":"Grammy Hall","img":null,"order":10},{"name":"Mordecai Lawner","character":"Alvy's Dad","img":null,"order":11},{"name":"Joan Neuman","character":"Alvy's Mom","img":null,"order":12},{"name":"Jonathan Munk","character":"Alvy - Age 9","img":null,"order":13},{"name":"Ruth Volner","character":"Alvy's Aunt","img":null,"order":14}],"movie:28":[{"name":"Martin Sheen","character":"Captain Benjamin Willard","img":null,"order":0},{"name":"Marlon Brando","character":"Colonel Walter Kurtz","img":null,"order":1},{"name":"Frederic Forrest","character":"Jay 'Chef' Hicks","img":null,"order":2},{"name":"Albert Hall","character":"Chief Phillips","img":null,"order":3},{"name":"Laurence Fishburne","character":"Tyrone 'Clean' Miller","img":null,"order":4},{"name":"Sam Bottoms","character":"Lance B. Johnson","img":null,"order":5},{"name":"Robert Duvall","character":"Lieutenant Colonel Bill Kilgore","img":null,"order":6},{"name":"G.D. Spradlin","character":"General Corman","img":null,"order":7},{"name":"Harrison Ford","character":"Colonel","img":null,"order":8},{"name":"Jerry Ziesmer","character":"Jerry, Civilian","img":null,"order":9},{"name":"Scott Glenn","character":"Lieutenant Richard M. Colby","img":null,"order":10},{"name":"Kerry Rossall","character":"Mike from San Diego","img":null,"order":11},{"name":"James Keane","character":"Kilgore's Gunner","img":null,"order":12},{"name":"Tom Mason","character":"Supply Sergeant","img":null,"order":13},{"name":"Cynthia Wood","character":"Playmate of the Year","img":null,"order":14}],"movie:329865":[{"name":"Amy Adams","character":"Louise Banks","img":null,"order":0},{"name":"Jeremy Renner","character":"Ian Donnelly","img":null,"order":1},{"name":"Forest Whitaker","character":"Colonel Weber","img":null,"order":2},{"name":"Michael Stuhlbarg","character":"Agent Halpern","img":null,"order":3},{"name":"Mark O'Brien","character":"Captain Marks","img":null,"order":4},{"name":"Tzi Ma","character":"General Shang","img":null,"order":5},{"name":"Abigail Pniowsky","character":"8-Year-Old Hannah","img":null,"order":6},{"name":"Julia Scarlett Dan","character":"12-Year-Old Hannah","img":null,"order":7},{"name":"Jadyn Malone","character":"6-Year-Old Hannah","img":null,"order":8},{"name":"Frank Schorpion","character":"Dr. Kettler","img":null,"order":9},{"name":"Lucas Chartier-Dessert","character":"Private Lasky","img":null,"order":10},{"name":"Christian Jadah","character":"Private Combs","img":null,"order":11},{"name":"Lucy Van Oldenbarneveld","character":"CNAC Anchor","img":null,"order":12},{"name":"Andrew Shaver","character":"Environmental Tech","img":null,"order":13},{"name":"Pat Kiely","character":"Environmental Tech","img":null,"order":14}],"movie:980431":[{"name":"Eric Nam","character":"Aang (voice)","img":null,"order":0},{"name":"Dave Bautista","character":"Tagah (voice)","img":null,"order":1},{"name":"Jessica Matten","character":"Katara (voice)","img":null,"order":2},{"name":"Rom\u00e1n Zaragoza","character":"Sokka (voice)","img":null,"order":3},{"name":"Steven Yeun","character":"Zuko (voice)","img":null,"order":4},{"name":"Dionne Quan","character":"Toph (voice)","img":null,"order":5},{"name":"Dee Bradley Baker","character":"Appa / Momo (voice)","img":null,"order":6},{"name":"Freida Pinto","character":"Avatar Sonam (voice)","img":null,"order":7},{"name":"Ke Huy Quan","character":"Avatar Xian (voice)","img":null,"order":8},{"name":"Taika Waititi","character":"Gorillavark (voice)","img":null,"order":9},{"name":"Geraldine Viswanathan","character":"Kallik (voice)","img":null,"order":10},{"name":"Peta Sergeant","character":"Jie (voice)","img":null,"order":11},{"name":"Jonathan Ohye","character":"Yuddah (voice)","img":null,"order":12},{"name":"Vic Chao","character":"Hong (voice)","img":null,"order":13},{"name":"Fran\u00e7ois Chau","character":"Monk Gyatso (voice)","img":null,"order":14}],"movie:424694":[{"name":"Rami Malek","character":"Freddie Mercury","img":null,"order":0},{"name":"Gwilym Lee","character":"Brian May","img":null,"order":1},{"name":"Ben Hardy","character":"Roger Taylor","img":null,"order":2},{"name":"Joseph Mazzello","character":"John Deacon","img":null,"order":3},{"name":"Lucy Boynton","character":"Mary Austin","img":null,"order":4},{"name":"Aidan Gillen","character":"John Reid","img":null,"order":5},{"name":"Allen Leech","character":"Paul Prenter","img":null,"order":6},{"name":"Tom Hollander","character":"Jim Beach","img":null,"order":7},{"name":"Mike Myers","character":"Ray Foster","img":null,"order":8},{"name":"Aaron McCusker","character":"Jim Hutton","img":null,"order":9},{"name":"Meneka Das","character":"Jer Bulsara","img":null,"order":10},{"name":"Ace Bhatti","character":"Bomi Bulsara","img":null,"order":11},{"name":"Priya Blackburn","character":"Kashmira Bulsara","img":null,"order":12},{"name":"Max Bennett","character":"David","img":null,"order":13},{"name":"Dermot Murphy","character":"Bob Geldof","img":null,"order":14}],"movie:508883":[{"name":"Soma Santoki","character":"Mahito Maki (voice)","img":null,"order":0},{"name":"Masaki Suda","character":"Gray Heron (voice)","img":null,"order":1},{"name":"Ko Shibasaki","character":"Kiriko (voice)","img":null,"order":2},{"name":"Aimyon","character":"Lady Himi (voice)","img":null,"order":3},{"name":"Yoshino Kimura","character":"Natsuko (voice)","img":null,"order":4},{"name":"Takuya Kimura","character":"Shoichi Maki (voice)","img":null,"order":5},{"name":"Keiko Takeshita","character":"Izumi (voice)","img":null,"order":6},{"name":"Jun Fubuki","character":"Utako (voice)","img":null,"order":7},{"name":"Sawako Agawa","character":"Eriko (voice)","img":null,"order":8},{"name":"Karen Takizawa","character":"Warawara (voice)","img":null,"order":9},{"name":"Shinobu Otake","character":"Aiko (voice)","img":null,"order":10},{"name":"Jun Kunimura","character":"The Parakeet King (voice)","img":null,"order":11},{"name":"Kaoru Kobayashi","character":"Noble Pelican (voice)","img":null,"order":12},{"name":"Sh\u014dhei Hino","character":"Granduncle (voice)","img":null,"order":13}],"movie:197":[{"name":"Mel Gibson","character":"William Wallace","img":null,"order":0},{"name":"Catherine McCormack","character":"Murron MacClannough","img":null,"order":1},{"name":"Sophie Marceau","character":"Princess Isabelle","img":null,"order":2},{"name":"Patrick McGoohan","character":"King Edward","img":null,"order":3},{"name":"Angus Macfadyen","character":"Robert the Bruce","img":null,"order":4},{"name":"Brendan Gleeson","character":"Hamish Campbell","img":null,"order":5},{"name":"James Robinson","character":"Young William Wallace","img":null,"order":6},{"name":"James Cosmo","character":"Campbell","img":null,"order":7},{"name":"Sean McGinley","character":"MacClannough","img":null,"order":8},{"name":"Gerda Stevenson","character":"Mother MacClannough","img":null,"order":9},{"name":"Mhairi Calvey","character":"Young Murron MacClannough","img":null,"order":10},{"name":"Jeanne Marine","character":"Nicolette","img":null,"order":11},{"name":"Sean Lawlor","character":"Malcolm Wallace","img":null,"order":12},{"name":"Sandy Nelson","character":"John Wallace","img":null,"order":13},{"name":"Alan Tall","character":"Elder Stewart","img":null,"order":14}],"movie:2108":[{"name":"Emilio Estevez","character":"Andrew Clark","img":null,"order":0},{"name":"Judd Nelson","character":"John Bender","img":null,"order":1},{"name":"Molly Ringwald","character":"Claire Standish","img":null,"order":2},{"name":"Anthony Michael Hall","character":"Brian Johnson","img":null,"order":3},{"name":"Ally Sheedy","character":"Allison Reynolds","img":null,"order":4},{"name":"Paul Gleason","character":"Richard Vernon","img":null,"order":5},{"name":"John Kapelos","character":"Carl","img":null,"order":6},{"name":"Perry Crawford","character":"Allison's Father","img":null,"order":7},{"name":"Mary Christian","character":"Brian's Sister","img":null,"order":8},{"name":"Ron Dean","character":"Andy's Father","img":null,"order":9},{"name":"Tim Gamble","character":"Claire's Father","img":null,"order":10},{"name":"Fran Gargano","character":"Allison's Mom","img":null,"order":11},{"name":"Mercedes Hall","character":"Brian's Mom","img":null,"order":12},{"name":"John Hughes","character":"Brian\u2019s Father (uncredited)","img":null,"order":13},{"name":"Jonathan Chapin","character":"Janitor (uncredited)","img":null,"order":14}],"movie:10515":[{"name":"Keiko Yokozawa","character":"Sheeta (voice)","img":null,"order":0},{"name":"Mayumi Tanaka","character":"Pazu (voice)","img":null,"order":1},{"name":"Minori Terada","character":"Muska (voice)","img":null,"order":2},{"name":"Kotoe Hatsui","character":"Dola (voice)","img":null,"order":3},{"name":"Fujio Tokita","character":"Uncle Pomme (voice)","img":null,"order":4},{"name":"Ichiro Nagai","character":"Shogun Mouro (voice)","img":null,"order":5},{"name":"Hiroshi Ito","character":"Oyakata (voice)","img":null,"order":6},{"name":"Machiko Washio","character":"Okami (voice)","img":null,"order":7},{"name":"Takuz\u014d Kamiyama","character":"Charles / Shalulu (voice)","img":null,"order":8},{"name":"Yoshito Yasuhara","character":"Louis (voice)","img":null,"order":9},{"name":"Sukekiyo Kameyama","character":"Henri (voice)","img":null,"order":10},{"name":"Eken Mine","character":"Underling (voice)","img":null,"order":11},{"name":"TARAKO","character":"Madge (voice)","img":null,"order":12},{"name":"Masashi Sugawara","character":"Underling (voice)","img":null,"order":13},{"name":"Reiko Suzuki","character":"Sheeta's Grandmother (voice)","img":null,"order":14}],"movie:15":[{"name":"Orson Welles","character":"Charles Foster Kane","img":null,"order":0},{"name":"Joseph Cotten","character":"Jedediah Leland","img":null,"order":1},{"name":"Dorothy Comingore","character":"Susan Alexander Kane","img":null,"order":2},{"name":"Ray Collins","character":"Jim W. Gettys","img":null,"order":3},{"name":"George Coulouris","character":"Walter Parks Thatcher","img":null,"order":4},{"name":"Agnes Moorehead","character":"Mary Kane","img":null,"order":5},{"name":"Paul Stewart","character":"Raymond","img":null,"order":6},{"name":"Ruth Warrick","character":"Emily Norton Kane","img":null,"order":7},{"name":"Erskine Sanford","character":"Herbert Carter","img":null,"order":8},{"name":"William Alland","character":"Jerry Thompson/Narrator","img":null,"order":9},{"name":"Everett Sloane","character":"Mr. Bernstein","img":null,"order":10},{"name":"Fortunio Bonanova","character":"Signor Matiste","img":null,"order":11},{"name":"Gus Schilling","character":"John","img":null,"order":12},{"name":"Philip Van Zandt","character":"Mr. Rawlston","img":null,"order":13},{"name":"Georgia Backus","character":"Bertha Anderson","img":null,"order":14}],"movie:974576":[{"name":"Ralph Fiennes","character":"Lawrence","img":null,"order":0},{"name":"Stanley Tucci","character":"Bellini","img":null,"order":1},{"name":"John Lithgow","character":"Tremblay","img":null,"order":2},{"name":"Isabella Rossellini","character":"Sister Agnes","img":null,"order":3},{"name":"Lucian Msamati","character":"Adeyemi","img":null,"order":4},{"name":"Carlos Diehz","character":"Benitez","img":null,"order":5},{"name":"Sergio Castellitto","character":"Tedesco","img":null,"order":6},{"name":"Br\u00edan F. O'Byrne","character":"O'Malley","img":null,"order":7},{"name":"Merab Ninidze","character":"Sabbadin","img":null,"order":8},{"name":"Thomas Loibl","character":"Mandorff","img":null,"order":9},{"name":"Jacek Koman","character":"Wozniak","img":null,"order":10},{"name":"Bruno Novelli","character":"Dead Pope","img":null,"order":11},{"name":"Rony Kramer","character":"Mendoza","img":null,"order":12},{"name":"Valerio Da Silva","character":"Father Haas","img":null,"order":13},{"name":"Joseph Mydell","character":"Nakitanda","img":null,"order":14}],"movie:567797":[{"name":"Eric Bana","character":"Aaron Falk","img":null,"order":0},{"name":"Genevieve O'Reilly","character":"Gretchen Schoner","img":"4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg","order":1},{"name":"Keir O'Donnell","character":"Greg Raco","img":null,"order":2},{"name":"John Polson","character":"Scott Whitlam","img":null,"order":3},{"name":"Matt Nable","character":"Grant Dow","img":null,"order":4},{"name":"Eddie Baroo","character":"McMurdo","img":null,"order":5},{"name":"Martin Dingle Wall","character":"Luke Hadler","img":null,"order":6},{"name":"Julia Blake","character":"Barb Hadler","img":null,"order":7},{"name":"Bruce Spence","character":"Gerry Hadler","img":null,"order":8},{"name":"BeBe Bettencourt","character":"Ellie Deacon","img":null,"order":9},{"name":"Jeremy Lindsay Taylor","character":"Erik Falk","img":null,"order":10},{"name":"Miranda Tapsell","character":"Rita Raco","img":null,"order":11},{"name":"Renee Lim","character":"Sandra Whitlam","img":null,"order":12},{"name":"James Frecheville","character":"Sullivan","img":null,"order":13},{"name":"William Zappa","character":"Mal Deacon","img":null,"order":14}],"movie:37735":[{"name":"Emma Stone","character":"Olive","img":null,"order":0},{"name":"Penn Badgley","character":"Woodchuck Todd","img":null,"order":1},{"name":"Amanda Bynes","character":"Marianne","img":null,"order":2},{"name":"Dan Byrd","character":"Brandon","img":null,"order":3},{"name":"Thomas Haden Church","character":"Mr. Griffith","img":null,"order":4},{"name":"Patricia Clarkson","character":"Rosemary","img":null,"order":5},{"name":"Cam Gigandet","character":"Micah","img":null,"order":6},{"name":"Lisa Kudrow","character":"Mrs. Griffith","img":null,"order":7},{"name":"Malcolm McDowell","character":"Principal Gibbons","img":null,"order":8},{"name":"Aly Michalka","character":"Rhiannon","img":null,"order":9},{"name":"Stanley Tucci","character":"Dill","img":null,"order":10},{"name":"Fred Armisen","character":"Pastor","img":null,"order":11},{"name":"Juliette Goglia","character":"Eighth Grade Olive","img":null,"order":12},{"name":"Jake Sandvig","character":"Anson","img":null,"order":13},{"name":"Morgan Rusler","character":"Mr. Abernathy","img":null,"order":14}],"movie:559969":[{"name":"Aaron Paul","character":"Jesse","img":null,"order":0},{"name":"Jesse Plemons","character":"Todd","img":null,"order":1},{"name":"Charles Baker","character":"Skinny Pete","img":null,"order":2},{"name":"Matt Jones","character":"Badger","img":null,"order":3},{"name":"Scott MacArthur","character":"Neil","img":null,"order":4},{"name":"Larry Hankin","character":"Old Joe","img":null,"order":5},{"name":"Scott Shepherd","character":"Casey","img":null,"order":6},{"name":"Tom Bower","character":"Lou","img":null,"order":7},{"name":"Robert Forster","character":"Ed","img":null,"order":8},{"name":"Jonathan Banks","character":"Mike","img":null,"order":9},{"name":"Bryan Cranston","character":"Walt","img":null,"order":10},{"name":"Krysten Ritter","character":"Jane","img":null,"order":11},{"name":"Kevin Rankin","character":"Kenny","img":null,"order":12},{"name":"Tess Harper","character":"Mrs. Pinkman","img":null,"order":13},{"name":"Michael Bofshever","character":"Mr. Pinkman","img":null,"order":14}],"movie:715931":[{"name":"Will Smith","character":"Peter","img":null,"order":0},{"name":"Ben Foster","character":"Jim Fassel","img":null,"order":1},{"name":"Charmaine Bingwa","character":"Dodienne","img":null,"order":2},{"name":"Gilbert Owuor","character":"Gordon","img":null,"order":3},{"name":"Ronnie Gene Blevins","character":"Harrington","img":null,"order":4},{"name":"Aaron Moten","character":"Knowls","img":null,"order":5},{"name":"Jabbar Lewis","character":"Tomas","img":null,"order":6},{"name":"Michael Luwoye","character":"John","img":null,"order":7},{"name":"Steven Ogg","character":"Howard","img":null,"order":8},{"name":"Grant Harvey","character":"Leeds","img":null,"order":9},{"name":"Mustafa Shakir","character":"Lt. Andrew Cailloux","img":null,"order":10},{"name":"Paul Ben-Victor","character":"Major G. Halstead","img":null,"order":11},{"name":"David Denman","character":"General William Dwight","img":null,"order":12},{"name":"Jesse C. Boyd","character":"Mike Hurley","img":null,"order":13},{"name":"Imani Pullum","character":"Betsy","img":null,"order":14}],"movie:445629":[{"name":"Florence Pugh","character":"Saraya 'Paige' Knight","img":null,"order":0},{"name":"Lena Headey","character":"Julia 'Sweet Saraya' Knight","img":null,"order":1},{"name":"Nick Frost","character":"Patrick 'Rowdy Ricky' Knight","img":null,"order":2},{"name":"Vince Vaughn","character":"Hutch Morgan","img":null,"order":3},{"name":"Jack Lowden","character":"Zak 'Zodiac' Knight","img":null,"order":4},{"name":"Dwayne Johnson","character":"Dwayne 'The Rock' Johnson","img":null,"order":5},{"name":"Thea Trinidad","character":"AJ Lee","img":null,"order":6},{"name":"Aqueela Zoll","character":"Kirsten","img":null,"order":7},{"name":"Stephen Merchant","character":"Hugh","img":null,"order":8},{"name":"Julia Davis","character":"Daphne","img":null,"order":9},{"name":"James Burrows","character":"Roy Knight","img":null,"order":10},{"name":"Hannah Rae","character":"Courtney","img":null,"order":11},{"name":"Kim Matula","character":"Jeri-Lynn","img":null,"order":12},{"name":"Paul Wight","character":"Big Show","img":null,"order":13},{"name":"Stephen Farrelly","character":"Sheamus","img":null,"order":14}],"movie:832262":[{"name":"Eric Bana","character":"Aaron Falk","img":null,"order":0},{"name":"Anna Torv","character":"Alice","img":null,"order":1},{"name":"Deborra-Lee Furness","character":"Jill Bailey","img":null,"order":2},{"name":"Robin McLeavy","character":"Lauren","img":null,"order":3},{"name":"Sisi Stringer","character":"Beth","img":null,"order":4},{"name":"Lucy Ansell","character":"Bree","img":null,"order":5},{"name":"Jacqueline McKenzie","character":"Carmen Cooper","img":null,"order":6},{"name":"Jeremy Lindsay Taylor","character":"Erik Falk","img":null,"order":7},{"name":"Richard Roxburgh","character":"Daniel Bailey","img":null,"order":8},{"name":"Tony Briggs","character":"Ian Chase","img":null,"order":9},{"name":"Kenneth Radley","character":"Sergeant King","img":null,"order":10},{"name":"Ash Ricardo","character":"Jennifer Falk","img":null,"order":11},{"name":"Ingrid Torelli","character":"Margot","img":null,"order":12},{"name":"Matilda May Pawsey","character":"Rebecca","img":null,"order":13},{"name":"Archie Thomson","character":"Aaron Falk","img":null,"order":14}],"movie:316152":[{"name":"Matthew McConaughey","character":"Newton Knight","img":null,"order":0},{"name":"Gugu Mbatha-Raw","character":"Rachel","img":null,"order":1},{"name":"Mahershala Ali","character":"Moses Washington","img":null,"order":2},{"name":"Keri Russell","character":"Serena Knight","img":null,"order":3},{"name":"Jacob Lofland","character":"Daniel","img":null,"order":4},{"name":"Sean Bridgers","character":"Sumrall","img":null,"order":5},{"name":"Brad Carter","character":"Lieutenant Barbour","img":null,"order":6},{"name":"Jane McNeill","character":"Miss Ellie","img":null,"order":7},{"name":"Gary Grubbs","character":"Prosecuting Attorney","img":null,"order":8},{"name":"Christopher Berry","character":"Jasper","img":null,"order":9},{"name":"Joe Chrest","character":"Amos Deason","img":null,"order":10},{"name":"David Jensen","character":"Quitman","img":null,"order":11},{"name":"Kurt Krause","character":"Injured Soldier","img":null,"order":12},{"name":"Carlton Caudle","character":"Confederate Color Guard","img":null,"order":13},{"name":"Martin Bats Bradford","character":"Freedman 1","img":null,"order":14}],"movie:83389":[{"name":"Masami Nagasawa","character":"Umi Matsuzaki (voice)","img":null,"order":0},{"name":"Junichi Okada","character":"Shun Kazama / Yuichiro Sawamura (voice)","img":null,"order":1},{"name":"Keiko Takeshita","character":"Hana Matsuzaki (voice)","img":null,"order":2},{"name":"Yuriko Ishida","character":"Miki Hokuto (voice)","img":null,"order":3},{"name":"Rumi Hiiragi","character":"Sachiko Hirokoji (voice)","img":null,"order":4},{"name":"Jun Fubuki","character":"Ryoko Matsuzaki (voice)","img":null,"order":5},{"name":"Takashi Naito","character":"Yoshio Onodera (voice)","img":null,"order":6},{"name":"Shunsuke Kazama","character":"Shiro Mizunuma / Hiroshi Tachibana (voice)","img":null,"order":7},{"name":"Nao Omori","character":"Akio Kazama (voice)","img":null,"order":8},{"name":"Teruyuki Kagawa","character":"Tokumaru Board Chairman (voice)","img":null,"order":9},{"name":"Haruka Shiraishi","character":"Sora Matsuzaki (voice)","img":null,"order":10},{"name":"Tsubasa Kobayashi","character":"Riku Matsuzaki (voice)","img":null,"order":11},{"name":"Ayako Ito","character":"Secretary to the Tokumaru Board Chairman (voice)","img":null,"order":12},{"name":"Naoya Fujimaki","character":"Tokumaru Front Desk Receptionist (voice)","img":null,"order":13},{"name":"Toshimi Kanno","character":"Nobuko (voice)","img":null,"order":14}],"movie:9323":[{"name":"Atsuko Tanaka","character":"Motoko Kusanagi (voice)","img":null,"order":0},{"name":"Akio Otsuka","character":"Batou (voice)","img":null,"order":1},{"name":"Iemasa Kayumi","character":"Puppetmaster (voice)","img":null,"order":2},{"name":"Koichi Yamadera","character":"Togusa (voice)","img":"biov5jENp2XYfehkcdnRvi796Wi.jpg","order":3},{"name":"Yutaka Nakano","character":"Ishikawa (voice)","img":null,"order":4},{"name":"Tamio Ohki","character":"Aramaki (voice)","img":null,"order":5},{"name":"Tessyo Genda","character":"Director Nakamura (voice)","img":null,"order":6},{"name":"Masakazu Namaki","character":"Dr. Willis (voice)","img":null,"order":7},{"name":"Masato Yamanouchi","character":"Minister of Foreign Affairs (voice)","img":null,"order":8},{"name":"Shinji Ogawa","character":"Gaik\u00f4kan (voice)","img":null,"order":9},{"name":"Mitsuru Miyamoto","character":"Mizuho Daita (voice)","img":null,"order":10},{"name":"Kazuhiro Yamaji","character":"Garbage Collector A (voice)","img":null,"order":11},{"name":"Shigeru Chiba","character":"Garbage Collector B (voice)","img":"mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg","order":12},{"name":"Hiroshi Yanaka","character":"Coroner (voice)","img":null,"order":13},{"name":"Ginzo Matsuo","character":"Old Man (voice)","img":null,"order":14}],"movie:12140":[{"name":"Atsuko Tanaka","character":"Motoko Kusanagi (voice)","img":null,"order":0},{"name":"Akio Otsuka","character":"Batou (voice)","img":null,"order":1},{"name":"Koichi Yamadera","character":"Togusa (voice)","img":"biov5jENp2XYfehkcdnRvi796Wi.jpg","order":2},{"name":"Tamio Ohki","character":"Section 9 Department Chief Aramaki (voice)","img":null,"order":3},{"name":"Yutaka Nakano","character":"Ishikawa (voice)","img":null,"order":4},{"name":"Hiroaki Hirata","character":"Koga (voice)","img":null,"order":5},{"name":"Masaki Terasoma","character":"Azuma (voice)","img":null,"order":6},{"name":"Sumi Mutoh","character":"Mysterious Woman (voice)","img":null,"order":7},{"name":"Yuzuru Fujimoto","character":"Criminal (voice)","img":null,"order":8},{"name":"Sukekiyo Kameyama","character":"Rin (voice)","img":null,"order":9},{"name":"Ryuji Nakagi","character":"Wakabayashi (voice)","img":null,"order":10},{"name":"Fumihiko Tachiki","character":"Bodyguard (voice)","img":null,"order":11},{"name":"Minoru Hirano","character":"Cyborg Doctor (voice)","img":null,"order":12},{"name":"Katsunosuke Hori","character":"Chief of Forensics Division (voice)","img":null,"order":13},{"name":"Naoto Takenaka","character":"Kim (voice)","img":null,"order":14}],"movie:238":[{"name":"Marlon Brando","character":"Don Vito Corleone","img":null,"order":0},{"name":"Al Pacino","character":"Michael Corleone","img":null,"order":1},{"name":"James Caan","character":"Sonny Corleone","img":null,"order":2},{"name":"Robert Duvall","character":"Tom Hagen","img":null,"order":3},{"name":"Richard S. Castellano","character":"Clemenza","img":null,"order":4},{"name":"Diane Keaton","character":"Kay Adams","img":null,"order":5},{"name":"Talia Shire","character":"Connie Corleone Rizzi","img":null,"order":6},{"name":"Gianni Russo","character":"Carlo Rizzi","img":null,"order":7},{"name":"Sterling Hayden","character":"Captain McCluskey","img":null,"order":8},{"name":"John Marley","character":"Jack Woltz","img":null,"order":9},{"name":"Richard Conte","character":"Barzini","img":null,"order":10},{"name":"Al Lettieri","character":"Sollozzo","img":null,"order":11},{"name":"Abe Vigoda","character":"Sal Tessio","img":null,"order":12},{"name":"John Cazale","character":"Fredo Corleone","img":null,"order":13},{"name":"Rudy Bond","character":"Cuneo","img":null,"order":14}],"movie:240":[{"name":"Al Pacino","character":"Don Michael Corleone","img":null,"order":0},{"name":"Robert Duvall","character":"Tom Hagen","img":null,"order":1},{"name":"Diane Keaton","character":"Kay Corleone","img":null,"order":2},{"name":"Robert De Niro","character":"Vito Corleone","img":null,"order":3},{"name":"John Cazale","character":"Frederico 'Fredo' Corleone","img":null,"order":4},{"name":"Talia Shire","character":"Constanzia 'Connie' Corleone","img":null,"order":5},{"name":"Lee Strasberg","character":"Hyman Roth","img":null,"order":6},{"name":"Michael V. Gazzo","character":"Frank Pentangeli","img":null,"order":7},{"name":"G.D. Spradlin","character":"Senator Pat Geary","img":null,"order":8},{"name":"Richard Bright","character":"Al Neri","img":null,"order":9},{"name":"Gastone Moschin","character":"Don Fanucci","img":null,"order":10},{"name":"Tom Rosqui","character":"Rocco Lampone","img":null,"order":11},{"name":"Bruno Kirby","character":"Young Clemenza","img":null,"order":12},{"name":"Frank Sivero","character":"Genco Abbandando","img":null,"order":13},{"name":"Francesca De Sapio","character":"Young Mama Corleone","img":null,"order":14}],"movie:242":[{"name":"Al Pacino","character":"Michael Corleone","img":null,"order":0},{"name":"Diane Keaton","character":"Kay Adams","img":null,"order":1},{"name":"Talia Shire","character":"Connie Corleone Rizzi","img":null,"order":2},{"name":"Andy Garcia","character":"Vincent Mancini","img":null,"order":3},{"name":"Eli Wallach","character":"Don Altobello","img":null,"order":4},{"name":"Joe Mantegna","character":"Joey Zasa","img":null,"order":5},{"name":"George Hamilton","character":"B.J. Harrison","img":null,"order":6},{"name":"Bridget Fonda","character":"Grace Hamilton","img":null,"order":7},{"name":"Sofia Coppola","character":"Mary Corleone","img":null,"order":8},{"name":"Raf Vallone","character":"Cardinal Lamberto","img":null,"order":9},{"name":"Franc D'Ambrosio","character":"Anthony Corleone","img":null,"order":10},{"name":"Donal Donnelly","character":"Archbishop Gilday","img":null,"order":11},{"name":"Richard Bright","character":"Al Neri","img":null,"order":12},{"name":"Helmut Berger","character":"Frederick Keinszig","img":null,"order":13},{"name":"Don Novello","character":"Dominic Abbandando","img":null,"order":14}],"movie:5279":[{"name":"Maggie Smith","character":"Constance Trentham","img":null,"order":0},{"name":"Michael Gambon","character":"William McCordle","img":null,"order":1},{"name":"Kristin Scott Thomas","character":"Sylvia McCordle","img":null,"order":2},{"name":"Camilla Rutherford","character":"Isobel McCordle","img":null,"order":3},{"name":"Charles Dance","character":"Raymond Stockbridge","img":null,"order":4},{"name":"Geraldine Somerville","character":"Louisa Stockbridge","img":null,"order":5},{"name":"Tom Hollander","character":"Anthony Meredith","img":null,"order":6},{"name":"Natasha Wightman","character":"Lavinia Meredith","img":null,"order":7},{"name":"Jeremy Northam","character":"Ivor Novello","img":null,"order":8},{"name":"Bob Balaban","character":"Morris Weissman","img":null,"order":9},{"name":"James Wilby","character":"Freddie Nesbitt","img":null,"order":10},{"name":"Claudie Blakley","character":"Mabel Nesbitt","img":null,"order":11},{"name":"Laurence Fox","character":"Rupert Standish","img":null,"order":12},{"name":"Trent Ford","character":"Jeremy Blond","img":null,"order":13},{"name":"Ryan Phillippe","character":"Henry Denton","img":null,"order":14}],"movie:12477":[{"name":"Tsutomu Tatsumi","character":"Seita (voice)","img":null,"order":0},{"name":"Ayano Shiraishi","character":"Setsuko (voice)","img":null,"order":1},{"name":"Yoshiko Shinohara","character":"Mother (voice)","img":null,"order":2},{"name":"Akemi Yamaguchi","character":"Auntie (voice)","img":null,"order":3},{"name":"Masayo Sakai","character":"Woman who takes care of Setsuko (voice)","img":null,"order":4},{"name":"Kozo Hashida","character":"Obayashi Chairman (voice)","img":null,"order":5},{"name":"Kazumi Nozaki","character":"Cousin (voice)","img":null,"order":6},{"name":"Yoshio Matsuoka","character":"Gosaku (voice)","img":null,"order":7},{"name":"Masahiro Kanetake","character":"Aunt's House Guest (voice)","img":null,"order":8},{"name":"Kiyoshi Yanagawa","character":"Patrolman (voice)","img":null,"order":9},{"name":"Hajime Maki","character":"Man who arrests Seita (voice)","img":null,"order":10},{"name":"Atsuo Omotem","character":"Person in Bank (voice)","img":null,"order":11},{"name":"Teruhisa Harita","character":"Station Worker (voice)","img":null,"order":12},{"name":"Hiroshi Tanaka","character":"Person in Bank (voice)","img":null,"order":13},{"name":"Michio Denpo","character":"Station Worker / Doctor (voice)","img":null,"order":14}],"movie:64682":[{"name":"Leonardo DiCaprio","character":"Jay Gatsby","img":null,"order":0},{"name":"Tobey Maguire","character":"Nick Carraway","img":null,"order":1},{"name":"Carey Mulligan","character":"Daisy Buchanan","img":null,"order":2},{"name":"Joel Edgerton","character":"Tom Buchanan","img":null,"order":3},{"name":"Elizabeth Debicki","character":"Jordan Baker","img":null,"order":4},{"name":"Isla Fisher","character":"Myrtle Wilson","img":null,"order":5},{"name":"Jason Clarke","character":"George Wilson","img":null,"order":6},{"name":"Amitabh Bachchan","character":"Meyer Wolfsheim","img":null,"order":7},{"name":"Callan McAuliffe","character":"Teen Jay Gatsby","img":null,"order":8},{"name":"Adelaide Clemens","character":"Catherine","img":null,"order":9},{"name":"Steve Bisley","character":"Dan Cody","img":null,"order":10},{"name":"Richard Carter","character":"Herzog","img":null,"order":11},{"name":"Jack Thompson","character":"Dr. Walter Perkins","img":null,"order":12},{"name":"Vince Colosimo","character":"Michaelis","img":null,"order":13},{"name":"Max Cullen","character":"Owl Eyes","img":null,"order":14}],"movie:590":[{"name":"Nicole Kidman","character":"Virginia Woolf","img":null,"order":0},{"name":"Julianne Moore","character":"Laura Brown","img":null,"order":1},{"name":"Meryl Streep","character":"Clarissa Vaughan","img":null,"order":2},{"name":"Stephen Dillane","character":"Leonard Woolf","img":null,"order":3},{"name":"Jack Rovello","character":"Richie","img":null,"order":4},{"name":"Ed Harris","character":"Richard Brown","img":null,"order":5},{"name":"Miranda Richardson","character":"Vanessa Bell","img":null,"order":6},{"name":"John C. Reilly","character":"Dan Brown","img":null,"order":7},{"name":"Allison Janney","character":"Sally Lester","img":null,"order":8},{"name":"George Loftus","character":"Quentin Bell","img":null,"order":9},{"name":"Charley Ramm","character":"Julian Bell","img":null,"order":10},{"name":"Sophie Wyburd","character":"Angelica Bell","img":null,"order":11},{"name":"Lyndsey Marshal","character":"Lottie Hope","img":null,"order":12},{"name":"Linda Bassett","character":"Nelly Boxall","img":null,"order":13},{"name":"Christian Coulson","character":"Ralph Partridge","img":null,"order":14}],"movie:4935":[{"name":"Chieko Baisho","character":"Sophie (voice)","img":null,"order":0},{"name":"Takuya Kimura","character":"Howl (voice)","img":null,"order":1},{"name":"Akihiro Miwa","character":"Witch of the Waste (voice)","img":null,"order":2},{"name":"Tatsuya Gashuin","character":"Calcifer (voice)","img":null,"order":3},{"name":"Ryunosuke Kamiki","character":"Markl (voice)","img":null,"order":4},{"name":"Mitsunori Isaki","character":"Kosho (voice)","img":null,"order":5},{"name":"Yo Oizumi","character":"Prince Justin / Turnip Head (voice)","img":null,"order":6},{"name":"Akio Otsuka","character":"King of Ingary (voice)","img":null,"order":7},{"name":"Daijir\u014d Harada","character":"Heen (voice)","img":null,"order":8},{"name":"Haruko Kat\u014d","character":"Madame Suliman (voice)","img":null,"order":9},{"name":"Makoto Yasumura","character":"(voice)","img":null,"order":10},{"name":"Manabu Muraji","character":"(voice)","img":null,"order":11},{"name":"Keiko Tsukamoto","character":"(voice)","img":null,"order":12},{"name":"Tsuzuki Kayako","character":"(voice)","img":null,"order":13},{"name":"Seiji Sasaki","character":"(voice)","img":null,"order":14}],"movie:467909":[{"name":"Anthony Ramos","character":"Usnavi","img":null,"order":0},{"name":"Corey Hawkins","character":"Benny","img":null,"order":1},{"name":"Leslie Grace","character":"Nina Rosario","img":null,"order":2},{"name":"Melissa Barrera","character":"Vanessa","img":null,"order":3},{"name":"Olga Merediz","character":"Abuela Claudia","img":null,"order":4},{"name":"Daphne Rubin-Vega","character":"Daniela","img":null,"order":5},{"name":"Gregory D\u00edaz IV","character":"Sonny","img":null,"order":6},{"name":"Marc Anthony","character":"Gapo","img":null,"order":7},{"name":"Jimmy Smits","character":"Kevin Rosario","img":null,"order":8},{"name":"Stephanie Beatriz","character":"Carla","img":null,"order":9},{"name":"Dascha Polanco","character":"Cuca","img":null,"order":10},{"name":"Noah Catala","character":"Graffiti Pete","img":null,"order":11},{"name":"Lin-Manuel Miranda","character":"Pirag\u00fcero","img":null,"order":12},{"name":"Mateo G\u00f3mez","character":"Alejandro","img":null,"order":13},{"name":"Patrick Page","character":"Pike","img":null,"order":14}],"movie:19994":[{"name":"Megan Fox","character":"Jennifer","img":null,"order":0},{"name":"Amanda Seyfried","character":"Needy","img":null,"order":1},{"name":"Johnny Simmons","character":"Chip","img":null,"order":2},{"name":"Adam Brody","character":"Nikolai","img":null,"order":3},{"name":"Sal Cortez","character":"Chas","img":null,"order":4},{"name":"Ryan Levine","character":"Mick","img":null,"order":5},{"name":"Juan Riedinger","character":"Dirk","img":null,"order":6},{"name":"Colin Askey","character":"Keyboardist","img":null,"order":7},{"name":"Chris Pratt","character":"Roman Duda","img":null,"order":8},{"name":"Juno Rinaldi","character":"Officer Warzak","img":null,"order":9},{"name":"Kyle Gallner","character":"Colin Gray","img":null,"order":10},{"name":"Josh Emerson","character":"Jonas Kozelle","img":null,"order":11},{"name":"J.K. Simmons","character":"Mr. Wroblewski","img":null,"order":12},{"name":"Amy Sedaris","character":"Needy's Mom","img":null,"order":13},{"name":"Cynthia Stevenson","character":"Chip's Mom","img":null,"order":14}],"movie:16859":[{"name":"Minami Takayama","character":"Kiki / Ursula (voice)","img":null,"order":0},{"name":"Rei Sakuma","character":"Jiji (voice)","img":null,"order":1},{"name":"Kappei Yamaguchi","character":"Tombo (voice)","img":null,"order":2},{"name":"Keiko Toda","character":"Osono (voice)","img":null,"order":3},{"name":"Mieko Nobusawa","character":"Kokiri (voice)","img":null,"order":4},{"name":"Koichi Miura","character":"Okino (voice)","img":null,"order":5},{"name":"Haruko Kat\u014d","character":"Old Lady (voice)","img":null,"order":6},{"name":"Hiroko Seki","character":"Bertha (voice)","img":null,"order":7},{"name":"Yuriko Fuchizaki","character":"Ketto (voice)","img":null,"order":8},{"name":"Koichi Yamadera","character":"Bakery Worker / Policeman (voice)","img":"biov5jENp2XYfehkcdnRvi796Wi.jpg","order":9},{"name":"Kikuko Inoue","character":"Maki (voice)","img":null,"order":10},{"name":"Yuko Kobayashi","character":"Young Senpai Witch (voice)","img":null,"order":11},{"name":"Mika Doi","character":"Ketto's Mother (voice)","img":null,"order":12},{"name":"Takaya Hashi","character":"Ketto's Father (voice)","img":null,"order":13},{"name":"Chika Sakamoto","character":"Baby (voice)","img":null,"order":14}],"movie:9361":[{"name":"Daniel Day-Lewis","character":"Hawkeye","img":null,"order":0},{"name":"Madeleine Stowe","character":"Cora","img":null,"order":1},{"name":"Jodhi May","character":"Alice","img":null,"order":2},{"name":"Russell Means","character":"Chingachgook","img":null,"order":3},{"name":"Wes Studi","character":"Magua","img":null,"order":4},{"name":"Eric Schweig","character":"Uncas","img":null,"order":5},{"name":"Steven Waddington","character":"Heyward","img":null,"order":6},{"name":"Maurice Ro\u00ebves","character":"Colonel Munro","img":null,"order":7},{"name":"Pete Postlethwaite","character":"Captain Beams","img":null,"order":8},{"name":"Colm Meaney","character":"Major Ambrose","img":null,"order":9},{"name":"Patrice Ch\u00e9reau","character":"General Montcalm","img":null,"order":10},{"name":"Edward Blatchford","character":"Jack Winthrop","img":null,"order":11},{"name":"Terry Kinney","character":"John Cameron","img":null,"order":12},{"name":"Tracey Ellis","character":"Alexandra Cameron","img":null,"order":13},{"name":"Justin M. Rice","character":"James Cameron","img":null,"order":14}],"movie:616":[{"name":"Tom Cruise","character":"Nathan Algren","img":null,"order":0},{"name":"Ken Watanabe","character":"Moritsugu Katsumoto","img":null,"order":1},{"name":"Timothy Spall","character":"Simon Graham","img":null,"order":2},{"name":"Tony Goldwyn","character":"Col. Benjamin Bagley","img":null,"order":3},{"name":"Hiroyuki Sanada","character":"Ujio","img":null,"order":4},{"name":"Koyuki","character":"Taka","img":null,"order":5},{"name":"Shin Koyamada","character":"Nobutada","img":null,"order":6},{"name":"Billy Connolly","character":"Zebulon Gant","img":null,"order":7},{"name":"Togo Igawa","character":"General Hasegawa","img":null,"order":8},{"name":"Shichinosuke Nakamura","character":"Emperor Meiji","img":null,"order":9},{"name":"Masato Harada","character":"Omura","img":null,"order":10},{"name":"William Atherton","character":"Winchester Rep","img":null,"order":11},{"name":"Chad Lindberg","character":"Winchester Rep Assistant","img":null,"order":12},{"name":"Scott Wilson","character":"Ambassador Swanbeck","img":null,"order":13},{"name":"Ray Godshall Sr.","character":"Convention Hall Attendee","img":null,"order":14}],"movie:11774":[{"name":"Emily Browning","character":"Violet Baudelaire","img":null,"order":0},{"name":"Liam Aiken","character":"Klaus Baudelaire","img":null,"order":1},{"name":"Kara Hoffman","character":"Sunny Baudelaire","img":null,"order":2},{"name":"Shelby Hoffman","character":"Sunny Baudelaire","img":null,"order":3},{"name":"Jim Carrey","character":"Count Olaf / Dr. Stephano / Captain Sham","img":null,"order":4},{"name":"Jude Law","character":"Lemony Snicket","img":null,"order":5},{"name":"Timothy Spall","character":"Mr. Poe","img":null,"order":6},{"name":"Meryl Streep","character":"Josephine Anwhistle","img":null,"order":7},{"name":"Billy Connolly","character":"Dr. Montgomery Montgomery","img":null,"order":8},{"name":"Catherine O'Hara","character":"Justice Strauss","img":null,"order":9},{"name":"Jennifer Coolidge","character":"White-Faced Women","img":null,"order":10},{"name":"Jane Adams","character":"White-Faced Women","img":null,"order":11},{"name":"Craig Ferguson","character":"Person of Indeterminate Gender","img":null,"order":12},{"name":"Luis Guzm\u00e1n","character":"Bald Man","img":null,"order":13},{"name":"Jamie Harris","character":"Hook-Handed Man","img":null,"order":14}],"movie:503919":[{"name":"Robert Pattinson","character":"Thomas Howard","img":null,"order":0},{"name":"Willem Dafoe","character":"Thomas Wake","img":null,"order":1},{"name":"Valeriia Karaman","character":"Mermaid","img":null,"order":2},{"name":"Logan Hawkes","character":"Ephraim Winslow","img":null,"order":3},{"name":"Kyla Nicolle","character":"Woman on the Rocks","img":null,"order":4},{"name":"Shaun Clarke","character":"Departing Wickie","img":null,"order":5},{"name":"Pierre Richard","character":"Departing Assistant Wickie","img":null,"order":6},{"name":"Preston Hudson","character":"Tender Mate","img":null,"order":7},{"name":"Jeff Cruts","character":"Tender Mate","img":null,"order":8}],"movie:603":[{"name":"Keanu Reeves","character":"Neo","img":null,"order":0},{"name":"Laurence Fishburne","character":"Morpheus","img":null,"order":1},{"name":"Carrie-Anne Moss","character":"Trinity","img":null,"order":2},{"name":"Hugo Weaving","character":"Agent Smith","img":null,"order":3},{"name":"Gloria Foster","character":"Oracle","img":null,"order":4},{"name":"Joe Pantoliano","character":"Cypher","img":null,"order":5},{"name":"Marcus Chong","character":"Tank","img":null,"order":6},{"name":"Julian Arahanga","character":"Apoc","img":null,"order":7},{"name":"Matt Doran","character":"Mouse","img":null,"order":8},{"name":"Belinda McClory","character":"Switch","img":null,"order":9},{"name":"Anthony Ray Parker","character":"Dozer","img":null,"order":10},{"name":"Paul Goddard","character":"Agent Brown","img":null,"order":11},{"name":"Robert Taylor","character":"Agent Jones","img":null,"order":12},{"name":"David Aston","character":"Rhineheart","img":null,"order":13},{"name":"Marc Aden Gray","character":"Choi","img":null,"order":14}],"movie:604":[{"name":"Keanu Reeves","character":"Neo","img":null,"order":0},{"name":"Laurence Fishburne","character":"Morpheus","img":null,"order":1},{"name":"Carrie-Anne Moss","character":"Trinity","img":null,"order":2},{"name":"Hugo Weaving","character":"Agent Smith","img":null,"order":3},{"name":"Jada Pinkett Smith","character":"Niobe","img":null,"order":4},{"name":"Gloria Foster","character":"The Oracle","img":null,"order":5},{"name":"Harold Perrineau","character":"Link","img":null,"order":6},{"name":"Monica Bellucci","character":"Persephone","img":null,"order":7},{"name":"Harry Lennix","character":"Commander Lock","img":null,"order":8},{"name":"Lambert Wilson","character":"The Merovingian","img":null,"order":9},{"name":"Randall Duk Kim","character":"Keymaker","img":null,"order":10},{"name":"Nona Gaye","character":"Zee","img":null,"order":11},{"name":"Anthony Zerbe","character":"Councillor Hamann","img":null,"order":12},{"name":"Daniel Bernhardt","character":"Agent Johnson","img":null,"order":13},{"name":"Helmut Bakaitis","character":"The Architect","img":null,"order":14}],"movie:624860":[{"name":"Keanu Reeves","character":"Neo / Thomas Anderson","img":null,"order":0},{"name":"Carrie-Anne Moss","character":"Trinity / Tiffany","img":null,"order":1},{"name":"Yahya Abdul-Mateen II","character":"Morpheus / Agent Smith","img":null,"order":2},{"name":"Jonathan Groff","character":"Smith","img":null,"order":3},{"name":"Jessica Henwick","character":"Bugs","img":null,"order":4},{"name":"Neil Patrick Harris","character":"The Analyst","img":null,"order":5},{"name":"Jada Pinkett Smith","character":"Niobe","img":null,"order":6},{"name":"Priyanka Chopra Jonas","character":"Sati","img":null,"order":7},{"name":"Christina Ricci","character":"Gwyn de Vere","img":null,"order":8},{"name":"Lambert Wilson","character":"The Merovingian","img":null,"order":9},{"name":"Andrew Lewis Caldwell","character":"Jude","img":null,"order":10},{"name":"Toby Onwumere","character":"Sequoia","img":null,"order":11},{"name":"Max Riemelt","character":"Sheperd","img":null,"order":12},{"name":"Joshua Grothe","character":"FunktI\u00f8n","img":null,"order":13},{"name":"Brian J. Smith","character":"Berg","img":null,"order":14}],"movie:605":[{"name":"Keanu Reeves","character":"Neo","img":null,"order":0},{"name":"Laurence Fishburne","character":"Morpheus","img":null,"order":1},{"name":"Carrie-Anne Moss","character":"Trinity","img":null,"order":2},{"name":"Hugo Weaving","character":"Agent Smith","img":null,"order":3},{"name":"Jada Pinkett Smith","character":"Niobe","img":null,"order":4},{"name":"Mary Alice","character":"The Oracle","img":null,"order":5},{"name":"Harold Perrineau","character":"Link","img":null,"order":6},{"name":"Collin Chou","character":"Seraph","img":null,"order":7},{"name":"Harry Lennix","character":"Commander Lock","img":null,"order":8},{"name":"Ian Bliss","character":"Bane","img":null,"order":9},{"name":"Lambert Wilson","character":"Merovingian","img":null,"order":10},{"name":"Bruce Spence","character":"Trainman","img":null,"order":11},{"name":"Monica Bellucci","character":"Persephone","img":null,"order":12},{"name":"Nona Gaye","character":"Zee","img":null,"order":13},{"name":"Anthony Zerbe","character":"Councillor Hamann","img":null,"order":14}],"movie:1236153":[{"name":"Chris Pratt","character":"Chris Raven","img":null,"order":0},{"name":"Rebecca Ferguson","character":"Judge Maddox","img":null,"order":1},{"name":"Kali Reis","character":"Jacqueline 'Jaq' Diallo","img":null,"order":2},{"name":"Kylie Rogers","character":"Britt Raven","img":null,"order":3},{"name":"Chris Sullivan","character":"Rob Nelson","img":null,"order":4},{"name":"Jeff Pierre","character":"Patrick Burke","img":null,"order":5},{"name":"Annabelle Wallis","character":"Nicole Raven","img":null,"order":6},{"name":"Rafi Gavron","character":"Holt Charles","img":null,"order":7},{"name":"Kenneth Choi","character":"Ray Vale","img":null,"order":8},{"name":"Jamie McBride","character":"Dan Vogel","img":null,"order":9},{"name":"Ross Gosla","character":"David Webb","img":null,"order":10},{"name":"Mark Daneri","character":"Jeff Martin","img":null,"order":11},{"name":"Haydn Dalton","character":"Bill Peterson","img":null,"order":12},{"name":"Michael C. Mahon","character":"Booking Sergeant","img":null,"order":13},{"name":"Noah Fearnley","character":"Tattoed Sleazebag","img":null,"order":14}],"movie:1242332":[{"name":"Bob Odenkirk","character":"Ulysses Richardson","img":null,"order":0},{"name":"Henry Winkler","character":"Mayor Kibner","img":null,"order":1},{"name":"Lena Headey","character":"Moira","img":null,"order":2},{"name":"Billy MacLellan","character":"Deputy Mike Nelson","img":null,"order":3},{"name":"Ryan Allen","character":"Deputy Blaine Anderson","img":null,"order":4},{"name":"Reena Jolly","character":"Lori","img":null,"order":5},{"name":"Brendan Fletcher","character":"Keith","img":null,"order":6},{"name":"Peter Shinkoda","character":"Joe","img":null,"order":7},{"name":"Jess McLeod","character":"Alex","img":null,"order":8},{"name":"Alex MacMillan","character":"Kent","img":null,"order":9},{"name":"Dan De Jaeger","character":"Ernie","img":null,"order":10},{"name":"John C. MacDonald","character":"Kenny","img":null,"order":11},{"name":"David Lawrence Brown","character":"Dr. Walter Vilie","img":null,"order":12},{"name":"Derek Barnes","character":"James","img":null,"order":13},{"name":"Megan MacArton","character":"Mary Beth","img":null,"order":14}],"movie:454227":[{"name":"Chris Pine","character":"Robert Bruce, Earl of Carrick","img":null,"order":0},{"name":"Aaron Taylor-Johnson","character":"James Douglas, Lord of Douglas","img":null,"order":1},{"name":"Florence Pugh","character":"Elizabeth Burgh","img":null,"order":2},{"name":"Billy Howle","character":"Edward, Prince of Wales","img":null,"order":3},{"name":"Sam Spruell","character":"Aymer de Valence, Earl of Pembroke","img":null,"order":4},{"name":"Tony Curran","character":"Angus Og Macdonald, Lord of Islay","img":null,"order":5},{"name":"Callan Mulvey","character":"John III Comyn, Lord of Badenoch","img":null,"order":6},{"name":"James Cosmo","character":"Robert Bruce Senior","img":null,"order":7},{"name":"Stephen Dillane","character":"King Edward I of England","img":null,"order":8},{"name":"Steven Cree","character":"Sir Christopher Seton","img":null,"order":9},{"name":"Alastair Mackenzie","character":"John Strathbogie, Earl of Atholl","img":null,"order":10},{"name":"Chris Fulton","character":"Euan Bruce","img":null,"order":11},{"name":"Lorne MacFadyen","character":"Neil Bruce","img":null,"order":12},{"name":"Jack Greenlees","character":"Alexander Bruce","img":null,"order":13},{"name":"Rebecca Robin","character":"Queen Margaret of England","img":null,"order":14}],"movie:4547":[{"name":"Jodie Foster","character":"Meg Altman","img":null,"order":0},{"name":"Kristen Stewart","character":"Sarah Altman","img":null,"order":1},{"name":"Forest Whitaker","character":"Burnham","img":null,"order":2},{"name":"Dwight Yoakam","character":"Raoul","img":null,"order":3},{"name":"Jared Leto","character":"Junior","img":null,"order":4},{"name":"Patrick Bauchau","character":"Stephen Altman","img":null,"order":5},{"name":"Ann Magnuson","character":"Lydia Lynch","img":null,"order":6},{"name":"Ian Buchanan","character":"Evan Kurlander","img":null,"order":7},{"name":"Andrew Kevin Walker","character":"Sleepy Neighbor","img":null,"order":8},{"name":"Paul Schulze","character":"Officer Keeney","img":null,"order":9},{"name":"Mel Rodriguez","character":"Officer Morales","img":null,"order":10},{"name":"Richard Conant","character":"SWAT Cop","img":null,"order":11},{"name":"Paul Simon","character":"SWAT Cop","img":null,"order":12},{"name":"Victor Thrash","character":"SWAT Cop","img":null,"order":13},{"name":"Ken Turner","character":"SWAT Cop","img":null,"order":14}],"movie:2024":[{"name":"Mel Gibson","character":"Benjamin Martin","img":null,"order":0},{"name":"Heath Ledger","character":"Gabriel Martin","img":null,"order":1},{"name":"Joely Richardson","character":"Charlotte Selton","img":null,"order":2},{"name":"Jason Isaacs","character":"Col. William Tavington","img":null,"order":3},{"name":"Chris Cooper","character":"Col. Harry Burwell","img":null,"order":4},{"name":"Tch\u00e9ky Karyo","character":"Jean Villeneuve","img":null,"order":5},{"name":"Ren\u00e9 Auberjonois","character":"Reverend Oliver","img":null,"order":6},{"name":"Lisa Brenner","character":"Anne Howard","img":null,"order":7},{"name":"Tom Wilkinson","character":"Gen. Cornwallis","img":null,"order":8},{"name":"Donal Logue","character":"Dan Scott","img":null,"order":9},{"name":"Leon Rippy","character":"John Billings","img":null,"order":10},{"name":"Adam Baldwin","character":"Capt. Wilkins","img":"lfbyJWNWGbiYfU0Yr1tjn6Trd2V.jpg","order":11},{"name":"Jay Arlen Jones","character":"Occam","img":null,"order":12},{"name":"Joey D. Vieira","character":"Peter Howard","img":null,"order":13},{"name":"Gregory Smith","character":"Thomas Martin","img":null,"order":14}],"movie:875828":[{"name":"Cillian Murphy","character":"Tommy Shelby","img":null,"order":0},{"name":"Barry Keoghan","character":"Duke Shelby","img":null,"order":1},{"name":"Rebecca Ferguson","character":"Kaulo / Zelda","img":null,"order":2},{"name":"Tim Roth","character":"John Beckett","img":null,"order":3},{"name":"Stephen Graham","character":"Hayden Stagg","img":null,"order":4},{"name":"Sophie Rundle","character":"Ada Shelby","img":null,"order":5},{"name":"Ned Dennehy","character":"Charlie Strong","img":null,"order":6},{"name":"Packy Lee","character":"Johnny Dogs","img":null,"order":7},{"name":"Ian Peck","character":"Curly","img":null,"order":8},{"name":"Jay Lycurgo","character":"Elijah","img":null,"order":9},{"name":"Kasper Hilton-Hille","character":"Karl Thorne","img":null,"order":10},{"name":"Ruby Ashbourne Serkis","character":"Agnes Shelby","img":null,"order":11},{"name":"Bonnie Stott","character":"Ruby","img":null,"order":12},{"name":"Thomas Arnold","character":"Virgil","img":null,"order":13},{"name":"Sam Baker Jones","character":"Jake","img":null,"order":14}],"movie:12429":[{"name":"Yuria Kozuki","character":"Ponyo (voice)","img":null,"order":0},{"name":"Hiroki Doi","character":"Sosuke (voice)","img":null,"order":1},{"name":"George Tokoro","character":"Fujimoto (voice)","img":null,"order":2},{"name":"Tomoko Yamaguchi","character":"Risa (voice)","img":null,"order":3},{"name":"Yuki Amami","character":"Gran Mamare (voice)","img":null,"order":4},{"name":"Kazushige Nagashima","character":"Koichi (voice)","img":null,"order":5},{"name":"Akiko Yano","character":"Ponyo's Sister (voice)","img":null,"order":6},{"name":"Shin'ichi Hatori","character":"Anchorman (voice)","img":null,"order":7},{"name":"Tokie Hidari","character":"Kayo (voice)","img":null,"order":8},{"name":"Rumi Hiiragi","character":"Young Mother (voice)","img":null,"order":9},{"name":"Tomoko Naraoka","character":"Yoshie (voice)","img":null,"order":10},{"name":"Kazuko Yoshiyuki","character":"Toki (voice)","img":null,"order":11},{"name":"Eimi Hiraoka","character":"Kumiko (voice)","img":null,"order":12},{"name":"Nozomi \u014chashi","character":"Karen (voice)","img":null,"order":13},{"name":"Akiko Takeguchi","character":"Noriko (voice)","img":null,"order":14}],"movie:11621":[{"name":"Sh\u016bichir\u014d Moriyama","character":"Porco Rosso (voice)","img":null,"order":0},{"name":"Tokiko Kato","character":"Madame Gina (voice)","img":null,"order":1},{"name":"Bunshi Katsura VI","character":"Grandpa Piccolo (voice)","img":null,"order":2},{"name":"Tsunehiko Kamij\u00f4","character":"Mamma Aiuto Boss (voice)","img":null,"order":3},{"name":"Akemi Okamura","character":"Fio Piccolo (voice)","img":null,"order":4},{"name":"Akio Otsuka","character":"Donald Curtis (voice)","img":null,"order":5},{"name":"Hiroko Seki","character":"Granny (voice)","img":null,"order":6},{"name":"Masayuki Inagaki","character":"Major Ferrarin (voice)","img":null,"order":7},{"name":"Reiz\u014d Nomoto","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":8},{"name":"Osamu Saka","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":9},{"name":"Yu Shimaka","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":10},{"name":"Yuzuru Fujimoto","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":11},{"name":"Tatsuyuki Jinnai","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":12},{"name":"Kazunori Arai","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":13},{"name":"Nobuo Tanaka","character":"Mamma Aiuto Gang Member (voice)","img":null,"order":14}],"movie:128":[{"name":"Yoji Matsuda","character":"Ashitaka (voice)","img":null,"order":0},{"name":"Yuriko Ishida","character":"San / Kaya (voice)","img":null,"order":1},{"name":"Yuko Tanaka","character":"Eboshi Gozen (voice)","img":null,"order":2},{"name":"Kaoru Kobayashi","character":"Jikobo (voice)","img":null,"order":3},{"name":"Masahiko Nishimura","character":"Kouroku (voice)","img":null,"order":4},{"name":"Tsunehiko Kamij\u00f4","character":"Gonza (voice)","img":null,"order":5},{"name":"Akihiro Miwa","character":"Mrs. Moro (voice)","img":null,"order":6},{"name":"Mitsuko Mori","character":"Mrs. Hii (voice)","img":null,"order":7},{"name":"Hisaya Morishige","character":"Chief of Affairs (voice)","img":null,"order":8},{"name":"Sumi Shimamoto","character":"Toki (voice)","img":null,"order":9},{"name":"Tetsu Watanabe","character":"Wild Dog (voice)","img":null,"order":10},{"name":"Makoto Sat\u014d","character":"Tatari God (voice)","img":null,"order":11},{"name":"Akira Nagoya","character":"Cowherd (voice)","img":null,"order":12},{"name":"Kei Iinuma","character":"Chief of the Sick (voice)","img":null,"order":13},{"name":"Tsuzuki Kayako","character":"Kiyo (voice)","img":null,"order":14}],"movie:146233":[{"name":"Hugh Jackman","character":"Keller Dover","img":null,"order":0},{"name":"Jake Gyllenhaal","character":"Detective Loki","img":null,"order":1},{"name":"Viola Davis","character":"Nancy Birch","img":null,"order":2},{"name":"Maria Bello","character":"Grace Dover","img":null,"order":3},{"name":"Terrence Howard","character":"Franklin Birch","img":null,"order":4},{"name":"Melissa Leo","character":"Holly Jones","img":null,"order":5},{"name":"Paul Dano","character":"Alex Jones","img":null,"order":6},{"name":"Dylan Minnette","character":"Ralph Dover","img":null,"order":7},{"name":"Zo\u00eb Soul","character":"Eliza Birch","img":null,"order":8},{"name":"Erin Gerasimovich","character":"Anna Dover","img":null,"order":9},{"name":"Kyla-Drew","character":"Joy Birch","img":null,"order":10},{"name":"Wayne Duvall","character":"Captain Richard O'Malley","img":null,"order":11},{"name":"Len Cariou","character":"Father Patrick Dunn","img":null,"order":12},{"name":"David Dastmalchian","character":"Bob Taylor","img":null,"order":13},{"name":"Brad James","character":"Officer Carter","img":null,"order":14}],"movie:6439":[{"name":"Frankie Muniz","character":"Stripes (voice)","img":null,"order":0},{"name":"Hayden Panettiere","character":"Channing Walsh","img":null,"order":1},{"name":"Bruce Greenwood","character":"Nolan Walsh","img":null,"order":2},{"name":"Mandy Moore","character":"Sandy (voice)","img":null,"order":3},{"name":"Wendie Malick","character":"Clara Dalrymple","img":null,"order":4},{"name":"M. Emmet Walsh","character":"Woodzie","img":null,"order":5},{"name":"Snoop Dogg","character":"Lightning (voice)","img":"rbxBK2m6fEq0oHeMoBUMVYkJEdJ.jpg","order":6},{"name":"Michael Clarke Duncan","character":"Clydesdale (voice)","img":null,"order":7},{"name":"Jeff Foxworthy","character":"Reggie (voice)","img":null,"order":8},{"name":"Whoopi Goldberg","character":"Franny (voice)","img":null,"order":9},{"name":"Steve Harvey","character":"Buzz (voice)","img":null,"order":10},{"name":"David Spade","character":"Scuzz (voice)","img":null,"order":11},{"name":"Dustin Hoffman","character":"Tucker (voice)","img":null,"order":12},{"name":"Joshua Jackson","character":"Trenton's Pride (voice)","img":null,"order":13},{"name":"Joe Pantoliano","character":"Goose (voice)","img":null,"order":14}],"movie:641":[{"name":"Ellen Burstyn","character":"Sara Goldfarb","img":null,"order":0},{"name":"Jared Leto","character":"Harry Goldfarb","img":null,"order":1},{"name":"Jennifer Connelly","character":"Marion Silver","img":null,"order":2},{"name":"Marlon Wayans","character":"Tyrone C. Love","img":null,"order":3},{"name":"Christopher McDonald","character":"Tappy Tibbons","img":null,"order":4},{"name":"Louise Lasser","character":"Ada","img":null,"order":5},{"name":"Marcia Jean Kurtz","character":"Rae","img":null,"order":6},{"name":"Janet Sarno","character":"Mrs. Pearlman","img":null,"order":7},{"name":"Suzanne Shepherd","character":"Mrs. Scarlini","img":null,"order":8},{"name":"Joanne Gordon","character":"Mrs. Ovadia","img":null,"order":9},{"name":"Charlotte Aronofsky","character":"Mrs. Miles","img":null,"order":10},{"name":"Mark Margolis","character":"Mr. Rabinowitz","img":null,"order":11},{"name":"Michael Kaycheck","character":"Donut Cop","img":null,"order":12},{"name":"Jack A. O'Connell","character":"Corn Dog Stand Boss","img":null,"order":13},{"name":"Chas Mastin","character":"Lyle Russel","img":null,"order":14}],"movie:330459":[{"name":"Felicity Jones","character":"Jyn Erso","img":null,"order":0},{"name":"Diego Luna","character":"Cassian Andor","img":"qwbjcyDN37OHiln4a6P4LP31o7F.jpg","order":1},{"name":"Alan Tudyk","character":"K-2SO","img":"jUuUbPuMGonFT5E2pcs4alfqaCN.jpg","order":2},{"name":"Donnie Yen","character":"Chirrut Imwe","img":null,"order":3},{"name":"Jiang Wen","character":"Baze Malbus","img":null,"order":4},{"name":"Ben Mendelsohn","character":"Orson Krennic","img":null,"order":5},{"name":"Guy Henry","character":"Governor Tarkin","img":null,"order":6},{"name":"Forest Whitaker","character":"Saw Gerrera","img":null,"order":7},{"name":"Riz Ahmed","character":"Bodhi Rook","img":null,"order":8},{"name":"Mads Mikkelsen","character":"Galen Erso","img":null,"order":9},{"name":"Jimmy Smits","character":"Bail Organa","img":null,"order":10},{"name":"Alistair Petrie","character":"General Draven","img":null,"order":11},{"name":"Genevieve O'Reilly","character":"Mon Mothma","img":"4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg","order":12},{"name":"Ben Daniels","character":"General Merrick","img":"phEIvq0Ay2KjI5ewmVvpQ1a29GJ.jpg","order":13},{"name":"Paul Kasey","character":"Admiral Raddus","img":null,"order":14}],"movie:13156":[{"name":"Michael Caine","character":"Garth","img":null,"order":0},{"name":"Robert Duvall","character":"Hub","img":null,"order":1},{"name":"Haley Joel Osment","character":"Walter","img":null,"order":2},{"name":"Josh Lucas","character":"Adult Walter","img":null,"order":3},{"name":"Kyra Sedgwick","character":"Mae","img":null,"order":4},{"name":"Christian Kane","character":"Young Hub","img":null,"order":5},{"name":"Nicky Katt","character":"Stan","img":null,"order":6},{"name":"Eric Balfour","character":"Sheik's Grandson","img":null,"order":7},{"name":"Kevin Haberer","character":"Young Garth","img":null,"order":8},{"name":"Emmanuelle Vaugier","character":"Jasmine","img":null,"order":9},{"name":"Mitchel Musso","character":"Boy","img":null,"order":10},{"name":"Marc Musso","character":"Boy","img":null,"order":11},{"name":"Michael O'Neill","character":"Ralph","img":null,"order":12},{"name":"Deirdre O'Connell","character":"Helen","img":null,"order":13},{"name":"Adam Ozturk","character":"The Sheik","img":null,"order":14}],"movie:412117":[{"name":"Patton Oswalt","character":"Max (voice)","img":null,"order":0},{"name":"Kevin Hart","character":"Snowball (voice)","img":null,"order":1},{"name":"Eric Stonestreet","character":"Duke (voice)","img":null,"order":2},{"name":"Jenny Slate","character":"Gidget (voice)","img":null,"order":3},{"name":"Tiffany Haddish","character":"Daisy (voice)","img":null,"order":4},{"name":"Lake Bell","character":"Chloe (voice)","img":null,"order":5},{"name":"Nick Kroll","character":"Sergei (voice)","img":null,"order":6},{"name":"Dana Carvey","character":"Pops (voice)","img":null,"order":7},{"name":"Ellie Kemper","character":"Katie (voice)","img":null,"order":8},{"name":"Harrison Ford","character":"Rooster (voice)","img":null,"order":9},{"name":"Chris Renaud","character":"Norman / Additional Voices (voice)","img":null,"order":10},{"name":"Bobby Moynihan","character":"Mel (voice)","img":null,"order":11},{"name":"Hannibal Buress","character":"Buddy (voice)","img":null,"order":12},{"name":"Pete Holmes","character":"Chuck (voice)","img":null,"order":13},{"name":"Henry Lynch","character":"Liam (voice)","img":null,"order":14}],"movie:1072876":[{"name":"Jake Johnson","character":"Tommy","img":null,"order":0},{"name":"Anna Kendrick","character":"Maddy","img":null,"order":1},{"name":"Natalie Morales","character":"Theresa","img":null,"order":2},{"name":"Andy Samberg","character":"Andy Samberg","img":null,"order":3},{"name":"Mary Holland","character":"Amy","img":null,"order":4},{"name":"Emily Hampshire","character":"Mary","img":null,"order":5},{"name":"Nancy Lenehan","character":"Laurie","img":null,"order":6},{"name":"Daryl J. Johnson","character":"Malcolm","img":null,"order":7},{"name":"Miriam Flynn","character":"Becky Ann","img":null,"order":8},{"name":"Boban Marjanovi\u0107","character":"7 Foot Man","img":null,"order":9},{"name":"Biff Wiff","character":"James / Walter","img":null,"order":10},{"name":"Eduardo Franco","character":"P.A. Ninja","img":null,"order":11},{"name":"Jeff Kober","character":"Cowboy","img":null,"order":12},{"name":"Christopher Lloyd","character":"Dennis Walcott","img":null,"order":13},{"name":"Theo Wilson","character":"Sinbad Guy","img":null,"order":14}],"movie:9040":[{"name":"Al Pacino","character":"Officer Frank Serpico","img":null,"order":0},{"name":"John Randolph","character":"Sidney Green","img":null,"order":1},{"name":"Jack Kehoe","character":"Tom Keough","img":null,"order":2},{"name":"Biff McGuire","character":"Captain McClain","img":null,"order":3},{"name":"Barbara Eda-Young","character":"Laurie","img":null,"order":4},{"name":"Cornelia Sharpe","character":"Leslie Lane","img":null,"order":5},{"name":"Tony Roberts","character":"Bob Blair","img":null,"order":6},{"name":"John Medici","character":"Pasquale","img":null,"order":7},{"name":"Allan Rich","character":"D. A. Tauber","img":null,"order":8},{"name":"Norman Ornellas","character":"Rubello","img":null,"order":9},{"name":"Edward Grover","character":"Lombardo","img":null,"order":10},{"name":"Albert Henderson","character":"Peluce","img":null,"order":11},{"name":"Hank Garrett","character":"Malone","img":null,"order":12},{"name":"Damien Leake","character":"Joey","img":null,"order":13},{"name":"Joseph Bova","character":"Potts","img":null,"order":14}],"movie:479":[{"name":"Samuel L. Jackson","character":"John Shaft","img":null,"order":0},{"name":"Vanessa Williams","character":"Carmen Vasquez","img":null,"order":1},{"name":"Jeffrey Wright","character":"Peoples Hernandez","img":null,"order":2},{"name":"Christian Bale","character":"Walter Wade, Jr.","img":null,"order":3},{"name":"Busta Rhymes","character":"Rasaan","img":null,"order":4},{"name":"Dan Hedaya","character":"Jack Roselli","img":null,"order":5},{"name":"Toni Collette","character":"Diane Palmieri","img":null,"order":6},{"name":"Richard Roundtree","character":"\"Uncle\" John Shaft","img":null,"order":7},{"name":"Ruben Santiago-Hudson","character":"Jimmy Groves","img":null,"order":8},{"name":"Josef Sommer","character":"Curt Flemming","img":null,"order":9},{"name":"Lynne Thigpen","character":"Carla Howard","img":null,"order":10},{"name":"Philip Bosco","character":"Walter Wade Senior","img":null,"order":11},{"name":"Pat Hingle","character":"Judge Dennis Bradford","img":null,"order":12},{"name":"Lee Tergesen","character":"Luger","img":null,"order":13},{"name":"Daniel von Bargen","character":"Lt Kearney","img":null,"order":14}],"movie:378064":[{"name":"Miyu Irino","character":"Shouya Ishida (voice)","img":null,"order":0},{"name":"Saori Hayami","character":"Shouko Nishimiya (voice)","img":null,"order":1},{"name":"Aoi Yuki","character":"Yuzuru Nishimiya (voice)","img":null,"order":2},{"name":"Kensho Ono","character":"Tomohiro Nagatsuka (voice)","img":null,"order":3},{"name":"Yuki Kaneko","character":"Naoka Ueno (voice)","img":null,"order":4},{"name":"Yui Ishikawa","character":"Miyoko Sahara (voice)","img":null,"order":5},{"name":"Megumi Han","character":"Miki Kawai (voice)","img":null,"order":6},{"name":"Toshiyuki Toyonaga","character":"Satoshi Mashiba (voice)","img":null,"order":7},{"name":"Mayu Matsuoka","character":"Shouya Ishida as an Elementary School Boy (voice)","img":null,"order":8},{"name":"Sachiko Kojima","character":"Kazuki Shimada as an Elementary School Boy (voice)","img":null,"order":9},{"name":"Hana Takeda","character":"Keisuke Hirose as an Elementary School Boy (voice)","img":null,"order":10},{"name":"Fuminori Komatsu","character":"Takeuchi-sensei (voice)","img":null,"order":11},{"name":"Ikuko Tani","character":"Ito Nishimiya (voice)","img":null,"order":12},{"name":"Erena Kamata","character":"Maria Ishida (voice)","img":null,"order":13},{"name":"Ayano Hamaguchi","character":"Shouya's Sister (voice)","img":null,"order":14}],"movie:14411":[{"name":"Brad Pitt","character":"Sinbad (voice)","img":null,"order":0},{"name":"Catherine Zeta-Jones","character":"Marina (voice)","img":null,"order":1},{"name":"Michelle Pfeiffer","character":"Eris (voice)","img":null,"order":2},{"name":"Joseph Fiennes","character":"Proteus (voice)","img":null,"order":3},{"name":"Dennis Haysbert","character":"Kale (voice)","img":null,"order":4},{"name":"Timothy West","character":"Dymas (voice)","img":null,"order":5},{"name":"Adriano Giannini","character":"Rat (voice)","img":null,"order":6},{"name":"Raman Hui Shing-Ngai","character":"Jin (voice)","img":null,"order":7},{"name":"Chung Chan","character":"Li (voice)","img":null,"order":8},{"name":"Jim Cummings","character":"Luca / Additional Voices (voice)","img":null,"order":9},{"name":"Conrad Vernon","character":"Jed (voice)","img":null,"order":10},{"name":"Andrew Birch","character":"Grum / Chum (voice)","img":null,"order":11},{"name":"Chris Miller","character":"Tower Guard (voice)","img":null,"order":12},{"name":"Frank Welker","character":"Spike (voice) (uncredited)","img":null,"order":13}],"movie:288036":[{"name":"Jason Sudeikis","character":"Jake Harper","img":null,"order":0},{"name":"Alison Brie","character":"Lainey Dalton","img":null,"order":1},{"name":"Adam Scott","character":"Matthew Sobvechik","img":null,"order":2},{"name":"Jason Mantzoukas","character":"Xander","img":null,"order":3},{"name":"Natasha Lyonne","character":"Kara","img":null,"order":4},{"name":"Adam Brody","character":"Sam","img":null,"order":5},{"name":"Amanda Peet","character":"Paula","img":null,"order":6},{"name":"Katherine Waterston","character":"Emma","img":null,"order":7},{"name":"Marc Blucas","character":"Chris Smith","img":null,"order":8},{"name":"Margarita Levieva","character":"Hannah","img":null,"order":9},{"name":"Andrea Savage","character":"Naomi","img":null,"order":10},{"name":"Daniella Pineda","character":"Danica","img":null,"order":11},{"name":"Remy Nozik","character":"George","img":null,"order":12},{"name":"Megan Guinan","character":"Alice","img":null,"order":13},{"name":"Anna Margaret Hollyman","character":"Renee","img":null,"order":14}],"movie:110415":[{"name":"Chris Evans","character":"Curtis","img":null,"order":0},{"name":"Song Kang-ho","character":"Namgoong Minsu","img":null,"order":1},{"name":"Ed Harris","character":"Wilford","img":null,"order":2},{"name":"John Hurt","character":"Gilliam","img":null,"order":3},{"name":"Tilda Swinton","character":"Mason","img":null,"order":4},{"name":"Jamie Bell","character":"Edgar","img":null,"order":5},{"name":"Octavia Spencer","character":"Tanya","img":null,"order":6},{"name":"Ewen Bremner","character":"Andrew","img":null,"order":7},{"name":"Ko Asung","character":"Yona","img":null,"order":8},{"name":"Alison Pill","character":"Teacher","img":null,"order":9},{"name":"Luke Pasqualino","character":"Grey","img":null,"order":10},{"name":"Vlad Ivanov","character":"Franco The Elder","img":null,"order":11},{"name":"Adnan Haskovi\u0107","character":"Franco The Younger","img":null,"order":12},{"name":"Emma Levie","character":"Claude","img":null,"order":13},{"name":"Steve Park","character":"Fuyu","img":null,"order":14}],"movie:969681":[{"name":"Tom Holland","character":"Peter Parker / Spider-Man","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":0},{"name":"Sadie Sink","character":"Jean Grey","img":null,"order":1},{"name":"Tramell Tillman","character":"William 'Bill' Metzger","img":null,"order":2},{"name":"Zendaya","character":"MJ","img":null,"order":3},{"name":"Jacob Batalon","character":"Ned Leeds","img":null,"order":4},{"name":"Jon Bernthal","character":"Frank Castle / Punisher","img":null,"order":5},{"name":"Mark Ruffalo","character":"Bruce Banner / Hulk","img":null,"order":6},{"name":"Florence Pugh","character":"Yelena Belova / Black Widow","img":null,"order":7},{"name":"Liza Col\u00f3n-Zayas","character":"Detective Jean DeWolff","img":null,"order":8},{"name":"Marisa Tomei","character":"May Parker","img":null,"order":9},{"name":"Naomi Watts","character":"E.V. (voice)","img":null,"order":10},{"name":"Michael Mando","character":"Mac Gargan / Scorpion","img":null,"order":11},{"name":"Keith David","character":"Narrator of Spider Video (voice)","img":null,"order":12},{"name":"Marvin Jones III","character":"Tombstone","img":null,"order":13},{"name":"Olivia Booth-Ford","character":"Sara","img":null,"order":14}],"movie:429617":[{"name":"Tom Holland","character":"Peter Parker / Spider-Man","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":0},{"name":"Jake Gyllenhaal","character":"Quentin Beck / Mysterio","img":null,"order":1},{"name":"Samuel L. Jackson","character":"Nick Fury","img":null,"order":2},{"name":"Marisa Tomei","character":"May Parker","img":null,"order":3},{"name":"Jon Favreau","character":"Happy Hogan","img":null,"order":4},{"name":"Zendaya","character":"MJ","img":null,"order":5},{"name":"Jacob Batalon","character":"Ned Leeds","img":null,"order":6},{"name":"Tony Revolori","character":"Flash Thompson","img":null,"order":7},{"name":"Angourie Rice","character":"Betty Brant","img":null,"order":8},{"name":"Remy Hii","character":"Brad Davis","img":null,"order":9},{"name":"Martin Starr","character":"Mr. Harrington","img":null,"order":10},{"name":"JB Smoove","character":"Mr. Dell","img":null,"order":11},{"name":"Jorge Lendeborg Jr.","character":"Jason Ionello","img":null,"order":12},{"name":"Cobie Smulders","character":"Maria Hill","img":null,"order":13},{"name":"Numan Acar","character":"Dimitri","img":null,"order":14}],"movie:315635":[{"name":"Tom Holland","character":"Peter Parker / Spider-Man","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":0},{"name":"Michael Keaton","character":"Adrian Toomes / Vulture","img":null,"order":1},{"name":"Robert Downey Jr.","character":"Tony Stark / Iron Man","img":null,"order":2},{"name":"Marisa Tomei","character":"May Parker","img":null,"order":3},{"name":"Jon Favreau","character":"Happy Hogan","img":null,"order":4},{"name":"Gwyneth Paltrow","character":"Pepper Potts","img":null,"order":5},{"name":"Zendaya","character":"Michelle","img":null,"order":6},{"name":"Donald Glover","character":"Aaron Davis","img":null,"order":7},{"name":"Jacob Batalon","character":"Ned","img":null,"order":8},{"name":"Laura Harrier","character":"Liz","img":null,"order":9},{"name":"Tony Revolori","character":"Flash","img":null,"order":10},{"name":"Bokeem Woodbine","character":"Herman Schultz / Shocker #2","img":null,"order":11},{"name":"Tyne Daly","character":"Anne Marie Hoag","img":null,"order":12},{"name":"Abraham Attah","character":"Abraham","img":null,"order":13},{"name":"Hannibal Buress","character":"Coach Wilson","img":null,"order":14}],"movie:634649":[{"name":"Tom Holland","character":"Peter Parker / Spider-Man","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":0},{"name":"Zendaya","character":"MJ","img":null,"order":1},{"name":"Benedict Cumberbatch","character":"Doctor Strange","img":null,"order":2},{"name":"Jacob Batalon","character":"Ned Leeds","img":null,"order":3},{"name":"Jon Favreau","character":"Happy Hogan","img":null,"order":4},{"name":"Jamie Foxx","character":"Max Dillon / Electro","img":null,"order":5},{"name":"Willem Dafoe","character":"Norman Osborn / Green Goblin","img":null,"order":6},{"name":"Alfred Molina","character":"Dr. Otto Octavius / Doc Ock","img":null,"order":7},{"name":"Benedict Wong","character":"Wong","img":null,"order":8},{"name":"Tony Revolori","character":"Flash Thompson","img":null,"order":9},{"name":"Marisa Tomei","character":"May Parker","img":null,"order":10},{"name":"Andrew Garfield","character":"Peter Parker / Spider-Man","img":null,"order":11},{"name":"Tobey Maguire","character":"Peter Parker / Spider-Man","img":null,"order":12},{"name":"Angourie Rice","character":"Betty Brant","img":null,"order":13},{"name":"Arian Moayed","character":"Agent Cleary","img":null,"order":14}],"movie:129":[{"name":"Rumi Hiiragi","character":"Chihiro (voice)","img":null,"order":0},{"name":"Miyu Irino","character":"Haku (voice)","img":null,"order":1},{"name":"Mari Natsuki","character":"Yubaba / Zeniba (voice)","img":null,"order":2},{"name":"Takashi Naito","character":"Father (voice)","img":null,"order":3},{"name":"Yasuko Sawaguchi","character":"Mother (voice)","img":null,"order":4},{"name":"Tatsuya Gashuin","character":"Aogaeru (voice)","img":null,"order":5},{"name":"Ryunosuke Kamiki","character":"Boh (voice)","img":null,"order":6},{"name":"Yumi Tamai","character":"Rin (voice)","img":null,"order":7},{"name":"Yo Oizumi","character":"Bandai Frog (voice)","img":null,"order":8},{"name":"Koba Hayashi","character":"River Spirit (voice)","img":null,"order":9},{"name":"Tsunehiko Kamij\u00f4","character":"Chichiyaku (voice)","img":null,"order":10},{"name":"Takehiko Ono","character":"Aniyaku (voice)","img":null,"order":11},{"name":"Bunta Sugawara","character":"Kamaji (voice)","img":null,"order":12},{"name":"Shigeru Wakita","character":"(voice)","img":null,"order":13},{"name":"Shirou Saitou","character":"(voice)","img":null,"order":14}],"movie:933260":[{"name":"Demi Moore","character":"Elisabeth","img":null,"order":0},{"name":"Margaret Qualley","character":"Sue","img":null,"order":1},{"name":"Dennis Quaid","character":"Harvey","img":null,"order":2},{"name":"Edward Hamilton-Clark","character":"Fred","img":null,"order":3},{"name":"Gore Abrams","character":"Oliver","img":null,"order":4},{"name":"Oscar Lesage","character":"Troy","img":null,"order":5},{"name":"Christian Erickson","character":"Man at Diner","img":null,"order":6},{"name":"Robin Greer","character":"Male Nurse","img":null,"order":7},{"name":"Tom Morton","character":"Doctor","img":null,"order":8},{"name":"Hugo Diego Garcia","character":"Diego - Boyfriend","img":null,"order":9},{"name":"Daniel Knight","character":"Casting Director","img":null,"order":10},{"name":"Jonathon Carley","character":"Assistant Casting Director","img":null,"order":11},{"name":"Jiselle Henderkott","character":"Girl Auditioning / New Year's Eve Show Dancer","img":null,"order":12},{"name":"Akil Wingate","character":"TV Host","img":null,"order":13},{"name":"Vincent Colombe","character":"Man in Suit #1 - Harvey's Office","img":null,"order":14}],"movie:149871":[{"name":"Aki Asakura","character":"The Princess Kaguya (voice)","img":null,"order":0},{"name":"Takeo Chii","character":"Sanuki no Miyatsuko (voice)","img":null,"order":1},{"name":"Nobuko Miyamoto","character":"\u014cna / Narrator (voice)","img":null,"order":2},{"name":"Kengo Kora","character":"Sutemaru (voice)","img":null,"order":3},{"name":"Atsuko Takahata","character":"Lady Sagami (voice)","img":null,"order":4},{"name":"Tomoko Tabata","character":"Menowarawa (voice)","img":null,"order":5},{"name":"Shinosuke Tatekawa","character":"Lord Inbe no Akita (voice)","img":null,"order":6},{"name":"Takaya Kamikawa","character":"Prince Ishitsukuri (voice)","img":null,"order":7},{"name":"Hikaru Ij\u016bin","character":"Minister of the Right Abe (voice)","img":null,"order":8},{"name":"Ryudo Uzaki","character":"Grand Counselor Otomo (voice)","img":null,"order":9},{"name":"Shichinosuke Nakamura","character":"The Emperor (voice)","img":null,"order":10},{"name":"Isao Hashizume","character":"Prince Kuramochi (voice)","img":null,"order":11},{"name":"Tatsuya Nakadai","character":"Charcoal-maker Old Man (voice)","img":null,"order":12},{"name":"Yukiji Asaoka","character":"Kita no kata (voice)","img":null,"order":13},{"name":"Tamaki Kojo","character":"Middle Counselor Isonokami (voice)","img":null,"order":14}],"movie:103":[{"name":"Robert De Niro","character":"Travis Bickle","img":null,"order":0},{"name":"Jodie Foster","character":"Iris","img":null,"order":1},{"name":"Cybill Shepherd","character":"Betsy","img":null,"order":2},{"name":"Harvey Keitel","character":"Sport","img":null,"order":3},{"name":"Peter Boyle","character":"Wizard","img":null,"order":4},{"name":"Leonard Harris","character":"Charles Palantine","img":null,"order":5},{"name":"Albert Brooks","character":"Tom","img":null,"order":6},{"name":"Diahnne Abbott","character":"Concession Girl","img":null,"order":7},{"name":"Frank Adu","character":"Angry Black Man","img":null,"order":8},{"name":"Victor Argo","character":"Melio","img":null,"order":9},{"name":"Gino Ardito","character":"Policeman at Rally","img":null,"order":10},{"name":"Garth Avery","character":"Iris' Friend","img":null,"order":11},{"name":"Harry Cohn","character":"Cabbie in Bellmore","img":null,"order":12},{"name":"Cooper Cunningham","character":"Hooker in Cab","img":null,"order":13},{"name":"Brenda Dickson","character":"Soap Opera Woman","img":null,"order":14}],"movie:497828":[{"name":"Harris Dickinson","character":"Carl","img":null,"order":0},{"name":"Charlbi Dean","character":"Yaya","img":null,"order":1},{"name":"Dolly de Leon","character":"Abigail","img":null,"order":2},{"name":"Woody Harrelson","character":"The Captain","img":null,"order":3},{"name":"Zlatko Buri\u0107","character":"Dimitry","img":null,"order":4},{"name":"Vicki Berlin","character":"Paula","img":null,"order":5},{"name":"Henrik Dorsin","character":"Jarmo","img":null,"order":6},{"name":"Iris Berben","character":"Therese","img":null,"order":7},{"name":"Jean-Christophe Folly","character":"Nelson","img":null,"order":8},{"name":"Amanda Walker","character":"Clementine","img":null,"order":9},{"name":"Oliver Ford Davies","character":"Winston","img":null,"order":10},{"name":"Sunnyi Melles","character":"Vera","img":null,"order":11},{"name":"Thobias Thorwid","character":"Lewis","img":null,"order":12},{"name":"Jiannis Moustos","character":"Driver","img":null,"order":13},{"name":"Timoleon Gketsos","character":"Handsome Crewman","img":null,"order":14}],"movie:868":[{"name":"Presley Chweneyagae","character":"Tsotsi","img":null,"order":0},{"name":"Jerry Mofokeng","character":"Morris","img":null,"order":1},{"name":"Terry Pheto","character":"Miriam","img":null,"order":2},{"name":"Zenzo Ngqobe","character":"Butcher","img":null,"order":3},{"name":"Zola","character":"Fela","img":null,"order":4},{"name":"Rapulana Seiphemo","character":"John Dube","img":null,"order":5},{"name":"Nambitha Mpumlwana","character":"Pumla Dube","img":null,"order":6},{"name":"Percy Matsemela","character":"Sergeant Zuma","img":null,"order":7},{"name":"Thembi Nyandeni","character":"Soekie","img":null,"order":8},{"name":"Owen Sejake","character":"Gumboot Dlamini","img":null,"order":9},{"name":"Israel Makoe","character":"Tsotsi's father","img":null,"order":10},{"name":"Sindi Khambule","character":"Tsotsi's Mother","img":null,"order":11},{"name":"Benny Moshe","character":"Young Tsotsi","img":null,"order":12},{"name":"Mothusi Magano","character":"Boston","img":null,"order":13},{"name":"Kenneth Nkosi","character":"Aap","img":null,"order":14}],"movie:1517772":[{"name":"Lauryn Licari","character":"Self","img":null,"order":0},{"name":"Owen McKenny","character":"Self","img":null,"order":1},{"name":"Sophie Weber","character":"Self","img":null,"order":2},{"name":"Macy Johnston","character":"Self","img":null,"order":3},{"name":"Shawn Licari","character":"Self","img":null,"order":4},{"name":"Kendra Licari","character":"Self","img":null,"order":5},{"name":"Dave McKenny","character":"Self","img":null,"order":6},{"name":"Jill McKenny","character":"Self","img":null,"order":7},{"name":"Bill Chillman","character":"Self - The Superintendent","img":null,"order":8},{"name":"Dan Boyer","character":"Self - Principal","img":null,"order":9},{"name":"Khloe Wilson","character":"Self","img":null,"order":10},{"name":"Craig Wilson","character":"Self","img":null,"order":11},{"name":"Tami Wilson","character":"Self","img":null,"order":12},{"name":"Korin Weber","character":"Self","img":null,"order":13},{"name":"Bradley Peter","character":"Self - FBI Liason","img":null,"order":14}],"movie:977942":[{"name":"Andrew Garfield","character":"Ploughman","img":null,"order":0},{"name":"Jamie Bell","character":"John Ball","img":null,"order":1},{"name":"Stephen Dillane","character":"Archbishop Sudbury","img":null,"order":2},{"name":"Woody Norman","character":"King Richard II","img":null,"order":3},{"name":"Tom Hollander","character":"Treasurer Hales","img":null,"order":4},{"name":"Cosmo Jarvis","character":"Wat Tyler","img":null,"order":5},{"name":"Thomasin McKenzie","character":"Alyce","img":null,"order":6},{"name":"Jonny Lee Miller","character":"Mayor Walworth","img":null,"order":7},{"name":"Katherine Waterston","character":"Countess","img":null,"order":8},{"name":"Stanley Townsend","character":"Chief Justice Bealknap","img":null,"order":9},{"name":"Sky Yang","character":"Scribe","img":null,"order":10},{"name":"D\u00f3nal Finn","character":"Village Priest","img":null,"order":11},{"name":"Gerald Tyler","character":"Lord Beauchamp","img":null,"order":12},{"name":"Simon Kunz","character":"Sir John Newton","img":null,"order":13},{"name":"Mark Redhead","character":"Lord Peterborough","img":null,"order":14}],"movie:429197":[{"name":"Christian Bale","character":"Dick Cheney","img":null,"order":0},{"name":"Amy Adams","character":"Lynne Cheney","img":null,"order":1},{"name":"Steve Carell","character":"Donald Rumsfeld","img":null,"order":2},{"name":"Sam Rockwell","character":"George W. Bush","img":null,"order":3},{"name":"Alison Pill","character":"Mary Cheney","img":null,"order":4},{"name":"Eddie Marsan","character":"Paul Wolfowitz","img":null,"order":5},{"name":"Justin Kirk","character":"Scooter Libby","img":null,"order":6},{"name":"LisaGay Hamilton","character":"Condoleezza Rice","img":null,"order":7},{"name":"Jesse Plemons","character":"Kurt","img":null,"order":8},{"name":"Bill Camp","character":"Gerald Ford","img":null,"order":9},{"name":"Don McManus","character":"David Addington","img":null,"order":10},{"name":"Lily Rabe","character":"Liz Cheney","img":null,"order":11},{"name":"Shea Whigham","character":"Wayne Vincent","img":null,"order":12},{"name":"Stephen Adly Guirgis","character":"George Tenet","img":null,"order":13},{"name":"Tyler Perry","character":"Colin Powell","img":null,"order":14}],"movie:81":[{"name":"Sumi Shimamoto","character":"Nausica\u00e4 (voice)","img":null,"order":0},{"name":"Ichiro Nagai","character":"Mito (voice)","img":null,"order":1},{"name":"Gor\u014d Naya","character":"Yupa (voice)","img":null,"order":2},{"name":"Yoji Matsuda","character":"Asbel (voice)","img":null,"order":3},{"name":"Yoshiko Sakakibara","character":"Kushana (voice)","img":null,"order":4},{"name":"Iemasa Kayumi","character":"Kurotowa (voice)","img":null,"order":5},{"name":"Hisako Kyoda","character":"Obaba (voice)","img":null,"order":6},{"name":"Mahito Tsujimura","character":"Jihl (voice)","img":null,"order":7},{"name":"Mina Tominaga","character":"Lastel (voice)","img":null,"order":8},{"name":"Kohei Miyauchi","character":"Gol (voice)","img":null,"order":9},{"name":"Joji Yanami","character":"Gikkuri (voice)","img":null,"order":10},{"name":"Minoru Yada","character":"Niga (voice)","img":null,"order":11},{"name":"Rihoko Yoshida","character":"Teto / Girl C (voice)","img":null,"order":12},{"name":"Tetsuo Mizutori","character":"Commando (voice)","img":null,"order":13},{"name":"Masako Sugaya","character":"Girl A (voice)","img":null,"order":14}],"movie:1198984":[{"name":"Daisy Ridley","character":"Ava","img":null,"order":0},{"name":"Brenton Thwaites","character":"Clay","img":null,"order":1},{"name":"Mark Coles Smith","character":"Riley","img":null,"order":2},{"name":"Matt Whelan","character":"Mitch","img":null,"order":3},{"name":"Chloe Hurst","character":"Katie Harris","img":null,"order":4},{"name":"Kym Jackson","character":"Lt. Wilkie","img":null,"order":5},{"name":"Holly Hargreaves","character":"Mrs. Smith","img":null,"order":6},{"name":"Deanna Cooney","character":"Bianca","img":null,"order":7},{"name":"Elijah Williams","character":"Greg","img":null,"order":8},{"name":"Salme Geransar","character":"Private Clarkson","img":null,"order":9},{"name":"Luke Jai McIntosh","character":"Cadet","img":null,"order":10},{"name":"Joel Jackson","character":"Anxious Bus Volunteer","img":null,"order":11},{"name":"Dan Paris","character":"Captain Vance","img":null,"order":12},{"name":"Nicola Bartlett","character":"Woman on Plane","img":null,"order":13},{"name":"Kim Fleming","character":"Briefing Colonel","img":null,"order":14}],"movie:953371":[{"name":"Benjamin O'Keefe","character":"Self - Activist & Public Speaker","img":null,"order":0},{"name":"Bobby Blanski","character":"Self - Former A&F Model","img":null,"order":1},{"name":"Ryan Daharsh","character":"Self - Former A&F Model","img":null,"order":2},{"name":"Jennifer Sheahan","character":"Self - Former Store Employee","img":null,"order":3},{"name":"Dr. Anthony Ocampo","character":"Self - Former Store Employee","img":null,"order":4},{"name":"Sapna Maheshwari","character":"Self - Business Reporter, NY Times","img":null,"order":5},{"name":"Alan Karo","character":"Self - Fashion Marketing & Advertising Executive","img":null,"order":6},{"name":"Treva Lindsey","character":"Self - Professor of History, Ohio State University","img":null,"order":7},{"name":"Carla Barrientos","character":"Self - Former Store Employee","img":null,"order":8},{"name":"Robin Givhan","character":"Self - Senior Critic-at-Large, Washington Post","img":null,"order":9},{"name":"Jose Sanchez","character":"Self - Former A&F Recruiter","img":null,"order":10},{"name":"Moe Tkacik","character":"Self - Journalist","img":null,"order":11},{"name":"Mike Jeffries","character":"Self - Former A&F CEO (archive footage)","img":null,"order":12},{"name":"Charles Martin","character":"Self - Former A&F Design VP","img":null,"order":13},{"name":"Cindy Smith-Maglione","character":"Self - Former A&F VP of Merchandising","img":null,"order":14}],"movie:1184918":[{"name":"Lupita Nyong'o","character":"Roz / Rummage (voice)","img":null,"order":0},{"name":"Pedro Pascal","character":"Fink (voice)","img":null,"order":1},{"name":"Kit Connor","character":"Brightbill (voice)","img":null,"order":2},{"name":"Bill Nighy","character":"Longneck (voice)","img":null,"order":3},{"name":"Stephanie Hsu","character":"Vontra (voice)","img":null,"order":4},{"name":"Matt Berry","character":"Paddler (voice)","img":null,"order":5},{"name":"Ving Rhames","character":"Thunderbolt (voice)","img":null,"order":6},{"name":"Mark Hamill","character":"Thorn (voice)","img":null,"order":7},{"name":"Catherine O'Hara","character":"Pinktail (voice)","img":null,"order":8},{"name":"Boone Storm","character":"Baby Brightbill (voice)","img":null,"order":9},{"name":"Alexandra Novelle","character":"Snowdown (voice)","img":null,"order":10},{"name":"Raphael Alejandro","character":"Peck (voice)","img":null,"order":11},{"name":"Paul-Mik\u00e9l Williams","character":"Feather (voice)","img":null,"order":12},{"name":"Eddie Park","character":"Honkington (voice)","img":null,"order":13},{"name":"Dee Bradley Baker","character":"Raccoons / Squirrels / Badgers (voice)","img":null,"order":14}],"movie:149870":[{"name":"Hideaki Anno","character":"Jiro Horikoshi (voice)","img":null,"order":0},{"name":"Hidetoshi Nishijima","character":"Honj\u00f4 (voice)","img":null,"order":1},{"name":"Miori Takimoto","character":"Naoko Satomi (voice)","img":null,"order":2},{"name":"Masahiko Nishimura","character":"Kurokawa (voice)","img":null,"order":3},{"name":"Stephen Alpert","character":"Castorp (voice)","img":null,"order":4},{"name":"Mansai Nomura","character":"Giovanni Battista (voice)","img":null,"order":5},{"name":"Jun Kunimura","character":"Hattori (voice)","img":null,"order":6},{"name":"Mirai Shida","character":"Kayo Horikoshi (voice)","img":null,"order":7},{"name":"Shinobu Otake","character":"Mrs. Kurokawa (voice)","img":null,"order":8},{"name":"Morio Kazama","character":"Satomi (voice)","img":null,"order":9},{"name":"Keiko Takeshita","character":"Jir\u00f4's mother (voice)","img":null,"order":10},{"name":"Sascha","character":"German Security Guard / Flight Engineer (voice)","img":null,"order":11},{"name":"Kaichi Kaburagi","character":"Young Jiro Horikoshi (voice)","img":null,"order":12},{"name":"Maki Shinta","character":"Young Kayo Horikoshi (voice)","img":null,"order":13},{"name":"Berndt Otto","character":"(voice)","img":null,"order":14}],"movie:666243":[{"name":"Theo James","character":"Vesemir (voice)","img":null,"order":0},{"name":"Mary McDonnell","character":"Lady Zerbst (voice)","img":null,"order":1},{"name":"Lara Pulver","character":"Tetra Gilcrest (voice)","img":null,"order":2},{"name":"Graham McTavish","character":"Deglan (voice)","img":null,"order":3},{"name":"Tom Canton","character":"Filavandrel (voice)","img":null,"order":4},{"name":"David Errigo Jr.","character":"Young Vesemir / Young Remus (voice)","img":null,"order":5},{"name":"Jennifer Hale","character":"Illyana / Noblewoman (voice)","img":null,"order":6},{"name":"Kari Wahlgren","character":"Kitsu / Boy (voice)","img":null,"order":7},{"name":"Matthew Yang King","character":"Luka / Skogar (voice)","img":null,"order":8},{"name":"Darryl Kurylo","character":"Sven / Nobleman (voice)","img":null,"order":9},{"name":"Keith Ferguson","character":"Lord Carlisle / Reidrich (voice)","img":null,"order":10},{"name":"Jennie Kwan","character":"Young Luka (voice)","img":null,"order":11},{"name":"AJ LoCascio","character":"Young Sven (voice)","img":null,"order":12},{"name":"Michaela Dietz","character":"Tomas (voice)","img":null,"order":13},{"name":"Harry Hissrich","character":"Young Boy (Geralt) (voice)","img":null,"order":14}],"movie:1203329":[{"name":"Doug Cockle","character":"Geralt (voice)","img":null,"order":0},{"name":"Joey Batey","character":"Jaskier / Young Jaskier (voice)","img":null,"order":1},{"name":"Christina Wren","character":"Essi (voice)","img":null,"order":2},{"name":"Emily Carey","character":"Sh'eenaz (voice)","img":null,"order":3},{"name":"Camrus Johnson","character":"Agloval (voice)","img":null,"order":4},{"name":"Simon Templeman","character":"King Usveldt (voice)","img":null,"order":5},{"name":"Mallory Jansen","character":"Melusina / Princess Nispen (voice)","img":null,"order":6},{"name":"Anya Chalotra","character":"Yennefer (voice)","img":null,"order":7},{"name":"Ray Chase","character":"Zelest (voice)","img":null,"order":8},{"name":"Cynthia Kaye McWilliams","character":"Queen Dahut (voice)","img":null,"order":9},{"name":"Ramon Tikaram","character":"King Basim (voice)","img":null,"order":10},{"name":"Kari Wahlgren","character":"Zmarra / Vodnik Assassin (voice)","img":null,"order":11},{"name":"Stephen Fu","character":"Deroua (voice)","img":null,"order":12},{"name":"George Ackles","character":"Drouhard (voice)","img":null,"order":13},{"name":"Brian George","character":"Barkeep / Herald (voice)","img":null,"order":14}],"tv:260458":[{"name":"Malik Elassal","character":"Samir Rahman","img":null,"order":0},{"name":"Lucy Freyer","character":"Billie Schaeffer","img":null,"order":1},{"name":"Jack Innanen","character":"Paul Baker","img":null,"order":2},{"name":"Amita Rao","character":"Issa Damani","img":null,"order":3},{"name":"Owen Thiele","character":"Anton Evans","img":null,"order":4},{"name":"Julia Fox","character":"Julia Fox","img":null,"order":500},{"name":"Rachel Marsh","character":"Carly","img":null,"order":502},{"name":"Charlie Cox","character":"Mr. Teacher","img":null,"order":521},{"name":"Julian Manjerico","character":"Zack-Carlos","img":null,"order":576},{"name":"Martha Girvin","character":"Serena","img":null,"order":582},{"name":"Andrea Pavlovic","character":"Lily","img":null,"order":585},{"name":"Meghan Swaby","character":"Mary","img":null,"order":587},{"name":"Liam Cullagh","character":"OJ","img":null,"order":594},{"name":"Will Sennett","character":"Roach","img":null,"order":595},{"name":"Joey Dardano","character":"Schnee","img":null,"order":596}],"tv:64513":[{"name":"Sterling K. Brown","character":"Christopher Darden","img":null,"order":0},{"name":"Kenneth Choi","character":"Lance Ito","img":null,"order":3},{"name":"Beanie Feldstein","character":"Monica Lewinsky","img":null,"order":5},{"name":"Christian Clemenson","character":"Bill Hodgman","img":null,"order":7},{"name":"Sarah Paulson","character":"Linda Tripp","img":null,"order":9},{"name":"Cuba Gooding Jr.","character":"O. J. Simpson","img":null,"order":9},{"name":"Margo Martindale","character":"Lucianne Goldberg","img":null,"order":11},{"name":"Bruce Greenwood","character":"Gil Garcetti","img":null,"order":12},{"name":"Edie Falco","character":"Hillary Clinton","img":null,"order":13},{"name":"Nathan Lane","character":"F. Lee Bailey","img":null,"order":14},{"name":"Clive Owen","character":"Bill Clinton","img":null,"order":15},{"name":"David Schwimmer","character":"Robert Kardashian","img":null,"order":18},{"name":"John Travolta","character":"Robert Shapiro","img":null,"order":19},{"name":"Courtney B. Vance","character":"Johnnie Cochran","img":null,"order":20},{"name":"Annaleigh Ashford","character":"Elizabeth Cote","img":null,"order":398}],"tv:215866":[{"name":"Taylor Kitsch","character":"Isaac Reed","img":null,"order":0},{"name":"Betty Gilpin","character":"Sara Rowell","img":null,"order":1},{"name":"Dane DeHaan","character":"Jacob Pratt","img":null,"order":2},{"name":"Saura Lightfoot-Leon","character":"Abish Pratt","img":null,"order":3},{"name":"Derek Hinkey","character":"Red Feather","img":null,"order":4},{"name":"Joe Tippett","character":"James Wolsey","img":null,"order":5},{"name":"Jai Courtney","character":"Virgil Cutter","img":null,"order":6},{"name":"Preston Mota","character":"Devin Rowell","img":null,"order":7},{"name":"Shawnee Pourier","character":"Two Moons","img":null,"order":8},{"name":"Shea Whigham","character":"Jim Bridger","img":null,"order":9},{"name":"Kyle Davis","character":"Tilly","img":null,"order":500},{"name":"Tokala Black Elk","character":"Buffalo Run","img":null,"order":501},{"name":"Nick Hargrove","character":"Cottrell","img":null,"order":502},{"name":"Alex Fine","character":"Gant","img":null,"order":504},{"name":"Andrew P. Logan","character":"Lucas","img":null,"order":506}],"tv:4589":[{"name":"Jason Bateman","character":"Michael Bluth","img":null,"order":0},{"name":"Portia de Rossi","character":"Lindsay Bluth F\u00fcnke","img":null,"order":1},{"name":"Will Arnett","character":"Gob Bluth","img":null,"order":2},{"name":"Michael Cera","character":"George Michael Bluth","img":null,"order":3},{"name":"Alia Shawkat","character":"Maeby F\u00fcnke","img":null,"order":4},{"name":"Tony Hale","character":"Byron 'Buster' Bluth","img":null,"order":5},{"name":"David Cross","character":"Tobias F\u00fcnke","img":null,"order":6},{"name":"Jeffrey Tambor","character":"George Bluth Sr.","img":null,"order":7},{"name":"Jessica Walter","character":"Lucille Bluth","img":null,"order":8},{"name":"Ron Howard","character":"Narrator","img":null,"order":9},{"name":"Justin Grant Wade","character":"Steve Holt","img":null,"order":508},{"name":"Liza Minnelli","character":"Lucille Austero","img":null,"order":520},{"name":"Mae Whitman","character":"Ann Veal","img":null,"order":533},{"name":"John Beard","character":"John Beard","img":null,"order":609},{"name":"Henry Winkler","character":"Barry Zuckerkorn","img":null,"order":635}],"tv:246":[{"name":"Zach Tyler Eisen","character":"Aang (voice)","img":null,"order":0},{"name":"Mae Whitman","character":"Katara (voice)","img":null,"order":1},{"name":"Jack De Sena","character":"Sokka (voice)","img":null,"order":2},{"name":"Dante Basco","character":"Zuko (voice)","img":null,"order":3},{"name":"Mako","character":"Iroh (voice)","img":null,"order":4},{"name":"Dee Bradley Baker","character":"Appa / Momo (voice)","img":null,"order":7},{"name":"Cricket Leigh","character":"Mai (voice)","img":null,"order":9},{"name":"Olivia Hack","character":"Ty Lee (voice)","img":null,"order":10},{"name":"Michaela Jill Murphy","character":"Toph Beifong (voice)","img":null,"order":173},{"name":"Andr\u00e9 Sogliuzzo","character":"Captain (voice)","img":null,"order":378},{"name":"Grey DeLisle","character":"Azula / Additional Voices (voice)","img":null,"order":491},{"name":"Greg Baldwin","character":"Iroh / Combustion Man (voice)","img":null,"order":545},{"name":"Mark Hamill","character":"Ozai (voice)","img":null,"order":581},{"name":"Jennie Kwan","character":"Suki (voice)","img":null,"order":624},{"name":"Jim Meskimen","character":"Avatar Kuruk (voice)","img":null,"order":657}],"tv:124428":[{"name":"Jeroen Kijk in de Vegte","character":"Voice-over","img":null,"order":0},{"name":"Martijn Bolhoeve","character":"Self - Owner of a B&B in Hua Hin, Thailand","img":null,"order":2},{"name":"Joy van Swieten","character":"Self - Owner of a B&B in Malaga, Spain","img":null,"order":3},{"name":"Marian Trott","character":"Self - Owner of a B&B in Budens, Portugal","img":null,"order":4},{"name":"Bram Eijgenhuijsen","character":"Self - Owner of a B&B in Norrhult, Sweden","img":null,"order":5},{"name":"Debbie de Jong","character":"Self - Owner of a B&B in Sant Andreu del Terri, Spain","img":null,"order":6},{"name":"Leendert Wijland","character":"Self - Owner of a B&B in La Roche-En-Ardenne, Belgium","img":null,"order":7},{"name":"Petri Damen","character":"Self - Owner of a B&B in Monclar, France","img":null,"order":8},{"name":"Walter Spangenberg","character":"Self - Owner of a B&B in Troche, France","img":null,"order":9},{"name":"Iris Villas","character":"Self - Owner of a B&B in Sparto, Greece","img":null,"order":13},{"name":"Robert Jan van Rheenen","character":"Self - Owner of a B&B in Dornoch, Scotland","img":null,"order":14},{"name":"Arno van Wilpe","character":"Self - Owner of a B&B in Payzac, France","img":null,"order":15},{"name":"Mike Verkoelen","character":"Self - Owner of a B&B in Ma\u00e7anet de la Selva, Spain","img":null,"order":16},{"name":"Paul Visser","character":"Self - Partner of Debbie de Jong","img":null,"order":48},{"name":"Dani Zijlstra","character":"Self - Partner of Joy van Swieten","img":null,"order":49}],"tv:136315":[{"name":"Jeremy Allen White","character":"Carmen 'Carmy' Berzatto","img":null,"order":0},{"name":"Ebon Moss-Bachrach","character":"Richard 'Richie' Jerimovich","img":null,"order":1},{"name":"Ayo Edebiri","character":"Sydney Adamu","img":null,"order":2},{"name":"Lionel Boyce","character":"Marcus Brooks","img":null,"order":3},{"name":"Abby Elliott","character":"Natalie 'Sugar' Berzatto","img":null,"order":4},{"name":"Matty Matheson","character":"Neil Fak","img":null,"order":5},{"name":"Liza Col\u00f3n-Zayas","character":"Bettina 'Tina' Marrero","img":null,"order":6},{"name":"Edwin Lee Gibson","character":"Ebraheim","img":null,"order":8},{"name":"Oliver Platt","character":"Uncle Jimmy 'Cicero' Kalinowski","img":null,"order":500},{"name":"Christopher Zucchero","character":"Chi-Chi","img":null,"order":505},{"name":"Chris Witaske","character":"Pete Katinsky","img":null,"order":513},{"name":"Corey Hendrix","character":"Gary 'Sweeps' Woods","img":null,"order":536},{"name":"Jose M. Cervantes","character":"Angel","img":null,"order":537},{"name":"Richard Esteras","character":"Manny","img":null,"order":538},{"name":"Ricky Staffieri","character":"Theodore Fak","img":null,"order":548}],"tv:74313":[{"name":"David Attenborough","character":"Self - Presenter","img":null,"order":0}],"tv:93290":[{"name":"Martin Freeman","character":"Paul Worsley","img":null,"order":0},{"name":"Daisy Haggard","character":"Ally Grant","img":null,"order":1},{"name":"Eve Prenelle","character":"Ava Worsley","img":null,"order":4},{"name":"Alex Eastwood","character":"Luke Worsley","img":null,"order":5},{"name":"Oscar Kennedy","character":"Luke Worsley","img":null,"order":6},{"name":"Zo\u00eb Athena","character":"Ava Worsley","img":null,"order":7},{"name":"George Wakeman","character":"Luke Worsley","img":null,"order":345},{"name":"Jayda Eyles","character":"Young Ava","img":null,"order":346},{"name":"Tim Steed","character":"Carl","img":null,"order":501},{"name":"Patrick Baladi","character":"Darren","img":null,"order":504},{"name":"Alun Armstrong","character":"Jim","img":null,"order":514},{"name":"Joanna Bacon","character":"Jackie","img":null,"order":515},{"name":"Stella Gonet","character":"Leah","img":null,"order":516},{"name":"Hugh Quarshie","character":"Alex","img":null,"order":519},{"name":"Aliyah Sesay","character":"Grace","img":null,"order":520}],"tv:87108":[{"name":"Jared Harris","character":"Valery Legasov","img":null,"order":0},{"name":"Stellan Skarsg\u00e5rd","character":"Boris Shcherbina","img":"mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg","order":1},{"name":"Emily Watson","character":"Ulana Khomyuk","img":null,"order":2},{"name":"Paul Ritter","character":"Anatoly Dyatlov","img":null,"order":3},{"name":"Jessie Buckley","character":"Lyudmilla Ignatenko","img":null,"order":4},{"name":"Adam Nagaitis","character":"Vasily Ignatenko","img":null,"order":5},{"name":"Sam Troughton","character":"Alexandr Akimov","img":null,"order":6},{"name":"Robert Emms","character":"Leonid Toptunov","img":null,"order":7},{"name":"Con O'Neill","character":"Viktor Bryukhanov","img":null,"order":8},{"name":"Adrian Rawlins","character":"Nikolai Fomin","img":null,"order":9},{"name":"Alan Williams","character":"KGB Chairman Charkov","img":null,"order":10},{"name":"David Dencik","character":"Mikhail Gorbachev","img":null,"order":11},{"name":"Mark Lewis Jones","character":"General Pikalov","img":null,"order":12},{"name":"Ralph Ineson","character":"General Tarakanov","img":null,"order":13},{"name":"Barry Keoghan","character":"Pavel","img":null,"order":14}],"tv:18347":[{"name":"Joel McHale","character":"Jeff Winger","img":null,"order":0},{"name":"Gillian Jacobs","character":"Britta Perry","img":null,"order":1},{"name":"Alison Brie","character":"Annie Edison","img":null,"order":2},{"name":"Donald Glover","character":"Troy Barnes","img":null,"order":3},{"name":"Yvette Nicole Brown","character":"Shirley Bennett","img":null,"order":4},{"name":"Jim Rash","character":"Dean Craig Pelton","img":null,"order":5},{"name":"Danny Pudi","character":"Abed Nadir","img":null,"order":6},{"name":"Chevy Chase","character":"Pierce Hawthorne","img":null,"order":7},{"name":"Ken Jeong","character":"Ben Chang","img":null,"order":10},{"name":"John Oliver","character":"Professor Ian Duncan","img":null,"order":154},{"name":"Charley Koontz","character":"Neil","img":null,"order":1003},{"name":"Danielle Kaplowitz","character":"Vicki","img":null,"order":1034},{"name":"Erik Charles Nielsen","character":"Garrett Lambert","img":null,"order":1035},{"name":"Dino Stamatopoulos","character":"Star-Burns","img":null,"order":1046},{"name":"Richard Erdman","character":"Leonard","img":null,"order":1049}],"tv:289517":[{"name":"Lee Jae-wook","character":"Byeon Hyeok-jun","img":null,"order":0},{"name":"Go Min-si","character":"Eum Yeon-joo","img":null,"order":1},{"name":"Kim Min-ha","character":"Byeon Ji-yoon","img":null,"order":2},{"name":"Lee Hee-jun","character":"Head of Spider Staffing Agency","img":null,"order":3},{"name":"Choi Hyun-wook","character":"Choi Hee-dong","img":null,"order":500},{"name":"Yoon Kyung-ho","character":"Jo Jun-seop","img":null,"order":501},{"name":"Cho Hyun-chul","character":"Cinephile","img":null,"order":502},{"name":"Yoon Byung-hee","character":"Theater Manager","img":null,"order":503},{"name":"Seo Jae-hee","character":"Director of Yangcheon Bio Clinic","img":null,"order":504},{"name":"Ko Chang-seok","character":"Foreman","img":null,"order":505},{"name":"Kim Si-eun","character":"Woman on the bridge","img":null,"order":506}],"tv:110695":[{"name":"Michael Keaton","character":"Samuel Finnix","img":null,"order":0},{"name":"Peter Sarsgaard","character":"Rick Mountcastle","img":null,"order":1},{"name":"Michael Stuhlbarg","character":"Richard Sackler","img":null,"order":2},{"name":"Will Poulter","character":"Billy Cutler","img":null,"order":3},{"name":"John Hoogenakker","character":"Randy Ramseyer","img":null,"order":4},{"name":"Kaitlyn Dever","character":"Betsy Mallum","img":null,"order":5},{"name":"Rosario Dawson","character":"Bridget Meyer","img":null,"order":6},{"name":"Will Chase","character":"Michael Friedman","img":null,"order":7},{"name":"Cleopatra Coleman","character":"Grace Pell","img":null,"order":8},{"name":"Parker Sack","character":"Dirk","img":null,"order":9},{"name":"Mare Winningham","character":"Diane Mallum","img":null,"order":504},{"name":"Ian Unterman","character":"Jonathan Sackler","img":null,"order":505},{"name":"Jake McDorman","character":"John Brownlee","img":null,"order":506},{"name":"Phillipa Soo","character":"Amber Collins","img":null,"order":507},{"name":"Brendan Patrick Connor","character":"Howard Udell","img":null,"order":508}],"tv:90228":[{"name":"Emily Watson","character":"Mother Superior Valya Harkonnen","img":null,"order":0},{"name":"Olivia Williams","character":"Reverend Mother Tula Harkonnen","img":null,"order":1},{"name":"Travis Fimmel","character":"Desmond Hart","img":null,"order":2},{"name":"Jodhi May","character":"Empress Natalya Arat Corrino","img":null,"order":3},{"name":"Sarah-Sofie Boussnina","character":"Princess Ynez Corrino","img":null,"order":4},{"name":"Chloe Lea","character":"Sister Lila","img":null,"order":5},{"name":"Chris Mason","character":"Keiran Atreides","img":null,"order":6},{"name":"Shalom Brune-Franklin","character":"Mikaela","img":null,"order":7},{"name":"Jade Anouka","character":"Sister Theodosia","img":null,"order":9},{"name":"Edward Davis","character":"Harrow Harkonnen","img":null,"order":10},{"name":"Josh Heuston","character":"Constantine Corrino","img":null,"order":11},{"name":"Faoileann Cunningham","character":"Sister Jen","img":null,"order":12},{"name":"Aoife Hinds","character":"Sister Emeline","img":null,"order":13},{"name":"Indira Varma","character":"","img":null,"order":14},{"name":"Ashley Walters","character":"","img":null,"order":15}],"tv:258165":[{"name":"Florence Pugh","character":"Cathy Ames","img":null,"order":0},{"name":"Christopher Abbott","character":"Adam Trask","img":null,"order":1},{"name":"Mike Faist","character":"Charles Trask","img":null,"order":2},{"name":"Joseph Zada","character":"Caleb Trask","img":null,"order":253},{"name":"Robert Morgan","character":"Sheriff Quinn","img":null,"order":500},{"name":"Christopher Sommers","character":"Mr. Bacon","img":null,"order":501},{"name":"Brad McMurray","character":"Joe Valery","img":null,"order":502},{"name":"Vaughan Reilly","character":"Adolescent Cathy","img":null,"order":503},{"name":"Hoon Lee","character":"Lee","img":null,"order":505},{"name":"Ciar\u00e1n Hinds","character":"Samuel Hamilton","img":null,"order":506},{"name":"Tandi Wright","character":"Mrs. Garb","img":null,"order":507},{"name":"Anita Hegh","character":"Liza Hamilton","img":null,"order":508},{"name":"Oscar Redding","character":"Rabbit Holman","img":null,"order":509},{"name":"Amelia Reynolds","character":"Ethel","img":null,"order":510},{"name":"Jonah Wren Phillips","character":"Young Aron Trask","img":null,"order":512}],"tv:106379":[{"name":"Ella Purnell","character":"Lucy MacLean","img":null,"order":0},{"name":"Aaron Moten","character":"Maximus","img":null,"order":1},{"name":"Mois\u00e9s Arias","character":"Norm MacLean","img":null,"order":2},{"name":"Frances Turner","character":"Barb Howard","img":null,"order":3},{"name":"Kyle MacLachlan","character":"Overseer Hank MacLean","img":null,"order":4},{"name":"Walton Goggins","character":"The Ghoul / Cooper Howard","img":null,"order":5},{"name":"Leslie Uggams","character":"Betty Pearson","img":null,"order":501},{"name":"Michael Cristofer","character":"Elder Cleric Quintus","img":null,"order":502},{"name":"Johnny Pemberton","character":"Thaddeus","img":null,"order":503},{"name":"Zach Cherry","character":"Woody Thomas","img":null,"order":506},{"name":"Annabel O'Hagan","character":"Stephanie Harper","img":null,"order":507},{"name":"Dave Register","character":"Chet","img":null,"order":508},{"name":"Rodrigo Luzzi","character":"Reg McPhee","img":null,"order":509},{"name":"Leer Leary","character":"Davey","img":null,"order":515},{"name":"Amir Carr","character":"Young Maximus","img":null,"order":527}],"tv:2207":[{"name":"John Cleese","character":"Basil Fawlty","img":null,"order":0},{"name":"Prunella Scales","character":"Sybil Fawlty","img":null,"order":1},{"name":"Connie Booth","character":"Polly Sherman","img":null,"order":2},{"name":"Andrew Sachs","character":"Manuel","img":null,"order":3},{"name":"Ballard Berkeley","character":"Major Gowen","img":null,"order":4},{"name":"Brian Hall","character":"Terry the Chef","img":null,"order":5},{"name":"Gilly Flower","character":"Miss Tibbs","img":null,"order":6},{"name":"Renee Roberts","character":"Miss Gatsby","img":null,"order":7},{"name":"Michael Gwynn","character":"Lord Melbury","img":null,"order":500},{"name":"Diana King","character":"Mrs. Rachel Lloyd","img":null,"order":501},{"name":"David Kelly","character":"O'Reilly","img":null,"order":502},{"name":"Brenda Cowling","character":"Sister","img":null,"order":503},{"name":"Elizabeth Benson","character":"Mrs. Heath","img":null,"order":540},{"name":"Terence Conoley","character":"Mr. Johnston","img":null,"order":548},{"name":"George Lee","character":"Delivery Man","img":null,"order":565}],"tv:290720":[{"name":"Jose Coronado","character":"Basil","img":null,"order":0},{"name":"Mar\u00eda Valverde","character":"Adela Figueora","img":null,"order":1},{"name":"Marti\u00f1o Rivas","character":"Fox\u00e1","img":null,"order":2},{"name":"Maribel Verd\u00fa","character":"Raquel Auslander","img":null,"order":3},{"name":"Gonzalo de Castro","character":"Fonseca","img":null,"order":4},{"name":"Cristina Kovani","character":"Vicky","img":null,"order":5},{"name":"Jos\u00e9 Condessa","character":"Toni","img":null,"order":6},{"name":"Pep\u00f3n Nieto","character":"Seruya","img":null,"order":7},{"name":"Daniel Holgu\u00edn","character":"Gerard","img":null,"order":8},{"name":"Michael Stobbe","character":"Se\u00f1or Klemmer","img":null,"order":9},{"name":"Yaiza Guimar\u00e9","character":"Se\u00f1ora Klemmer","img":null,"order":10},{"name":"Almudena Salort","character":"Catalina","img":null,"order":11},{"name":"Paloma Bloyd","character":"Elisa","img":null,"order":12}],"tv:243316":[{"name":"Tina Fey","character":"Kate","img":null,"order":0},{"name":"Steve Carell","character":"Nick Pagano","img":null,"order":1},{"name":"Colman Domingo","character":"Danny","img":null,"order":2},{"name":"Erika Henningsen","character":"Ginny","img":null,"order":3},{"name":"Kerri Kenney","character":"Anne Pagano","img":null,"order":4},{"name":"Will Forte","character":"Jack","img":null,"order":5},{"name":"Marco Calvani","character":"Claude","img":null,"order":6},{"name":"Julia Lester","character":"Lila","img":null,"order":502},{"name":"Alan Alda","character":"Don","img":null,"order":503},{"name":"Ashlyn Maddox","character":"Beth","img":null,"order":509},{"name":"Eric Elizaga","character":"Desk Clerk","img":null,"order":513},{"name":"Toby Huss","character":"Terry","img":null,"order":519},{"name":"Blake Russell","character":"State Trooper","img":null,"order":528},{"name":"Steven Pasquale","character":"Mark Brett","img":null,"order":531},{"name":"Brielle Pribbernow","character":"Boardwalk Kid","img":null,"order":532}],"tv:65345":[{"name":"Justice Smith","character":"Ezekiel Figuero","img":null,"order":0},{"name":"Shameik Moore","character":"Shaolin Fantastic","img":null,"order":1},{"name":"Herizen F. Guardiola","character":"Mylene Cruz","img":null,"order":2},{"name":"Skylan Brooks","character":"Ra-Ra Kipling","img":null,"order":3},{"name":"Jaden Smith","character":"Marcus 'Dizzee' Kipling","img":null,"order":4},{"name":"Tremaine Brown Jr.","character":"Boo-Boo Kipling","img":null,"order":5},{"name":"Yahya Abdul-Mateen II","character":"Clarence 'Cadillac' Caldwell","img":null,"order":6},{"name":"Jimmy Smits","character":"Francisco 'Papa Fuerte' Cruz","img":null,"order":7},{"name":"Stefan\u00e9e Martin","character":"Yolanda Kipling","img":null,"order":8},{"name":"Shyrley Rodriguez","character":"Regina Diaz","img":null,"order":9},{"name":"Mamoudou Athie","character":"Grandmaster Flash","img":null,"order":10},{"name":"Giancarlo Esposito","character":"Pastor Ramon Cruz","img":null,"order":11},{"name":"Ron Cephas Jones","character":"Winston Kipling","img":null,"order":505},{"name":"RayJonaldy Rodriguez","character":"Silent Carlito","img":null,"order":506},{"name":"Zuri Reed","character":"Zulu Girl Yvonne","img":null,"order":508}],"tv:204304":[{"name":"Michael Pollan","character":"Self","img":null,"order":0},{"name":"Ann Shulgin","character":"Self - Author, Mentor & Guide Therapist","img":null,"order":500},{"name":"Alexander Shulgin","character":"Self - Chemist, Author & Professor (archive footage)","img":null,"order":501},{"name":"Rick Doblin","character":"Self - Founder, MAPS","img":null,"order":502},{"name":"Paul F. Daley","character":"Self","img":null,"order":503}],"tv:96488":[{"name":"Peter Sarsgaard","character":"David Russell","img":null,"order":0},{"name":"Kyle Gallner","character":"Eric Fisher","img":null,"order":1},{"name":"David Strathairn","character":"Henry Fisher","img":null,"order":2},{"name":"Kodi Smit-McPhee","character":"Chris Keller","img":null,"order":3},{"name":"Jersey Johnston","character":"Debbie Russell","img":null,"order":533},{"name":"Frank Whaley","character":"Detective James Connor","img":null,"order":577},{"name":"Ellen Humphreys","character":"Sharon Russell","img":null,"order":578},{"name":"Melinda McGraw","character":"Faith Turner","img":null,"order":579},{"name":"Chad L. Coleman","character":"Mr. Franklin","img":null,"order":604},{"name":"Joanna Going","character":"Mrs. Fisher","img":null,"order":621},{"name":"Ebon Moss-Bachrach","character":"Trey Carano","img":null,"order":622},{"name":"Autry Haydon-Wilson","character":"Young Amy Harlow","img":null,"order":623},{"name":"Vincent D'Onofrio","character":"Sgt. Ian Lynch","img":null,"order":624},{"name":"Andre Royo","character":"Charlie Shannon","img":null,"order":625},{"name":"Barbie Robertson","character":"Debbie Russell","img":null,"order":636}],"tv:2710":[{"name":"Charlie Day","character":"Charlie Kelly","img":null,"order":0},{"name":"Glenn Howerton","character":"Dennis Reynolds","img":null,"order":1},{"name":"Rob Mac","character":"Ronald 'Mac' McDonald","img":null,"order":2},{"name":"Kaitlin Olson","character":"Dee Reynolds","img":null,"order":3},{"name":"Danny DeVito","character":"Frank Reynolds","img":null,"order":4},{"name":"Artemis Pebdani","character":"Artemis","img":null,"order":6},{"name":"Mary Elizabeth Ellis","character":"Waitress","img":null,"order":7},{"name":"David Hornsby","character":"Matthew 'Rickety Cricket' Mara","img":null,"order":8},{"name":"Lynne Marie Stewart","character":"Charlie's Mom","img":null,"order":216},{"name":"Sandy Martin","character":"Mac's Mom","img":null,"order":847},{"name":"Jimmi Simpson","character":"Liam McPoyle","img":null,"order":1009},{"name":"Nate Mooney","character":"Ryan McPoyle","img":null,"order":1011},{"name":"Andrew Friedman","character":"Uncle Jack","img":null,"order":1140},{"name":"Lance Barber","character":"Bill Ponderosa","img":null,"order":1201},{"name":"Gregory Scott Cummins","character":"Luther","img":null,"order":1230}],"tv:224372":[{"name":"Peter Claffey","character":"Ser Duncan 'Dunk' the Tall","img":null,"order":0},{"name":"Dexter Sol Ansell","character":"Egg","img":null,"order":1},{"name":"Ross Anderson","character":"Ser Humfrey Hardyng","img":null,"order":500},{"name":"Daniel Monks","character":"Ser Manfred Dondarrion","img":null,"order":504},{"name":"Tom Vaughan-Lawlor","character":"Plummer","img":null,"order":505},{"name":"Sam Spruell","character":"Prince Maekar Targaryen","img":null,"order":510},{"name":"Finn Bennett","character":"Prince Aerion 'Brightflame' Targaryen","img":null,"order":511},{"name":"Bertie Carvel","character":"Prince Baelor 'Breakspear' Targaryen","img":null,"order":512},{"name":"Danny Webb","character":"Ser Arlan of Pennytree","img":null,"order":513},{"name":"Tanzyn Crawford","character":"Tanselle","img":null,"order":514},{"name":"Henry Ashton","character":"Prince Daeron 'the Drunken' Targaryen","img":null,"order":517},{"name":"Edward Ashley","character":"Ser Steffon Fossoway","img":null,"order":518},{"name":"Cara Harris","character":"Lady Gwin","img":null,"order":527},{"name":"Shaun Thomas","character":"Ser Raymun 'The Reluctant' Fossoway","img":null,"order":532},{"name":"Daniel Ings","character":"Ser Lyonel 'The Laughing Storm' Baratheon","img":null,"order":543}],"tv:157741":[{"name":"Billy Bob Thornton","character":"Tommy Norris","img":null,"order":0},{"name":"Demi Moore","character":"Cami Miller","img":null,"order":1},{"name":"Andy Garcia","character":"Gallino","img":null,"order":2},{"name":"Ali Larter","character":"Angela Norris","img":null,"order":3},{"name":"Jacob Lofland","character":"Cooper Norris","img":null,"order":4},{"name":"Michelle Randolph","character":"Ainsley Norris","img":null,"order":5},{"name":"Paulina Ch\u00e1vez","character":"Ariana Medina","img":null,"order":6},{"name":"Kayla Wallace","character":"Rebecca Falcone","img":null,"order":7},{"name":"Mark Collie","character":"Sheriff Walt Joeberg","img":null,"order":8},{"name":"James Jordan","character":"Dale Bradley","img":null,"order":9},{"name":"Jon Hamm","character":"Monty Miller","img":null,"order":10},{"name":"Sam Elliott","character":"Thomas 'T.L.' Norris","img":null,"order":11},{"name":"Colm Feore","character":"Nathan","img":null,"order":501},{"name":"Mustafa Speaks","character":"Boss","img":null,"order":531},{"name":"Caleb Martin","character":"Ben 'BR' Reynolds","img":null,"order":611}],"tv:100088":[{"name":"Bella Ramsey","character":"Ellie Williams","img":null,"order":1},{"name":"Gabriel Luna","character":"Tommy Miller","img":null,"order":2},{"name":"Isabela Merced","character":"Dina","img":null,"order":3},{"name":"Young Mazino","character":"Jesse","img":null,"order":4},{"name":"Pedro Pascal","character":"Joel Miller","img":null,"order":164},{"name":"Anna Torv","character":"Tess","img":null,"order":523},{"name":"Lamar Johnson","character":"Henry Burrell","img":null,"order":565},{"name":"Rutina Wesley","character":"Maria","img":null,"order":567},{"name":"Samuel Hoeksema","character":"Museum Clicker","img":null,"order":571},{"name":"Kaitlyn Dever","character":"Abby Anderson","img":null,"order":576},{"name":"Danny Ramirez","character":"Manny Alvarez","img":null,"order":577},{"name":"Tati Gabrielle","character":"Nora","img":null,"order":578},{"name":"Spencer Lord","character":"Owen","img":null,"order":579},{"name":"Robert John Burke","character":"Seth","img":null,"order":580},{"name":"Ariela Barer","character":"Mel","img":null,"order":581}],"tv:252193":[{"name":"Junichi Okada","character":"Shujiro Saga","img":null,"order":0},{"name":"Yumia Fujisaki","character":"Futaba Katsuki","img":null,"order":1},{"name":"Kaya Kiyohara","character":"Iroha Kinugasa","img":null,"order":2},{"name":"Masahiro Higashide","character":"Kyojin Tsuge","img":null,"order":3},{"name":"Taichi Saotome","character":"Shikura Adashino","img":null,"order":4},{"name":"Yuya Endo","character":"Sansuke Gion","img":null,"order":5},{"name":"Hideaki Ito","character":"Bukotsu Kanjiya","img":null,"order":6},{"name":"Hiroshi Abe","character":"Okabe Gentosai","img":null,"order":7},{"name":"Gaku Hamada","character":"Kawaji Toshiyoshi","img":null,"order":501},{"name":"Arata Iura","character":"Okubo Toshimichi","img":null,"order":502},{"name":"Yuya Matsuura","character":"Kouemon Tachikawa","img":null,"order":504},{"name":"Nobi Nakanishi","character":"Narrator","img":null,"order":505},{"name":"Tetsushi Tanaka","character":"Hisoka Maejima","img":null,"order":506},{"name":"Kairi Jo","character":"Shinjiro Sayama","img":null,"order":514},{"name":"Taiiku Okazaki","character":"Keage Jinroku","img":null,"order":519}],"tv:270572":[{"name":"Winston Sawyers","character":"Ralph","img":null,"order":0},{"name":"David McKenna","character":"Nicholas \"Piggy\"","img":null,"order":1},{"name":"Lox Pratt","character":"Jack Merridew","img":null,"order":2},{"name":"Ike Talbut","character":"Simon Camborne","img":null,"order":3},{"name":"Thomas Connor","character":"Roger","img":null,"order":4},{"name":"Cornelius Brandreth","character":"Maurice","img":null,"order":5},{"name":"Cassius Flemyng","character":"Eric","img":null,"order":6},{"name":"Noah Flemyng","character":"Sam","img":null,"order":7},{"name":"Harrison Metcalf","character":"Frankie","img":null,"order":8},{"name":"Rafael de Belligny","character":"Robert","img":null,"order":9},{"name":"Felix Tandon","character":"William","img":null,"order":10},{"name":"Beau Thompson","character":"Philip","img":null,"order":11},{"name":"Kai Wilson","character":"Jasper","img":null,"order":12},{"name":"Freddie Lee-Grey","character":"Percival","img":null,"order":13},{"name":"Tom Page-Turner","character":"Bill","img":null,"order":14}],"tv:84773":[{"name":"Morfydd Clark","character":"Galadriel","img":null,"order":1},{"name":"Charlie Vickers","character":"Halbrand","img":null,"order":1},{"name":"Robert Aramayo","character":"Elrond","img":null,"order":3},{"name":"Charles Edwards","character":"Lord Celebrimbor","img":null,"order":4},{"name":"Owain Arthur","character":"Durin IV","img":null,"order":5},{"name":"Sophia Nomvete","character":"Disa","img":null,"order":6},{"name":"Daniel Weyman","character":"The Stranger","img":null,"order":7},{"name":"Markella Kavenagh","character":"Elanor 'Nori' Brandyfoot","img":null,"order":8},{"name":"Megan Richards","character":"Poppy Proudfellow","img":null,"order":9},{"name":"Ismael Cruz Cordova","character":"Arondir","img":null,"order":10},{"name":"Lloyd Owen","character":"Elendil","img":null,"order":11},{"name":"Benjamin Walker","character":"High King Gil-Galad","img":null,"order":16},{"name":"Cynthia Addai-Robinson","character":"M\u00edriel","img":null,"order":17},{"name":"Trystan Gravelle","character":"Pharaz\u00f4n","img":null,"order":18},{"name":"Ema Horvath","character":"E\u00e4rien","img":null,"order":19}],"tv:73411":[{"name":"Emma Stone","character":"Annie Landsberg","img":null,"order":0},{"name":"Jonah Hill","character":"Owen Milgrim","img":null,"order":1},{"name":"Sonoya Mizuno","character":"Dr. Azumi Fujita","img":null,"order":2},{"name":"Justin Theroux","character":"Dr. James K. Mantleray","img":null,"order":8},{"name":"Sally Field","character":"Dr. Greta Mantleray","img":null,"order":9},{"name":"Aaralyn Anderson","character":"Australia","img":null,"order":347},{"name":"James Monroe Iglehart","character":"Carl","img":null,"order":539},{"name":"Dai Ishiguro","character":"Head Control Tech","img":null,"order":540},{"name":"Gabriel Byrne","character":"Porter Milgrim","img":null,"order":546},{"name":"Kathleen Choe","character":"3","img":null,"order":547},{"name":"Allyce Beasley","character":"11","img":null,"order":550},{"name":"Stephen Hill","character":"7","img":null,"order":553},{"name":"Danny Hoch","character":"5","img":null,"order":556},{"name":"Billy Magnussen","character":"Jed Milgrim","img":null,"order":559},{"name":"Sejal Shah","character":"First Medical Tech","img":null,"order":562}],"tv:212333":[{"name":"Luca Marinelli","character":"Benito Mussolini","img":null,"order":0},{"name":"Francesco Russo","character":"Cesare Rossi","img":null,"order":1},{"name":"Barbara Chichiarelli","character":"Margherita Sarfatti","img":null,"order":2},{"name":"Benedetta Cimatti","character":"Rachele Mussolini","img":null,"order":3},{"name":"Gaetano Bruno","character":"Giacomo Matteotti","img":null,"order":4},{"name":"Maurizio Lombardi","character":"Emilio De Bono","img":null,"order":5},{"name":"Gianmarco Vettori","character":"Dino Grandi","img":null,"order":6},{"name":"Massimo De Lorenzo","character":"Alfredo Rocco","img":null,"order":7},{"name":"Fulvio Falzarano","character":"Giovanni Giolitti","img":null,"order":8},{"name":"Amedeo Gull\u00e0","character":"Augusto Malacria","img":null,"order":9},{"name":"Federico Majorana","character":"Amerigo Dumini","img":null,"order":500},{"name":"Lorenzo Zurzolo","character":"Italo Balbo","img":null,"order":501},{"name":"Vincenzo Nemolato","character":"King Vittorio Emanuele III","img":null,"order":502},{"name":"Giovanni Alfieri","character":"Amleto Poverno","img":null,"order":503},{"name":"Gabriele Falsetta","character":"Roberto Farinacci","img":null,"order":504}],"tv:61859":[{"name":"Tom Hiddleston","character":"Jonathan Pine","img":null,"order":0},{"name":"Hugh Laurie","character":"Richard Roper","img":null,"order":1},{"name":"Diego Calva","character":"Teddy Dos Santos","img":null,"order":2},{"name":"Camila Morrone","character":"Roxana Bola\u00f1os","img":null,"order":3},{"name":"Douglas Hodge","character":"Rex Mayhew","img":null,"order":5},{"name":"Elizabeth Debicki","character":"Jed Marshall","img":null,"order":6},{"name":"Paul Chahidi","character":"Basil Karapetian","img":null,"order":7},{"name":"David Harewood","character":"Joel Steadman","img":null,"order":8},{"name":"Hayley Squires","character":"Sally Price-Jones","img":null,"order":9},{"name":"Tom Hollander","character":"Lance Corkoran","img":null,"order":14},{"name":"Olivia Colman","character":"Angela Burr","img":"lawJUdbLb1YW0niaYjiVGR9IBG8.jpg","order":19},{"name":"Michael Nardone","character":"Frisky","img":null,"order":20},{"name":"Alistair Petrie","character":"Sandy Langbourne","img":null,"order":21},{"name":"Aure Atika","character":"","img":null,"order":281},{"name":"Noah Jupe","character":"","img":null,"order":380}],"tv:111110":[{"name":"I\u00f1aki Godoy","character":"Monkey D. Luffy","img":null,"order":0},{"name":"Emily Rudd","character":"Nami","img":null,"order":1},{"name":"Mackenyu","character":"Roronoa Zoro","img":null,"order":2},{"name":"Jacob Gibson","character":"Usopp","img":null,"order":3},{"name":"Vincent Regan","character":"Vice-Admiral Garp","img":null,"order":4},{"name":"Jeff Ward","character":"Buggy the Clown","img":null,"order":5},{"name":"Taz Skylar","character":"Sanji","img":null,"order":6},{"name":"Charithra Chandran","character":"Miss Wednesday / Nefertari Vivi","img":null,"order":7},{"name":"Mikaela Hoover","character":"Tony Tony Chopper (voice)","img":null,"order":8},{"name":"Morgan Davies","character":"Koby","img":null,"order":10},{"name":"Lera Abova","character":"Miss All Sunday","img":null,"order":11},{"name":"Armand Aucamp","character":"Bogard","img":null,"order":510},{"name":"McKinley Belcher III","character":"Captain Arlong","img":null,"order":546},{"name":"Jandre le Roux","character":"Kuroobi","img":null,"order":551},{"name":"Aidan Scott","character":"Helmeppo","img":null,"order":592}],"tv:61617":[{"name":"Elijah Wood","character":"Wirt (voice)","img":null,"order":0},{"name":"Collin Dean","character":"Gregory (voice)","img":null,"order":1},{"name":"Melanie Lynskey","character":"Beatrice (voice)","img":null,"order":2},{"name":"Samuel Ramey","character":"The Beast (voice)","img":null,"order":3},{"name":"Christopher Lloyd","character":"The Woodsman (voice)","img":null,"order":4},{"name":"Janet Klein","character":"Miss Langtree (voice)","img":null,"order":500},{"name":"Deborah Voigt","character":"Queen of the Clouds / Additional Voices (voice)","img":null,"order":501},{"name":"Chris Isaak","character":"Enoch / Additional Voices (voice)","img":null,"order":503},{"name":"John Cleese","character":"Adelaide (voice)","img":null,"order":510},{"name":"Sam Marin","character":"Old Man Langtree (voice)","img":null,"order":512},{"name":"Emily Brundige","character":"Sara / Additional Voices (voice)","img":null,"order":519},{"name":"Fred Stoller","character":"Fred The Horse (voice)","img":null,"order":521},{"name":"Cole Sanchez","character":"Jason Funderberker / Additional Voices (voice)","img":null,"order":521},{"name":"Shirley Jones","character":"Beatrice's Mother / Additional Voices (voice)","img":null,"order":523},{"name":"Jack Jones","character":"Gregory's Frog (voice)","img":null,"order":526}],"tv:60574":[{"name":"Cillian Murphy","character":"Thomas Shelby","img":null,"order":0},{"name":"Paul Anderson","character":"Arthur Shelby","img":null,"order":1},{"name":"Joe Cole","character":"John Shelby","img":null,"order":2},{"name":"Helen McCrory","character":"Polly Gray","img":null,"order":4},{"name":"Sophie Rundle","character":"Ada Shelby","img":null,"order":6},{"name":"Natasha O'Keeffe","character":"Lizzie Stark","img":null,"order":12},{"name":"Finn Cole","character":"Michael Gray","img":null,"order":14},{"name":"Ian Peck","character":"Curly","img":null,"order":15},{"name":"Tom Hardy","character":"Alfie Solomons","img":"d81K0RH8UX7tZj49tZaQhZ9ewH.jpg","order":16},{"name":"Ned Dennehy","character":"Charlie Strong","img":null,"order":17},{"name":"Harry Kirton","character":"Finn Shelby","img":null,"order":87},{"name":"Kate Phillips","character":"Linda Shelby","img":null,"order":104},{"name":"Annabelle Wallis","character":"Grace Shelby","img":null,"order":181},{"name":"Aimee-Ffion Edwards","character":"Esme Shelby","img":null,"order":606},{"name":"Packy Lee","character":"Johnny Dogs","img":null,"order":621}],"tv:250307":[{"name":"Noah Wyle","character":"Dr. Michael 'Robby' Robinavitch","img":null,"order":0},{"name":"Patrick Ball","character":"Dr. Frank Langdon","img":null,"order":2},{"name":"Katherine LaNasa","character":"Charge Nurse Dana Evans","img":null,"order":3},{"name":"Supriya Ganesh","character":"Dr. Samira Mohan","img":null,"order":4},{"name":"Fiona Dourif","character":"Dr. Cassie McKay","img":null,"order":5},{"name":"Taylor Dearden","character":"Dr. Melissa King","img":null,"order":6},{"name":"Isa Briones","character":"Dr. Trinity Santos","img":null,"order":7},{"name":"Gerran Howell","character":"Student Doctor Dennis Whitaker","img":null,"order":8},{"name":"Shabana Azeez","character":"Student Doctor Victoria Javadi","img":null,"order":9},{"name":"Alexandra Metz","character":"Dr. Yolanda Garcia","img":null,"order":508},{"name":"Ned Brower","character":"Nurse Jesse","img":null,"order":523},{"name":"Amielynn Abellera","character":"Nurse Perlah Alawi","img":null,"order":707},{"name":"Jalen Thomas Brooks","character":"Nurse Mateo Diaz","img":null,"order":708},{"name":"Brandon Mendez Homer","character":"Nurse Donald 'Donnie' Donahue","img":null,"order":709},{"name":"Kristin Villanueva","character":"Nurse Princess","img":null,"order":710}],"tv:225171":[{"name":"Rhea Seehorn","character":"Carol Sturka","img":null,"order":0},{"name":"Karolina Wydra","character":"Zosia","img":null,"order":1},{"name":"Carlos-Manuel Vesga","character":"Manousos Oviedo","img":null,"order":2},{"name":"Miriam Shor","character":"Helen","img":null,"order":500},{"name":"Samba Schutte","character":"Mr. Diabat\u00e9","img":null,"order":524},{"name":"Menik Gooneratne","character":"Laxmi","img":null,"order":525},{"name":"Darinka Arones","character":"Kusimayu","img":null,"order":526},{"name":"Anna Mhairi","character":"Vesper","img":null,"order":527},{"name":"Max Reeves","character":"Margaux","img":null,"order":528},{"name":"Olivia Rouyre","character":"Genevieve","img":null,"order":529},{"name":"Rayaan Kamal Khan","character":"Ravi","img":null,"order":532},{"name":"Elena Est\u00e9r","character":"T'ika","img":null,"order":537},{"name":"Soledad Campos","character":"Maternal Other","img":null,"order":559},{"name":"Jennifer Esquivel","character":"Sisa","img":null,"order":581},{"name":"Patrick Fabian","character":"Others' Phone Answering Service (voice) (uncredited)","img":null,"order":591}],"tv:94028":[{"name":"Andrew Scott","character":"Tom Ripley","img":"4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg","order":0},{"name":"Dakota Fanning","character":"Marge Sherwood","img":null,"order":1},{"name":"Johnny Flynn","character":"Dickie Greenleaf","img":null,"order":2},{"name":"Kenneth Lonergan","character":"Herbert Greenleaf","img":null,"order":500},{"name":"Francesca Romana Bergamo","character":"Ermelinda","img":null,"order":511},{"name":"Pasquale Esposito","character":"Pucci","img":null,"order":512},{"name":"Arcangelo Iannace","character":"Giulio","img":null,"order":513},{"name":"Vittorio Viviani","character":"Matteo","img":null,"order":514},{"name":"Eliot Sumner","character":"Frederick 'Freddie' Miles","img":null,"order":515},{"name":"Bokeem Woodbine","character":"Alvin MacCarron","img":null,"order":516},{"name":"Renato Solpietro","character":"Carlo","img":null,"order":519},{"name":"Francesco Formichetti","character":"Sanremo Desk Clerk","img":null,"order":523},{"name":"Margherita Buy","character":"Sig. Silvana Buffi","img":null,"order":540},{"name":"Maurizio Lombardi","character":"Inspector Pietro Ravini","img":null,"order":544},{"name":"Gennaro Arena","character":"Naples Kid","img":null,"order":578}],"tv:245256":[{"name":"Lola Petticrew","character":"Dolours Price","img":null,"order":0},{"name":"Maxine Peake","character":"Older Dolours Price","img":null,"order":1},{"name":"Hazel Doupe","character":"Marian Price","img":null,"order":2},{"name":"Anthony Boyle","character":"Brendan Hughes","img":null,"order":3},{"name":"Tom Vaughan-Lawlor","character":"Older Brendan Hughes","img":null,"order":4},{"name":"Josh Finan","character":"Gerry Adams","img":null,"order":5},{"name":"Rory Kinnear","character":"Frank Kitson","img":null,"order":6},{"name":"Seamus O'Hara","character":"Mackers","img":null,"order":500},{"name":"Helen Behan","character":"Older Marian Price","img":null,"order":501},{"name":"Michael Colgan","character":"Older Gerry Adams","img":null,"order":502},{"name":"Conor O'Donnell","character":"Eamon","img":null,"order":503},{"name":"James Doherty","character":"Chief Inspector","img":null,"order":504},{"name":"Bill Jones","character":"Ian","img":null,"order":505},{"name":"Shannon Watson","character":"Make-up Artist","img":null,"order":506},{"name":"Emma Canning","character":"Kathleen","img":null,"order":508}],"tv:95396":[{"name":"Adam Scott","character":"Mark Scout","img":null,"order":0},{"name":"Britt Lower","character":"Helly Riggs","img":null,"order":1},{"name":"Tramell Tillman","character":"Seth Milchick","img":null,"order":2},{"name":"Zach Cherry","character":"Dylan George","img":null,"order":3},{"name":"Patricia Arquette","character":"Harmony Cobel","img":null,"order":4},{"name":"Jen Tullock","character":"Devon Scout-Hale","img":null,"order":5},{"name":"Michael Chernus","character":"Ricken Hale","img":null,"order":6},{"name":"Dichen Lachman","character":"Ms. Casey","img":null,"order":7},{"name":"Sarah Bock","character":"Eustice Huang","img":null,"order":8},{"name":"John Turturro","character":"Irving Bailiff","img":null,"order":9},{"name":"Christopher Walken","character":"Burt Goodman","img":null,"order":10},{"name":"Sydney Cole Alexander","character":"Natalie Kalen","img":null,"order":515},{"name":"Karen Aldridge","character":"Asal Regabhi","img":null,"order":522},{"name":"Michael Cumpsty","character":"Doug Graner","img":null,"order":529},{"name":"Claudia Robinson","character":"Felicia","img":null,"order":537}],"tv:1274":[{"name":"Peter Krause","character":"Nate Fisher","img":null,"order":0},{"name":"Michael C. Hall","character":"David Fisher","img":null,"order":1},{"name":"Frances Conroy","character":"Ruth Fisher","img":null,"order":2},{"name":"Lauren Ambrose","character":"Claire Fisher","img":null,"order":3},{"name":"Freddy Rodr\u00edguez","character":"Federico Diaz","img":null,"order":4},{"name":"Mathew St. Patrick","character":"Keith Charles","img":null,"order":5},{"name":"Rachel Griffiths","character":"Brenda Chenowith","img":null,"order":6},{"name":"Ben Foster","character":"Russell Corwin","img":null,"order":7},{"name":"Justina Machado","character":"Vanessa Diaz","img":null,"order":8},{"name":"Jeremy Sisto","character":"Billy Chenowith","img":null,"order":10},{"name":"Richard Jenkins","character":"Nathaniel Fisher","img":null,"order":11},{"name":"Lili Taylor","character":"Lisa Kimmel Fisher","img":null,"order":12},{"name":"James Cromwell","character":"George Sibley","img":null,"order":13},{"name":"Bronwyn Tosh","character":"Maya Fisher","img":null,"order":559},{"name":"Brenna Tosh","character":"Maya Fisher","img":null,"order":608}],"tv:86942":[{"name":"Tom Waes","character":"Bob Lemmens","img":null,"order":0},{"name":"Frank Lammers","character":"Ferry Bouman","img":null,"order":1},{"name":"Raymond Thiry","character":"John Zwart","img":null,"order":2},{"name":"Elise Schaap","character":"Danielle Bouman","img":null,"order":4},{"name":"Anna Drijver","character":"Kim De Rooij","img":null,"order":5},{"name":"Jeroen Van der Ven","character":"Patrick Dierickx","img":null,"order":6},{"name":"Ruth Becquart","character":"Nathalie Geudens","img":null,"order":7},{"name":"Manou Kersting","character":"Nick Janssens","img":null,"order":8},{"name":"Wim Willaert","character":"Laurent Berger","img":null,"order":10},{"name":"Sebastien Dewaele","character":"Jean-Pierre Berger","img":null,"order":11},{"name":"Emma Verlinden","character":"Polly","img":null,"order":187},{"name":"Katrien De Ruysscher","character":"Liesbeth Mertens","img":null,"order":553},{"name":"Robbie Cleiren","character":"Marc Gevers","img":null,"order":554},{"name":"Lieke van den Broek","character":"Sonja Van Kamp","img":null,"order":562},{"name":"Celest Henri Cornelis","character":"Jackson Geudens","img":null,"order":582}],"tv:86248":[{"name":"Robbie Amell","character":"Nathan Brown","img":null,"order":0},{"name":"Andy Allo","character":"Nora Antony","img":null,"order":1},{"name":"Allegra Edwards","character":"Ingrid Kannerman","img":null,"order":2},{"name":"Zainab Johnson","character":"Aleesha","img":null,"order":3},{"name":"Kevin Bigley","character":"Luke","img":null,"order":4},{"name":"Owen Daniels","character":"A.I. Guy","img":null,"order":5},{"name":"Andrea Rosen","character":"Lucy Slack","img":null,"order":6},{"name":"Josh Banday","character":"Ivan Spelich","img":null,"order":8},{"name":"Mackenzie Cardwell","character":"Tinsley","img":null,"order":9},{"name":"Andy Thompson","character":"Lionel Winter","img":null,"order":600},{"name":"Barclay Hope","character":"Oliver Kannerman","img":null,"order":607},{"name":"William B. Davis","character":"David Choak","img":null,"order":624},{"name":"Chloe Coleman","character":"Nevaeh","img":null,"order":632},{"name":"Yvetta Fisher","character":"Batia","img":null,"order":633},{"name":"Jessica Tuck","character":"Viv","img":null,"order":651}],"tv:210664":[{"name":"Stephen Graham","character":"Matthew Collins","img":null,"order":0},{"name":"Jason Flemyng","character":"Nick","img":null,"order":1},{"name":"Leanne Best","character":"Alison","img":null,"order":2},{"name":"Andrew Ellis","character":"Robbie","img":null,"order":3},{"name":"Chris Coghill","character":"Chris","img":null,"order":4},{"name":"Dean-Charles Chapman","character":"Jack","img":null,"order":5},{"name":"Sami Amber","character":"Billy","img":null,"order":6},{"name":"Steven Miller","character":"Young Lenny","img":null,"order":500}],"tv:270476":[{"name":"Matthew Rhys","character":"Mayor Tom Loftis","img":null,"order":0},{"name":"Kate O'Flynn","character":"Patricia Moyer","img":null,"order":1},{"name":"Kevin Carroll","character":"Sheriff Bechir Clemmons","img":null,"order":2},{"name":"Dale Dickey","character":"Rosemary","img":null,"order":3},{"name":"Kingston Rumi Southwick","character":"Evan Loftis","img":null,"order":4},{"name":"Stephen Root","character":"Wyck Crawford","img":null,"order":5},{"name":"Neil Casey","character":"Kurt","img":null,"order":500},{"name":"Jeff Hiller","character":"Dale","img":null,"order":501},{"name":"K Callan","character":"Ruth","img":null,"order":503},{"name":"Kylie McNeill","character":"Kelly","img":null,"order":504},{"name":"Beck Nolan","character":"PJ","img":null,"order":505},{"name":"Charles Van Flaherty","character":"Gil","img":null,"order":506},{"name":"Nancy Lenehan","character":"Gerrie","img":null,"order":508},{"name":"Christian Clemenson","character":"Dr. Morgan","img":null,"order":509},{"name":"Sipiwe Moyo","character":"Chelle","img":null,"order":529}],"tv:71912":[{"name":"Henry Cavill","character":"Geralt of Rivia","img":null,"order":0},{"name":"Anya Chalotra","character":"Yennefer of Vengerberg","img":null,"order":2},{"name":"Freya Allan","character":"Princess Cirilla 'Ciri' of Cintra","img":null,"order":3},{"name":"Joey Batey","character":"Jaskier / The Sandpiper","img":null,"order":4},{"name":"Eamon Farren","character":"Cahir","img":null,"order":5},{"name":"Mim\u00ee M. Khayisa","character":"Fringilla Vigo","img":null,"order":6},{"name":"Anna Shaffer","character":"Triss Merigold","img":null,"order":7},{"name":"Mahesh Jadu","character":"Vilgefortz of Roggeveen","img":null,"order":8},{"name":"Cassie Clare","character":"Philippa Eilhart","img":null,"order":11},{"name":"MyAnna Buring","character":"Tissaia De Vries","img":null,"order":14},{"name":"Mecia Simson","character":"Francesca Findabair","img":null,"order":18},{"name":"Bart Edwards","character":"The White Flame","img":null,"order":345},{"name":"Wilson Mbomio","character":"Dara","img":null,"order":500},{"name":"Therica Wilson-Read","character":"Sabrina","img":null,"order":542},{"name":"Royce Pierreson","character":"Istredd","img":null,"order":671}],"movie:76203":[{"name":"Chiwetel Ejiofor","character":"Solomon Northup","img":null,"order":0},{"name":"Michael Fassbender","character":"Edwin Epps","img":null,"order":1},{"name":"Lupita Nyong'o","character":"Patsey","img":null,"order":2},{"name":"Benedict Cumberbatch","character":"William Ford","img":null,"order":3},{"name":"Paul Dano","character":"John Tibeats","img":null,"order":4},{"name":"Sarah Paulson","character":"Mary Epps","img":null,"order":5},{"name":"Adepero Oduye","character":"Eliza","img":null,"order":6},{"name":"Scoot McNairy","character":"Brown","img":null,"order":7},{"name":"Paul Giamatti","character":"Theophilus Freeman","img":null,"order":8},{"name":"Brad Pitt","character":"Samuel Bass","img":null,"order":9},{"name":"Michael Kenneth Williams","character":"Robert","img":null,"order":10},{"name":"Alfre Woodard","character":"Mistress Shaw","img":null,"order":11},{"name":"Chris Chalk","character":"Clemens","img":null,"order":12},{"name":"Taran Killam","character":"Hamilton","img":null,"order":13},{"name":"Bill Camp","character":"Radburn","img":null,"order":14}],"movie:13654":[{"name":"Barry Bostwick","character":"Thunderbolt (voice)","img":null,"order":0},{"name":"Jason Alexander","character":"Lightning (voice)","img":null,"order":1},{"name":"Martin Short","character":"Lars (voice)","img":null,"order":2},{"name":"Bobby Lockwood","character":"Patch (voice)","img":null,"order":3},{"name":"Susanne Blakeslee","character":"Cruella (voice)","img":null,"order":4},{"name":"Samuel West","character":"Pongo (voice)","img":null,"order":5},{"name":"Maurice LaMarche","character":"Horace (voice)","img":null,"order":6},{"name":"Jeff Bennett","character":"Jasper (voice)","img":null,"order":7},{"name":"Jodi Benson","character":"Anita (voice)","img":null,"order":8},{"name":"Tim Bentinck","character":"Roger (voice)","img":null,"order":9},{"name":"Kath Soucie","character":"Perdita (voice)","img":null,"order":10},{"name":"Mary MacLeod","character":"Nanny (voice)","img":null,"order":11},{"name":"Michael Lerner","character":"Producer (voice)","img":null,"order":12},{"name":"Kathryn Beaumont","character":"Additional Voices (voice)","img":null,"order":13},{"name":"Nicolette Little","character":"Additional Voices (voice)","img":null,"order":14}],"movie:974453":[{"name":"Liam Neeson","character":"Thug","img":null,"order":0},{"name":"Ron Perlman","character":"Charlie Conner","img":null,"order":1},{"name":"Yolonda Ross","character":"Woman","img":null,"order":2},{"name":"Frankie Shaw","character":"Daisy","img":null,"order":3},{"name":"Daniel Diemer","character":"Kyle Conner","img":null,"order":4},{"name":"Jimmy Gonzales","character":"Diego Machado","img":null,"order":5},{"name":"Josh Drennen","character":"Thug's Dad","img":null,"order":6},{"name":"Deanna Tarraza","character":"Araceli","img":null,"order":7},{"name":"Terrence Pulliam","character":"Dre","img":null,"order":8},{"name":"Omar Moustafa Ghonim","character":"Armando","img":null,"order":9},{"name":"Tom Kemp","character":"Burt","img":null,"order":10},{"name":"Ryan Homchick","character":"Dr. Gruber","img":null,"order":11},{"name":"William Xifaras","character":"Kiko","img":null,"order":12},{"name":"Kris Eivers","character":"Priest","img":null,"order":13},{"name":"Ryan Caraway","character":"George","img":null,"order":14}],"movie:446791":[{"name":"Michelle Williams","character":"Gail Harris","img":null,"order":0},{"name":"Mark Wahlberg","character":"Fletcher Chase","img":null,"order":1},{"name":"Christopher Plummer","character":"Jean Paul Getty","img":null,"order":2},{"name":"Charlie Plummer","character":"John Paul Getty III","img":null,"order":3},{"name":"Romain Duris","character":"Cinquanta","img":null,"order":4},{"name":"Timothy Hutton","character":"Oswald Hinge","img":null,"order":5},{"name":"Charlie Shotwell","character":"John Paul Getty III (Age 7)","img":null,"order":6},{"name":"Andrew Buchan","character":"John Paul Getty II","img":null,"order":7},{"name":"Marco Leonardi","character":"Saverio Mammoliti","img":null,"order":8},{"name":"Giuseppe Bonifati","character":"Giovanni Iacovoni","img":null,"order":9},{"name":"Nicolas Vaporidis","character":"Il Tamia 'Chipmunk'","img":null,"order":10},{"name":"Andrea Piedimonte Bodini","character":"Corvo","img":null,"order":11},{"name":"Guglielmo Favilla","character":"Piccolino","img":null,"order":12},{"name":"Adele Tirante","character":"Maria","img":null,"order":13},{"name":"Stacy Martin","character":"Nancy","img":null,"order":14}],"movie:1013601":[{"name":"Robert De Niro","character":"Vito Genovese / Frank Costello","img":null,"order":0},{"name":"Debra Messing","character":"Bobbie Costello","img":null,"order":1},{"name":"Cosmo Jarvis","character":"Vincent Gigante","img":null,"order":2},{"name":"Kathrine Narducci","character":"Anna Genovese","img":null,"order":3},{"name":"Michael Rispoli","character":"Albert Anastasia","img":null,"order":4},{"name":"Michael Adler","character":"Senator Tobey","img":null,"order":5},{"name":"Ed Amatrudo","character":"Rudolph Halley","img":null,"order":6},{"name":"Joe Bacino","character":"Joe Profaci","img":null,"order":7},{"name":"Anthony J. Gallo","character":"Tommy Lucchese","img":null,"order":8},{"name":"Wallace Langham","character":"Senator Estes Kefauver","img":null,"order":9},{"name":"Louis Mustillo","character":"Joe Bonanno","img":null,"order":10},{"name":"Frank Piccirillo","character":"Richie Boiardo","img":null,"order":11},{"name":"Matt Servitto","character":"George Wolf","img":null,"order":12},{"name":"Robert Uricola","character":"Tony Bender","img":null,"order":13},{"name":"Belmont Cameli","character":"Frankie Boy","img":null,"order":14}],"movie:416153":[{"name":"James Franco","character":"Lemmy (voice)","img":null,"order":0},{"name":"Jeremy Renner","character":"Swifty (voice)","img":null,"order":1},{"name":"Alec Baldwin","character":"PB (voice)","img":null,"order":2},{"name":"Anjelica Huston","character":"Magda (voice)","img":null,"order":3},{"name":"Michael Madsen","character":"Duke (voice)","img":null,"order":4},{"name":"Heidi Klum","character":"Jade / Bertha (voice)","img":null,"order":5},{"name":"John Cleese","character":"Otto von Walrus (voice)","img":null,"order":6},{"name":"Omar Sy","character":"Leopold (voice)","img":null,"order":7},{"name":"Laurie Holden","character":"Dakota (voice)","img":null,"order":8},{"name":"Donny Falsetti","character":"Dusty (voice)","img":null,"order":9},{"name":"Susan Spano","character":"Mary (voice)","img":null,"order":10},{"name":"Jason Deline","character":"Naz Narwahl (voice)","img":null,"order":11},{"name":"Aaron Woodley","character":"Puffin Leader (voice)","img":null,"order":12},{"name":"Lucie Hager","character":"Countdown Inka (voice)","img":null,"order":13},{"name":"Lillian Moloy","character":"Young Jade","img":null,"order":14}],"movie:1607":[{"name":"Robert De Niro","character":"Lorenzo Anello","img":null,"order":0},{"name":"Chazz Palminteri","character":"Sonny LoSpecchio","img":null,"order":1},{"name":"Lillo Brancato","character":"Calogero 'C' Anello (Age 17)","img":null,"order":2},{"name":"Francis Capra","character":"Calogero 'C' Anello (Age 9)","img":null,"order":3},{"name":"Taral Hicks","character":"Jane Williams","img":null,"order":4},{"name":"Kathrine Narducci","character":"Rosina Anello","img":null,"order":5},{"name":"Clem Caserta","character":"Jimmy Whispers","img":null,"order":6},{"name":"Alfred Sauchelli, Jr.","character":"Bobby Bars","img":null,"order":7},{"name":"Frank Pietrangolare","character":"Danny K.O.","img":null,"order":8},{"name":"Joe Pesci","character":"Carmine","img":null,"order":9},{"name":"Robert D'Andrea","character":"Tony Toupee","img":null,"order":10},{"name":"Eddie Montanaro","character":"Eddie Mush","img":null,"order":11},{"name":"Fred Fischer","character":"JoJo 'The Whale'","img":null,"order":12},{"name":"Dave Salerno","character":"Frankie Coffeecake","img":null,"order":13},{"name":"Joseph D'Onofrio","character":"Slick (Age 17)","img":null,"order":14}],"movie:1380417":[{"name":"Yahya Abdul-Mateen II","character":"Wayne Strider","img":null,"order":0},{"name":"Mark Wahlberg","character":"Gregory Scarpa","img":null,"order":1},{"name":"Nicole Beharie","character":"Allison Strider","img":null,"order":2},{"name":"Josh Lucas","character":"Special Agent Everett Rhodes","img":null,"order":3},{"name":"David Strathairn","character":"Special Agent Roy K. Moore","img":null,"order":4},{"name":"Giancarlo Esposito","character":"Vernon Dahmer","img":null,"order":5},{"name":"Ethan Embry","character":"Sam Bowers","img":null,"order":6},{"name":"LisaGay Hamilton","character":"Ms. Rebecca Brewer","img":null,"order":7},{"name":"LaChanze","character":"Hattie Dahmer","img":null,"order":8},{"name":"Biko Eisen-Martin","character":"Special Agent Archie Crawford","img":null,"order":9},{"name":"Will Brittain","character":"Billy Roy Pitts","img":null,"order":10},{"name":"DeVere Jehl","character":"Edgar Ray Killen","img":null,"order":11},{"name":"Jody Thompson","character":"Lawrence Byrd","img":null,"order":12},{"name":"Zach Lane","character":"Howard Styles","img":null,"order":13},{"name":"Lee Perkins","character":"Sheriff Mitchell","img":null,"order":14}],"movie:271110":[{"name":"Chris Evans","character":"Steve Rogers / Captain America","img":null,"order":0},{"name":"Robert Downey Jr.","character":"Tony Stark / Iron Man","img":null,"order":1},{"name":"Scarlett Johansson","character":"Natasha Romanoff / Black Widow","img":null,"order":2},{"name":"Sebastian Stan","character":"Bucky Barnes / Winter Soldier","img":null,"order":3},{"name":"Anthony Mackie","character":"Sam Wilson / Falcon","img":null,"order":4},{"name":"Don Cheadle","character":"Lieutenant James Rhodes / War Machine","img":null,"order":5},{"name":"Jeremy Renner","character":"Clint Barton / Hawkeye","img":null,"order":6},{"name":"Chadwick Boseman","character":"T'Challa / Black Panther","img":null,"order":7},{"name":"Paul Bettany","character":"Vision","img":null,"order":8},{"name":"Elizabeth Olsen","character":"Wanda Maximoff / Scarlet Witch","img":null,"order":9},{"name":"Paul Rudd","character":"Scott Lang / Ant-Man","img":null,"order":10},{"name":"Emily VanCamp","character":"Sharon Carter","img":null,"order":11},{"name":"Tom Holland","character":"Peter Parker / Spider-Man","img":"xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","order":12},{"name":"Daniel Br\u00fchl","character":"Zemo","img":null,"order":13},{"name":"Frank Grillo","character":"Brock Rumlow / Crossbones","img":null,"order":14}],"movie:1771":[{"name":"Chris Evans","character":"Captain America / Steve Rogers","img":null,"order":0},{"name":"Hayley Atwell","character":"Peggy Carter","img":null,"order":1},{"name":"Sebastian Stan","character":"James Buchanan 'Bucky' Barnes","img":null,"order":2},{"name":"Tommy Lee Jones","character":"Colonel Chester Phillips","img":null,"order":3},{"name":"Hugo Weaving","character":"Johann Schmidt / Red Skull","img":null,"order":4},{"name":"Dominic Cooper","character":"Howard Stark","img":null,"order":5},{"name":"Richard Armitage","character":"Heinz Kruger","img":null,"order":6},{"name":"Stanley Tucci","character":"Dr. Abraham Erskine","img":null,"order":7},{"name":"Samuel L. Jackson","character":"Nick Fury","img":null,"order":8},{"name":"Toby Jones","character":"Dr. Arnim Zola","img":null,"order":9},{"name":"Neal McDonough","character":"Timothy 'Dum Dum' Dugan","img":null,"order":10},{"name":"Derek Luke","character":"Gabe Jones","img":null,"order":11},{"name":"Kenneth Choi","character":"Jim Morita","img":null,"order":12},{"name":"JJ Feild","character":"James Montgomery Falsworth","img":null,"order":13},{"name":"Bruno Ricci","character":"Jacques Dernier","img":null,"order":14}],"movie:100402":[{"name":"Chris Evans","character":"Steve Rogers / Captain America","img":null,"order":0},{"name":"Samuel L. Jackson","character":"Nick Fury","img":null,"order":1},{"name":"Scarlett Johansson","character":"Natasha Romanoff / Black Widow","img":null,"order":2},{"name":"Robert Redford","character":"Alexander Pierce","img":null,"order":3},{"name":"Sebastian Stan","character":"Bucky Barnes / Winter Soldier","img":null,"order":4},{"name":"Anthony Mackie","character":"Sam Wilson / Falcon","img":null,"order":5},{"name":"Cobie Smulders","character":"Maria Hill","img":null,"order":6},{"name":"Frank Grillo","character":"Brock Rumlow","img":null,"order":7},{"name":"Maximiliano Hern\u00e1ndez","character":"Jasper Sitwell","img":null,"order":8},{"name":"Emily VanCamp","character":"Kate / Agent 13","img":null,"order":9},{"name":"Hayley Atwell","character":"Peggy Carter","img":null,"order":10},{"name":"Toby Jones","character":"Dr. Arnim Zola","img":null,"order":11},{"name":"Stan Lee","character":"Smithsonian Guard","img":null,"order":12},{"name":"Callan Mulvey","character":"Jack Rollins","img":null,"order":13},{"name":"Jenny Agutter","character":"Councilwoman Hawley","img":null,"order":14}],"tv:60948":[{"name":"Aaron Stanford","character":"James Cole","img":null,"order":0},{"name":"Amanda Schull","character":"Cassandra Railly","img":null,"order":1},{"name":"Kirk Acevedo","character":"Jos\u00e9 Ramse","img":null,"order":2},{"name":"Barbara Sukowa","character":"Katarina Jones","img":null,"order":3},{"name":"Emily Hampshire","character":"Jennifer Goines","img":null,"order":4},{"name":"Todd Stashwick","character":"Deacon","img":null,"order":6},{"name":"Noah Bean","character":"Aaron Marker","img":null,"order":7},{"name":"Alisen Richmond-Peck","character":"Striking Woman","img":null,"order":366},{"name":"Scottie Thompson","character":"Vivian Rutledge","img":null,"order":519},{"name":"Peter DaCunha","character":"Samuel Ramse","img":null,"order":760},{"name":"Murray Furrow","character":"Lasky","img":null,"order":797},{"name":"Andrew Gillies","character":"Julian Adler","img":null,"order":800},{"name":"Tom Noonan","character":"Pallid Man","img":null,"order":805},{"name":"Demore Barnes","character":"Marcus Whitley","img":null,"order":806},{"name":"Brooke Williams","character":"Hannah Jones","img":null,"order":807}],"tv:202097":[{"name":"Jill Hennessy","character":"Lynn Harmon","img":null,"order":501},{"name":"Oakes Fegley","character":"Devin Harmon","img":null,"order":502},{"name":"Robert Wisdom","character":"Mitch Becker","img":null,"order":503},{"name":"J. Harrison Ghee","character":"Kevin Milstead","img":null,"order":504},{"name":"Evan Marsh","character":"Alex Harmon","img":null,"order":505},{"name":"Sonny McKnight","character":"Young Devin (Age 3)","img":null,"order":506},{"name":"Coleby Wojtak-Hissong","character":"Young Devin (Age 5)","img":null,"order":507},{"name":"Athan Giazitzidis","character":"Young Alex","img":null,"order":508},{"name":"Ali Momen","character":"Principal Sam Canosa","img":null,"order":509},{"name":"Tara Rosling","character":"Dr. Laurie Grotstein","img":null,"order":510},{"name":"Deklon Roberts","character":"Captain Kramer","img":null,"order":511},{"name":"Seema Doad","character":"Uniformed Officer","img":null,"order":512},{"name":"Michael Chiklis","character":"Dr. Scott Harmon","img":null,"order":596},{"name":"Daniel Maslany","character":"Public Defender Eli Demille","img":null,"order":633},{"name":"Matthew James Thomas","character":"Alexei","img":null,"order":639}],"tv:79410":[{"name":"Ricky Gervais","character":"Tony","img":null,"order":0},{"name":"Tom Basden","character":"Matt","img":null,"order":1},{"name":"Kerry Godliman","character":"Lisa","img":null,"order":2},{"name":"David Bradley","character":"Ray","img":null,"order":3},{"name":"Joe Wilkinson","character":"Pat","img":null,"order":4},{"name":"Roisin Conaty","character":"Daphne \"Roxy\"","img":null,"order":6},{"name":"Tony Way","character":"Lenny","img":null,"order":7},{"name":"Diane Morgan","character":"Kath","img":null,"order":9},{"name":"Ashley Jensen","character":"Emma","img":null,"order":11},{"name":"Tommy Finnegan","character":"George","img":null,"order":12},{"name":"Jo Hartley","character":"June","img":null,"order":13},{"name":"Penelope Wilton","character":"Anne","img":null,"order":14},{"name":"David Earl","character":"Brian","img":null,"order":15},{"name":"Ethan Lawrence","character":"James","img":null,"order":16},{"name":"Michelle Greenidge","character":"Valerie","img":null,"order":17}],"tv:7406":[{"name":"Lynda Bellingham","character":"Helen Herriot","img":null,"order":0},{"name":"Robert Hardy","character":"Siegfried Farnon","img":null,"order":1},{"name":"Christopher Timothy","character":"James Herriot","img":null,"order":2},{"name":"Carol Drinkwater","character":"Helen Herriot","img":null,"order":3},{"name":"Mary Hignett","character":"Mrs. Hall","img":null,"order":4},{"name":"Margaretta Scott","character":"Mrs. Pumphrey","img":null,"order":7},{"name":"Teddy Turner","character":"Hodgekin","img":null,"order":8},{"name":"Peter Davison","character":"Tristan Farnon","img":null,"order":10},{"name":"John McGlynn","character":"Calum Buchanan","img":null,"order":11},{"name":"Jean Heywood","character":"Mrs Alton","img":null,"order":12},{"name":"Peter Martin","character":"Handshaw","img":null,"order":839},{"name":"Oliver Wilson","character":"Jimmy Herriot","img":null,"order":928},{"name":"Rebecca Smith","character":"Rosie Herriot","img":null,"order":929},{"name":"John Sharp","character":"Mr. Biggins","img":null,"order":952},{"name":"Andrea Gibb","character":"Deidre Buchanan","img":null,"order":971}],"tv:67117":[{"name":"Katy Mixon","character":"Katie Otto","img":null,"order":0},{"name":"Diedrich Bader","character":"Greg Otto","img":null,"order":1},{"name":"Meg Donnelly","character":"Taylor Otto","img":null,"order":2},{"name":"Giselle Eisenberg","character":"Anna-Kat Otto","img":null,"order":3},{"name":"Julia Butters","character":"Anna-Kat Otto","img":null,"order":4},{"name":"Daniel DiMaggio","character":"Oliver Otto","img":null,"order":5},{"name":"Carly Hughes","character":"Angela","img":null,"order":6},{"name":"Ali Wong","character":"Doris","img":null,"order":7},{"name":"Jerry Lambert","character":"Principal Ablin","img":null,"order":515},{"name":"Logan Pepper","character":"Cooper Bradford","img":null,"order":518},{"name":"Peyton Meyer","character":"Trip Windsor","img":null,"order":519},{"name":"Wendie Malick","character":"Kathryn","img":null,"order":532},{"name":"Carly Craig","character":"Tara","img":null,"order":541},{"name":"Evan O'Toole","character":"Franklin","img":null,"order":565},{"name":"Julie Meyer","character":"Maria","img":null,"order":634}],"tv:47054":[{"name":"Mark Addy","character":"Hercules","img":null,"order":0},{"name":"Jack Donnelly","character":"Jason","img":null,"order":1},{"name":"Robert Emms","character":"Pythagoras","img":null,"order":2},{"name":"Aiysha Hart","character":"Ariadne","img":null,"order":3},{"name":"Sarah Parish","character":"Pasiphae","img":null,"order":4},{"name":"Juliet Stevenson","character":"The Oracle","img":null,"order":5},{"name":"Jemima Rooper","character":"Medusa","img":null,"order":6},{"name":"Amy Manson","character":"Medea","img":null,"order":7},{"name":"Alexander Siddig","character":"Minos","img":null,"order":8},{"name":"Vincent Regan","character":"Dion","img":null,"order":9},{"name":"Lorcan Cranitch","character":"Cilix","img":null,"order":10},{"name":"Robert Lindsay","character":"Daedalus","img":null,"order":11},{"name":"Anya Taylor-Joy","character":"Cassandra","img":null,"order":514},{"name":"Stuart Davidson","character":"Lysis's Masseur","img":null,"order":516},{"name":"Kate Davies-Speak","character":"Atlantean","img":null,"order":533}],"tv:62481":[{"name":"Bobby Flay","character":"Self","img":null,"order":0},{"name":"Giada De Laurentiis","character":"Self","img":null,"order":475},{"name":"Michael Symon","character":"Self","img":null,"order":493},{"name":"Scott Conant","character":"Self - Guest Judge","img":null,"order":503},{"name":"Alex Guarnaschelli","character":"Self - Guest Judge","img":null,"order":512},{"name":"Eddie Jackson","character":"Self","img":null,"order":512},{"name":"Sunny Anderson","character":"Self","img":null,"order":515},{"name":"Jet Tila","character":"Self","img":null,"order":524},{"name":"Anne Burrell","character":"Self","img":null,"order":526},{"name":"Ted Allen","character":"Self","img":null,"order":534},{"name":"Simon Majumdar","character":"Self","img":null,"order":536},{"name":"Geoffrey Zakarian","character":"Self","img":null,"order":537},{"name":"Michael Voltaggio","character":"Self","img":null,"order":549},{"name":"Amanda Freitag","character":"Self","img":null,"order":569},{"name":"Katie Lee Biegel","character":"Self","img":null,"order":645}],"tv:1849":[{"name":"Robert Guillaume","character":"Benson DuBois","img":null,"order":0},{"name":"James Noble","character":"Eugene Gatling","img":null,"order":1},{"name":"Inga Swenson","character":"Gretchen Kraus","img":null,"order":2},{"name":"Caroline McWilliams","character":"Marcy Hill","img":null,"order":3},{"name":"Missy Gold","character":"Katie Gatling","img":null,"order":4},{"name":"Ethan Phillips","character":"Pete Downey","img":null,"order":5},{"name":"Didi Conn","character":"Denise Stevens","img":null,"order":6},{"name":"Lewis J. Stadlen","character":"John Taylor","img":null,"order":7},{"name":"Ren\u00e9 Auberjonois","character":"Clayton Endicott III","img":null,"order":8},{"name":"Billie Bird","character":"Rose Cassidy","img":null,"order":9},{"name":"Beverly Todd","character":"Casey Turner","img":null,"order":507},{"name":"Ren\u00e9 Enr\u00edquez","character":"","img":null,"order":509},{"name":"Norman Bartold","character":"","img":null,"order":513},{"name":"G.W. Bailey","character":"","img":null,"order":539},{"name":"Jerry Seinfeld","character":"Frankie","img":null,"order":552}],"tv:249039":[{"name":"Jason Bateman","character":"Vince Friedken","img":null,"order":0},{"name":"Jude Law","character":"Jake Friedken","img":null,"order":1},{"name":"Chris Coy","character":"Babbitt","img":null,"order":2},{"name":"Troy Kotsur","character":"Joe Mancuso","img":null,"order":3},{"name":"Abbey Lee","character":"Anna","img":null,"order":4},{"name":"Odessa Young","character":"Gen","img":null,"order":5},{"name":"Robin de Jes\u00fas","character":"Tony","img":null,"order":6},{"name":"Cleopatra Coleman","character":"Estelle","img":null,"order":500},{"name":"\u1e62\u1ecdp\u1eb9\u0301 D\u00ecr\u00eds\u00f9","character":"Wes","img":null,"order":501},{"name":"Amaka Okafor","character":"Roxie","img":null,"order":502},{"name":"Dagmara Dominczyk","character":"Val","img":null,"order":503},{"name":"Don Harvey","character":"Matt","img":null,"order":504},{"name":"Gus Birney","character":"Mel Whitney","img":null,"order":505},{"name":"Forrest Weber","character":"Junior","img":null,"order":506},{"name":"Michael Cash","character":"Hunter","img":null,"order":510}],"tv:61381":[{"name":"Anthony Anderson","character":"Andre Johnson","img":null,"order":0},{"name":"Tracee Ellis Ross","character":"Rainbow Johnson","img":null,"order":1},{"name":"Yara Shahidi","character":"Zoey Johnson","img":null,"order":2},{"name":"Marcus Scribner","character":"Junior Johnson","img":null,"order":3},{"name":"Marsai Martin","character":"Diane Johnson","img":null,"order":4},{"name":"Miles Brown","character":"Jack Johnson","img":null,"order":5},{"name":"Jenifer Lewis","character":"Ruby Johnson","img":null,"order":6},{"name":"Deon Cole","character":"Charlie Telphy","img":null,"order":7},{"name":"Peter MacKenzie","character":"Leslie Stevens","img":null,"order":8},{"name":"Jeff Meacham","character":"Josh Oppenhol","img":null,"order":9},{"name":"Katlyn Nichol","character":"Olivia Lockhart","img":null,"order":10},{"name":"Laurence Fishburne","character":"Pops Johnson","img":null,"order":500},{"name":"Nelson Franklin","character":"Connor Stevens","img":null,"order":534},{"name":"Berlin Gross","character":"Devante Johnson","img":null,"order":562},{"name":"August Gross","character":"Devante Johnson","img":null,"order":563}],"tv:1396":[{"name":"Bryan Cranston","character":"Walter White","img":null,"order":0},{"name":"Aaron Paul","character":"Jesse Pinkman","img":null,"order":1},{"name":"Anna Gunn","character":"Skyler White","img":null,"order":2},{"name":"RJ Mitte","character":"Walter White Jr.","img":null,"order":3},{"name":"Dean Norris","character":"Hank Schrader","img":null,"order":4},{"name":"Betsy Brandt","character":"Marie Schrader","img":null,"order":5},{"name":"Giancarlo Esposito","character":"Gustavo Fring","img":null,"order":6},{"name":"Bob Odenkirk","character":"Saul Goodman","img":null,"order":7},{"name":"Steven Michael Quezada","character":"Steven Gomez","img":null,"order":8},{"name":"Jonathan Banks","character":"Mike Ehrmantraut","img":null,"order":9},{"name":"Christopher Cousins","character":"Ted Beneke","img":null,"order":604},{"name":"Michael Shamus Wiles","character":"ASAC George Merkert","img":null,"order":606},{"name":"Lavell Crawford","character":"Huell","img":null,"order":637},{"name":"Charles Baker","character":"Skinny Pete","img":null,"order":656},{"name":"Matt Jones","character":"Badger","img":null,"order":658}],"tv:93166":[{"name":"Holliday Grainger","character":"DI Rachel Carey","img":null,"order":0},{"name":"Callum Turner","character":"Shaun Emery","img":null,"order":1},{"name":"Ben Miles","character":"Commander Danny Hart","img":null,"order":2},{"name":"Lia Williams","character":"DSU Gemma Garland","img":null,"order":3},{"name":"Ron Perlman","character":"Frank Napier","img":null,"order":4},{"name":"Laura Haddock","character":"Hannah Roberts","img":null,"order":5},{"name":"Barry Ward","character":"Charlie Hall","img":null,"order":6},{"name":"Ginny Holder","character":"DC / DI Nadia Latif","img":null,"order":7},{"name":"Cavan Clerkin","character":"DS Patrick Flynn","img":null,"order":8},{"name":"Indira Varma","character":"Khadija Khan","img":null,"order":10},{"name":"Tessa Wong","character":"DC Chloe Tan","img":null,"order":12},{"name":"Andy Nyman","character":"Rowan Gill","img":null,"order":13},{"name":"Daisy Waterstone","character":"Abigail Carey","img":null,"order":15},{"name":"Nigel Lindsay","character":"DSI / DSU Tom Kendricks","img":null,"order":106},{"name":"Paapa Essiedu","character":"Isaac Turner","img":null,"order":160}]},"availableArt":["assets/cast/26R1LBiSOkkP08saSsz6LmVTY16.jpg","assets/cast/2tFblvjBRUYwcVT1V1i79Ppd6wG.jpg","assets/cast/320qW5yEbxpmyxQ3evmClJbtKag.jpg","assets/cast/4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg","assets/cast/4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg","assets/cast/4gyHyg6FJ1oFczOm5pmMkdEEo2J.jpg","assets/cast/5L0kzTVITmOQdQ94NhicKM8ecCX.jpg","assets/cast/5VbadQsYCKdiJysP01LJFPmYB8A.jpg","assets/cast/5eIRHUbNOH7ibXa0IfUfFvj5I5L.jpg","assets/cast/5hn7WeXi6wguwxTPTSmcmzHAs9.jpg","assets/cast/6hmUs5Q1EETapn32yKNt3c3i1sn.jpg","assets/cast/7LVYeFTgUpjYDrxnU7Wm5ZQUX9t.jpg","assets/cast/7MHdNxS1gRnx0ZQGPrfS5eRjMe3.jpg","assets/cast/7Y96dAfg0HcFrcLjlD5eD9N0uj4.jpg","assets/cast/8v3lDa0QCJlDtOx9flqlMNIA3uf.jpg","assets/cast/9pxaoasotR1pdCXYSfN1pkm0geO.jpg","assets/cast/aHQk6XxqyJinfvrVRADEIg31nOk.jpg","assets/cast/aW6vCxkUZtwb6iH2Wf88Uq0XNVv.jpg","assets/cast/bLgmpSFgEiSFNshqmdl3EIZkRZm.jpg","assets/cast/biov5jENp2XYfehkcdnRvi796Wi.jpg","assets/cast/d81K0RH8UX7tZj49tZaQhZ9ewH.jpg","assets/cast/fD8GuLHOOSSAG98VjwN3NhAmqAJ.jpg","assets/cast/fbWWkhe7GVSlhBljY89rJd01OnA.jpg","assets/cast/fdFVatb9pt5dfT9tDK8tikKWafS.jpg","assets/cast/fjJc03z1cOlMxLZbC1qihsq3C37.jpg","assets/cast/hOntuFYAMwn9QaXT0AwLCPU79ce.jpg","assets/cast/hrTwOPWek7nkPJgFhh6inFu7vSS.jpg","assets/cast/hzu7OqTmNCT1xdCdmwBSf2yJvaO.jpg","assets/cast/iQLEawIP2593yrGy22GzLSpnfgl.jpg","assets/cast/j0GXzLelO1dOzI7d1WVIwQXhLz8.jpg","assets/cast/jUuUbPuMGonFT5E2pcs4alfqaCN.jpg","assets/cast/lF06PTNYrlXeJ3DYApvOcPXJr87.jpg","assets/cast/lUhVV9u5VGMCNdCAVhGzg8GFSdo.jpg","assets/cast/lawJUdbLb1YW0niaYjiVGR9IBG8.jpg","assets/cast/lfbyJWNWGbiYfU0Yr1tjn6Trd2V.jpg","assets/cast/lpEs9yDsMQlsbm0RVsvDIleCXcI.jpg","assets/cast/mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg","assets/cast/mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg","assets/cast/mk5vpYLmER7asqOhEwVKXD3P3YE.jpg","assets/cast/mnKYU7HIsuRkx3YjdRO5FLXCdX5.jpg","assets/cast/msqe5A1q5YUUND0RhzB9dw1AEAO.jpg","assets/cast/nAxS5l9vb1C8ltCp9yAa0f6lazu.jpg","assets/cast/nmgOd3X2Xn3jIp9OLCRJzLExRWN.jpg","assets/cast/o7splcOIVAdbHnwohJOUAvXWVlk.jpg","assets/cast/oMHMLhoV3ry8aOfdvs1kxyowhNq.jpg","assets/cast/omWXGbeRR32MWuYhpnLOSqJ6xkO.jpg","assets/cast/pVwR9hsEiHzVTo9giCjR3vIfa8W.jpg","assets/cast/phEIvq0Ay2KjI5ewmVvpQ1a29GJ.jpg","assets/cast/qP5vQMIbLNvSsSFkEMLZRVt4ZW5.jpg","assets/cast/qiWu2gphSg64igYl3Nx0hEIOQF3.jpg","assets/cast/qwbjcyDN37OHiln4a6P4LP31o7F.jpg","assets/cast/r6s7dkYZUJpd00C7p9itT6Gp8XB.jpg","assets/cast/rDTxblyw0AwU5oSeksgP7GwMyrF.jpg","assets/cast/rIqcwewXdkrDTMTwX56QPa7MNTC.jpg","assets/cast/rLzduQpYJnZJKkXv3jRc7MDHwoI.jpg","assets/cast/rbxBK2m6fEq0oHeMoBUMVYkJEdJ.jpg","assets/cast/sG6CWLdENCtzukNFkFYyPxqyRmS.jpg","assets/cast/sh4ANkKoIHgmHtkKQbJvF8y0Fjq.jpg","assets/cast/tqFHQiJUycuob3ytvilaAWDlbQ8.jpg","assets/cast/uL8yfcm0dCFYFQt052kczVQVvac.jpg","assets/cast/ub2IuMWFNQGYghHTPq0lpmn2Ue0.jpg","assets/cast/ukDNHgvEgdU95eaw6pngiAPucv4.jpg","assets/cast/v4sRxLpRpHEd9vbk43wm9VL0v0K.jpg","assets/cast/wWcby4ARYqr8d92ZdjyQcfaiJBW.jpg","assets/cast/waayy0jpNbxLDQ8JO1uRDng8SLF.jpg","assets/cast/x0lmqgkmphvOCSTHk2oxsaxA9Ra.jpg","assets/cast/xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg","assets/cast/xV9UrP8kZdxOiTI2efDFhp3t4iA.jpg","assets/cast/xm2pYFBggiXSkFBV2I612i3fDd3.jpg","assets/cast/xx3As5SWcE8vYOKZgtjDjqmT3jc.jpg","assets/cast/yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg","assets/cast/yWZILsr2mBQBCG1LOcCYYshIbts.jpg","assets/cast/yxGsAKnrJZs2g6q4unNgkfm7NAA.jpg","assets/cast/ztxDIZvuPreUixO6ZPxwkCg2UlO.jpg","assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg","assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg","assets/episode-announcement-2022-s1e7-backdrop.jpg","assets/episode-announcement-2022-s1e7-poster.jpg","assets/episode-episode-1-2025-s1e1-backdrop.jpg","assets/episode-episode-1-2025-s1e1-poster.jpg","assets/episode-episode-2-2016-s1e2-backdrop.jpg","assets/episode-episode-2-2016-s1e2-poster.jpg","assets/episode-erickerhead-2026-s9e6-backdrop.jpg","assets/episode-erickerhead-2026-s9e6-poster.jpg","assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg","assets/episode-girls-interrupted-2007-s1e7-poster.jpg","assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg","assets/episode-i-heart-ny-2002-s4e18-poster.jpg","assets/episode-new-day-2021-s1e2-backdrop.jpg","assets/episode-new-day-2021-s1e2-poster.jpg","assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg","assets/episode-no-honeymoon-2020-s1e8-poster.jpg","assets/episode-notapusy-2005-s3e4-backdrop.jpg","assets/episode-notapusy-2005-s3e4-poster.jpg","assets/episode-one-of-us-2024-s1e7-backdrop.jpg","assets/episode-one-of-us-2024-s1e7-poster.jpg","assets/episode-remembrance-day-2026-s2e1-backdrop.jpg","assets/episode-remembrance-day-2026-s2e1-poster.jpg","assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg","assets/episode-sex-and-the-country-2001-s4e9-poster.jpg","assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg","assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg","assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg","assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg","assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg","assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg","assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg","assets/episode-the-uncomfortability-2010-s1e2-poster.jpg","assets/episode-three-monsters-1992-s1e6-backdrop.jpg","assets/episode-three-monsters-1992-s1e6-poster.jpg","assets/episode-tomorrow-2024-s3e1-backdrop.jpg","assets/episode-tomorrow-2024-s3e1-poster.jpg","assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg","assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg","assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg","assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg","assets/movie-a-bronx-tale-1993-backdrop.jpg","assets/movie-a-bronx-tale-1993-poster.jpg","assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg","assets/movie-a-silent-voice-the-movie-2016-poster.jpg","assets/movie-absolution-2024-backdrop.jpg","assets/movie-absolution-2024-poster.jpg","assets/movie-across-the-hall-2009-backdrop.jpg","assets/movie-across-the-hall-2009-poster.jpg","assets/movie-addicted-2014-backdrop.jpg","assets/movie-addicted-2014-poster.jpg","assets/movie-adulterers-2016-backdrop.jpg","assets/movie-adulterers-2016-poster.jpg","assets/movie-all-the-money-in-the-world-2017-backdrop.jpg","assets/movie-all-the-money-in-the-world-2017-poster.jpg","assets/movie-alone-with-her-2006-backdrop.jpg","assets/movie-alone-with-her-2006-poster.jpg","assets/movie-american-gangster-2007-backdrop.jpg","assets/movie-american-gangster-2007-poster.jpg","assets/movie-annie-hall-1977-backdrop.jpg","assets/movie-annie-hall-1977-poster.jpg","assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg","assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg","assets/movie-appleseed-alpha-2014-backdrop.jpg","assets/movie-appleseed-alpha-2014-poster.jpg","assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg","assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg","assets/movie-arctic-dogs-2019-backdrop.jpg","assets/movie-arctic-dogs-2019-poster.jpg","assets/movie-arrival-2016-backdrop.jpg","assets/movie-arrival-2016-poster.jpg","assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg","assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg","assets/movie-avengers-age-of-ultron-2015-backdrop.jpg","assets/movie-avengers-age-of-ultron-2015-poster.jpg","assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg","assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg","assets/movie-avengers-infinity-war-2018-backdrop.jpg","assets/movie-avengers-infinity-war-2018-poster.jpg","assets/movie-awake-2007-backdrop.jpg","assets/movie-awake-2007-poster.jpg","assets/movie-batman-ninja-2018-backdrop.jpg","assets/movie-batman-ninja-2018-poster.jpg","assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg","assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg","assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg","assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg","assets/movie-black-adam-2022-backdrop.jpg","assets/movie-black-adam-2022-poster.jpg","assets/movie-black-bear-2020-backdrop.jpg","assets/movie-black-bear-2020-poster.jpg","assets/movie-black-panther-2018-backdrop.jpg","assets/movie-black-panther-2018-poster.jpg","assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg","assets/movie-black-panther-wakanda-forever-2022-poster.jpg","assets/movie-blame-2017-backdrop.jpg","assets/movie-blame-2017-poster.jpg","assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg","assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg","assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg","assets/movie-butterfly-on-a-wheel-2007-poster.jpg","assets/movie-captain-america-civil-war-2016-backdrop.jpg","assets/movie-captain-america-civil-war-2016-poster.jpg","assets/movie-careful-what-you-wish-for-2015-backdrop.jpg","assets/movie-careful-what-you-wish-for-2015-poster.jpg","assets/movie-castle-in-the-sky-1986-backdrop.jpg","assets/movie-castle-in-the-sky-1986-poster.jpg","assets/movie-chicago-2002-backdrop.jpg","assets/movie-chicago-2002-poster.jpg","assets/movie-citizen-kane-1941-backdrop.jpg","assets/movie-citizen-kane-1941-poster.jpg","assets/movie-conclave-2024-backdrop.jpg","assets/movie-conclave-2024-poster.jpg","assets/movie-crime-of-passion-1956-backdrop.jpg","assets/movie-crime-of-passion-1956-poster.jpg","assets/movie-deep-water-2022-backdrop.jpg","assets/movie-deep-water-2022-poster.jpg","assets/movie-derailed-2005-backdrop.jpg","assets/movie-derailed-2005-poster.jpg","assets/movie-dream-house-2011-backdrop.jpg","assets/movie-dream-house-2011-poster.jpg","assets/movie-dream-lover-1994-backdrop.jpg","assets/movie-dream-lover-1994-poster.jpg","assets/movie-drop-2025-backdrop.jpg","assets/movie-drop-2025-poster.jpg","assets/movie-eastern-promises-2007-backdrop.jpg","assets/movie-eastern-promises-2007-poster.jpg","assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg","assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg","assets/movie-fatale-2020-backdrop.jpg","assets/movie-fatale-2020-poster.jpg","assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg","assets/movie-force-of-nature-the-dry-2-2024-poster.jpg","assets/movie-ghost-in-the-shell-1995-backdrop.jpg","assets/movie-ghost-in-the-shell-1995-poster.jpg","assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg","assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg","assets/movie-gosford-park-2001-backdrop.jpg","assets/movie-gosford-park-2001-poster.jpg","assets/movie-howl-s-moving-castle-2004-backdrop.jpg","assets/movie-howl-s-moving-castle-2004-poster.jpg","assets/movie-in-the-heights-2021-backdrop.jpg","assets/movie-in-the-heights-2021-poster.jpg","assets/movie-kiki-s-delivery-service-1989-backdrop.jpg","assets/movie-kiki-s-delivery-service-1989-poster.jpg","assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg","assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg","assets/movie-locke-2014-backdrop.jpg","assets/movie-locke-2014-poster.jpg","assets/movie-memories-1995-backdrop.jpg","assets/movie-memories-1995-poster.jpg","assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg","assets/movie-night-is-short-walk-on-girl-2017-poster.jpg","assets/movie-panic-room-2002-backdrop.jpg","assets/movie-panic-room-2002-poster.jpg","assets/movie-ponyo-2008-backdrop.jpg","assets/movie-ponyo-2008-poster.jpg","assets/movie-porco-rosso-1992-backdrop.jpg","assets/movie-porco-rosso-1992-poster.jpg","assets/movie-princess-mononoke-1997-backdrop.jpg","assets/movie-princess-mononoke-1997-poster.jpg","assets/movie-prisoners-2013-backdrop.jpg","assets/movie-prisoners-2013-poster.jpg","assets/movie-racing-stripes-2005-backdrop.jpg","assets/movie-racing-stripes-2005-poster.jpg","assets/movie-secondhand-lions-2003-backdrop.jpg","assets/movie-secondhand-lions-2003-poster.jpg","assets/movie-shaft-2000-backdrop.jpg","assets/movie-shaft-2000-poster.jpg","assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg","assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg","assets/movie-sleeping-with-other-people-2015-backdrop.jpg","assets/movie-sleeping-with-other-people-2015-poster.jpg","assets/movie-snowpiercer-2013-backdrop.jpg","assets/movie-snowpiercer-2013-poster.jpg","assets/movie-spider-man-brand-new-day-2026-backdrop.jpg","assets/movie-spider-man-brand-new-day-2026-poster.jpg","assets/movie-spider-man-far-from-home-2019-backdrop.jpg","assets/movie-spider-man-far-from-home-2019-poster.jpg","assets/movie-spider-man-homecoming-2017-backdrop.jpg","assets/movie-spider-man-homecoming-2017-poster.jpg","assets/movie-spider-man-no-way-home-2021-backdrop.jpg","assets/movie-spider-man-no-way-home-2021-poster.jpg","assets/movie-spirited-away-2001-backdrop.jpg","assets/movie-spirited-away-2001-poster.jpg","assets/movie-the-alto-knights-2025-backdrop.jpg","assets/movie-the-alto-knights-2025-poster.jpg","assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg","assets/movie-the-amazing-spider-man-2-2014-poster.jpg","assets/movie-the-amazing-spider-man-2012-backdrop.jpg","assets/movie-the-amazing-spider-man-2012-poster.jpg","assets/movie-the-animatrix-2003-backdrop.jpg","assets/movie-the-animatrix-2003-poster.jpg","assets/movie-the-avengers-2012-backdrop.jpg","assets/movie-the-avengers-2012-poster.jpg","assets/movie-the-boy-and-the-heron-2023-backdrop.jpg","assets/movie-the-boy-and-the-heron-2023-poster.jpg","assets/movie-the-breakfast-club-1985-backdrop.jpg","assets/movie-the-breakfast-club-1985-poster.jpg","assets/movie-the-dry-2021-backdrop.jpg","assets/movie-the-dry-2021-poster.jpg","assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg","assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg","assets/movie-the-godfather-1972-backdrop.jpg","assets/movie-the-godfather-1972-poster.jpg","assets/movie-the-godfather-part-iii-1990-backdrop.jpg","assets/movie-the-godfather-part-iii-1990-poster.jpg","assets/movie-the-great-gatsby-2013-backdrop.jpg","assets/movie-the-great-gatsby-2013-poster.jpg","assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg","assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg","assets/movie-the-lighthouse-2019-backdrop.jpg","assets/movie-the-lighthouse-2019-poster.jpg","assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg","assets/movie-the-secret-life-of-pets-2-2019-poster.jpg","assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg","assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg","assets/movie-the-wild-robot-2024-backdrop.jpg","assets/movie-the-wild-robot-2024-poster.jpg","assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg","assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg","assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg","assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg","assets/movie-thelma-louise-1991-backdrop.jpg","assets/movie-thelma-louise-1991-poster.jpg","assets/movie-triangle-of-sadness-2022-backdrop.jpg","assets/movie-triangle-of-sadness-2022-poster.jpg","assets/movie-vice-2018-backdrop.jpg","assets/movie-vice-2018-poster.jpg","assets/tv-12-monkeys-2015-backdrop.jpg","assets/tv-12-monkeys-2015-poster.jpg","assets/tv-56-days-2026-backdrop.jpg","assets/tv-56-days-2026-poster.jpg","assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg","assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg","assets/tv-absolutely-fabulous-1992-backdrop.jpg","assets/tv-absolutely-fabulous-1992-poster.jpg","assets/tv-accel-world-2012-backdrop.jpg","assets/tv-accel-world-2012-poster.jpg","assets/tv-accused-2023-backdrop.jpg","assets/tv-accused-2023-poster.jpg","assets/tv-after-life-2019-backdrop.jpg","assets/tv-after-life-2019-poster.jpg","assets/tv-agatha-christie-s-marple-2004-backdrop.jpg","assets/tv-agatha-christie-s-marple-2004-poster.jpg","assets/tv-ahsoka-2023-backdrop.jpg","assets/tv-ahsoka-2023-poster.jpg","assets/tv-all-creatures-great-and-small-1978-backdrop.jpg","assets/tv-all-creatures-great-and-small-1978-poster.jpg","assets/tv-all-creatures-great-small-2020-backdrop.jpg","assets/tv-all-creatures-great-small-2020-poster.jpg","assets/tv-american-dad-2005-backdrop.jpg","assets/tv-american-dad-2005-poster.jpg","assets/tv-american-gods-2017-backdrop.jpg","assets/tv-american-gods-2017-poster.jpg","assets/tv-american-housewife-2016-backdrop.jpg","assets/tv-american-housewife-2016-poster.jpg","assets/tv-american-primeval-2025-backdrop.jpg","assets/tv-american-primeval-2025-poster.jpg","assets/tv-and-just-like-that-2021-backdrop.jpg","assets/tv-and-just-like-that-2021-poster.jpg","assets/tv-andor-2022-backdrop.jpg","assets/tv-andor-2022-poster.jpg","assets/tv-andromeda-2000-backdrop.jpg","assets/tv-andromeda-2000-poster.jpg","assets/tv-anne-with-an-e-2017-backdrop.jpg","assets/tv-anne-with-an-e-2017-poster.jpg","assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg","assets/tv-aqua-teen-hunger-force-2000-poster.jpg","assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg","assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg","assets/tv-arrow-2012-backdrop.jpg","assets/tv-arrow-2012-poster.jpg","assets/tv-atlantis-2013-backdrop.jpg","assets/tv-atlantis-2013-poster.jpg","assets/tv-avatar-seven-havens-2026-backdrop.jpg","assets/tv-avatar-seven-havens-2026-poster.jpg","assets/tv-avatar-the-last-airbender-2005-backdrop.jpg","assets/tv-avatar-the-last-airbender-2005-poster.jpg","assets/tv-avatar-the-last-airbender-2024-backdrop.jpg","assets/tv-avatar-the-last-airbender-2024-poster.jpg","assets/tv-awkward-2011-backdrop.jpg","assets/tv-awkward-2011-poster.jpg","assets/tv-back-to-life-2019-backdrop.jpg","assets/tv-back-to-life-2019-poster.jpg","assets/tv-ballers-2015-backdrop.jpg","assets/tv-ballers-2015-poster.jpg","assets/tv-baretta-1975-backdrop.jpg","assets/tv-baretta-1975-poster.jpg","assets/tv-based-on-a-true-story-2023-backdrop.jpg","assets/tv-based-on-a-true-story-2023-poster.jpg","assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg","assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg","assets/tv-batwoman-2019-backdrop.jpg","assets/tv-batwoman-2019-poster.jpg","assets/tv-big-boys-2022-backdrop.jpg","assets/tv-big-boys-2022-poster.jpg","assets/tv-big-city-greens-2018-backdrop.jpg","assets/tv-big-city-greens-2018-poster.jpg","assets/tv-big-mouth-2017-backdrop.jpg","assets/tv-big-mouth-2017-poster.jpg","assets/tv-black-lightning-2018-backdrop.jpg","assets/tv-black-lightning-2018-poster.jpg","assets/tv-black-rabbit-2025-backdrop.jpg","assets/tv-black-rabbit-2025-poster.jpg","assets/tv-bleach-2004-backdrop.jpg","assets/tv-bleach-2004-poster.jpg","assets/tv-brassic-2019-backdrop.jpg","assets/tv-brassic-2019-poster.jpg","assets/tv-breaking-bad-2008-backdrop.jpg","assets/tv-breaking-bad-2008-poster.jpg","assets/tv-californication-2007-backdrop.jpg","assets/tv-californication-2007-poster.jpg","assets/tv-chainsaw-man-2022-backdrop.jpg","assets/tv-chainsaw-man-2022-poster.jpg","assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg","assets/tv-cyberpunk-edgerunners-2022-poster.jpg","assets/tv-derry-girls-2018-backdrop.jpg","assets/tv-derry-girls-2018-poster.jpg","assets/tv-desperate-housewives-2004-backdrop.jpg","assets/tv-desperate-housewives-2004-poster.jpg","assets/tv-dune-prophecy-2024-backdrop.jpg","assets/tv-dune-prophecy-2024-poster.jpg","assets/tv-etoile-2025-backdrop.jpg","assets/tv-etoile-2025-poster.jpg","assets/tv-fallout-2024-backdrop.jpg","assets/tv-fallout-2024-poster.jpg","assets/tv-firefly-2002-backdrop.jpg","assets/tv-firefly-2002-poster.jpg","assets/tv-fleabag-2016-backdrop.jpg","assets/tv-fleabag-2016-poster.jpg","assets/tv-gilmore-girls-2000-backdrop.jpg","assets/tv-gilmore-girls-2000-poster.jpg","assets/tv-high-potential-2024-backdrop.jpg","assets/tv-high-potential-2024-poster.jpg","assets/tv-invincible-2021-backdrop.jpg","assets/tv-invincible-2021-poster.jpg","assets/tv-last-samurai-standing-2025-backdrop.jpg","assets/tv-last-samurai-standing-2025-poster.jpg","assets/tv-lord-of-the-flies-2026-backdrop.jpg","assets/tv-lord-of-the-flies-2026-poster.jpg","assets/tv-love-death-robots-2019-backdrop.jpg","assets/tv-love-death-robots-2019-poster.jpg","assets/tv-maniac-2018-backdrop.jpg","assets/tv-maniac-2018-poster.jpg","assets/tv-neon-genesis-evangelion-1995-backdrop.jpg","assets/tv-neon-genesis-evangelion-1995-poster.jpg","assets/tv-norsemen-2016-backdrop.jpg","assets/tv-norsemen-2016-poster.jpg","assets/tv-one-piece-2023-backdrop.jpg","assets/tv-one-piece-2023-poster.jpg","assets/tv-only-murders-in-the-building-2021-backdrop.jpg","assets/tv-only-murders-in-the-building-2021-poster.jpg","assets/tv-over-the-garden-wall-2014-backdrop.jpg","assets/tv-over-the-garden-wall-2014-poster.jpg","assets/tv-pluribus-2025-backdrop.jpg","assets/tv-pluribus-2025-poster.jpg","assets/tv-rick-and-morty-2013-backdrop.jpg","assets/tv-rick-and-morty-2013-poster.jpg","assets/tv-secret-level-2024-backdrop.jpg","assets/tv-secret-level-2024-poster.jpg","assets/tv-severance-2022-backdrop.jpg","assets/tv-severance-2022-poster.jpg","assets/tv-sex-and-the-city-1998-backdrop.jpg","assets/tv-sex-and-the-city-1998-poster.jpg","assets/tv-the-100-2014-backdrop.jpg","assets/tv-the-100-2014-poster.jpg","assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg","assets/tv-the-abbott-and-costello-show-1952-poster.jpg","assets/tv-the-afterparty-2022-backdrop.jpg","assets/tv-the-afterparty-2022-poster.jpg","assets/tv-the-bear-2022-backdrop.jpg","assets/tv-the-bear-2022-poster.jpg","assets/tv-the-beast-in-me-2025-backdrop.jpg","assets/tv-the-beast-in-me-2025-poster.jpg","assets/tv-the-bold-type-2017-backdrop.jpg","assets/tv-the-bold-type-2017-poster.jpg","assets/tv-the-boys-2019-backdrop.jpg","assets/tv-the-boys-2019-poster.jpg","assets/tv-the-capture-2019-backdrop.jpg","assets/tv-the-capture-2019-poster.jpg","assets/tv-the-durrells-2016-backdrop.jpg","assets/tv-the-durrells-2016-poster.jpg","assets/tv-the-final-problem-2026-backdrop.jpg","assets/tv-the-final-problem-2026-poster.jpg","assets/tv-the-legend-of-korra-2012-backdrop.jpg","assets/tv-the-legend-of-korra-2012-poster.jpg","assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg","assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg","assets/tv-the-night-manager-2016-backdrop.jpg","assets/tv-the-night-manager-2016-poster.jpg","assets/tv-the-tatami-galaxy-2010-backdrop.jpg","assets/tv-the-tatami-galaxy-2010-poster.jpg","assets/tv-the-white-lotus-2021-backdrop.jpg","assets/tv-the-white-lotus-2021-poster.jpg","assets/tv-the-witcher-2019-backdrop.jpg","assets/tv-the-witcher-2019-poster.jpg","assets/tv-upload-2020-backdrop.jpg","assets/tv-upload-2020-poster.jpg","assets/tv-widow-s-bay-2026-backdrop.jpg","assets/tv-widow-s-bay-2026-poster.jpg","assets/tv-yellowjackets-2021-backdrop.jpg","assets/tv-yellowjackets-2021-poster.jpg"]} diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/26R1LBiSOkkP08saSsz6LmVTY16.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/26R1LBiSOkkP08saSsz6LmVTY16.jpg deleted file mode 100644 index 12a79fbe..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/26R1LBiSOkkP08saSsz6LmVTY16.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/2tFblvjBRUYwcVT1V1i79Ppd6wG.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/2tFblvjBRUYwcVT1V1i79Ppd6wG.jpg deleted file mode 100644 index 51c11420..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/2tFblvjBRUYwcVT1V1i79Ppd6wG.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/320qW5yEbxpmyxQ3evmClJbtKag.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/320qW5yEbxpmyxQ3evmClJbtKag.jpg deleted file mode 100644 index 0c5b151f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/320qW5yEbxpmyxQ3evmClJbtKag.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg deleted file mode 100644 index a639becf..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/4F8XpjyQCvtuu21WFm5d8RF5Rl.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg deleted file mode 100644 index f0a4e83a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/4eOoW1PNBZfxRW0g8RmYH8bL6mq.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/4gyHyg6FJ1oFczOm5pmMkdEEo2J.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/4gyHyg6FJ1oFczOm5pmMkdEEo2J.jpg deleted file mode 100644 index b28de566..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/4gyHyg6FJ1oFczOm5pmMkdEEo2J.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/5L0kzTVITmOQdQ94NhicKM8ecCX.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/5L0kzTVITmOQdQ94NhicKM8ecCX.jpg deleted file mode 100644 index 35a785df..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/5L0kzTVITmOQdQ94NhicKM8ecCX.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/5VbadQsYCKdiJysP01LJFPmYB8A.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/5VbadQsYCKdiJysP01LJFPmYB8A.jpg deleted file mode 100644 index 41618a2e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/5VbadQsYCKdiJysP01LJFPmYB8A.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/5eIRHUbNOH7ibXa0IfUfFvj5I5L.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/5eIRHUbNOH7ibXa0IfUfFvj5I5L.jpg deleted file mode 100644 index 5c6e94de..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/5eIRHUbNOH7ibXa0IfUfFvj5I5L.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/5hn7WeXi6wguwxTPTSmcmzHAs9.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/5hn7WeXi6wguwxTPTSmcmzHAs9.jpg deleted file mode 100644 index 1b59f62f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/5hn7WeXi6wguwxTPTSmcmzHAs9.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/6hmUs5Q1EETapn32yKNt3c3i1sn.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/6hmUs5Q1EETapn32yKNt3c3i1sn.jpg deleted file mode 100644 index 21dc4e89..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/6hmUs5Q1EETapn32yKNt3c3i1sn.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/7LVYeFTgUpjYDrxnU7Wm5ZQUX9t.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/7LVYeFTgUpjYDrxnU7Wm5ZQUX9t.jpg deleted file mode 100644 index dde817a1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/7LVYeFTgUpjYDrxnU7Wm5ZQUX9t.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/7MHdNxS1gRnx0ZQGPrfS5eRjMe3.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/7MHdNxS1gRnx0ZQGPrfS5eRjMe3.jpg deleted file mode 100644 index 5094cb73..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/7MHdNxS1gRnx0ZQGPrfS5eRjMe3.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/7Y96dAfg0HcFrcLjlD5eD9N0uj4.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/7Y96dAfg0HcFrcLjlD5eD9N0uj4.jpg deleted file mode 100644 index 1f88cd10..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/7Y96dAfg0HcFrcLjlD5eD9N0uj4.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/8v3lDa0QCJlDtOx9flqlMNIA3uf.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/8v3lDa0QCJlDtOx9flqlMNIA3uf.jpg deleted file mode 100644 index 5d5930b7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/8v3lDa0QCJlDtOx9flqlMNIA3uf.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/9pxaoasotR1pdCXYSfN1pkm0geO.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/9pxaoasotR1pdCXYSfN1pkm0geO.jpg deleted file mode 100644 index 6a483975..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/9pxaoasotR1pdCXYSfN1pkm0geO.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/aHQk6XxqyJinfvrVRADEIg31nOk.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/aHQk6XxqyJinfvrVRADEIg31nOk.jpg deleted file mode 100644 index a1df3481..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/aHQk6XxqyJinfvrVRADEIg31nOk.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/aW6vCxkUZtwb6iH2Wf88Uq0XNVv.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/aW6vCxkUZtwb6iH2Wf88Uq0XNVv.jpg deleted file mode 100644 index 50197dbe..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/aW6vCxkUZtwb6iH2Wf88Uq0XNVv.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/bLgmpSFgEiSFNshqmdl3EIZkRZm.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/bLgmpSFgEiSFNshqmdl3EIZkRZm.jpg deleted file mode 100644 index 5aa08a58..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/bLgmpSFgEiSFNshqmdl3EIZkRZm.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/biov5jENp2XYfehkcdnRvi796Wi.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/biov5jENp2XYfehkcdnRvi796Wi.jpg deleted file mode 100644 index 07813572..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/biov5jENp2XYfehkcdnRvi796Wi.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/d81K0RH8UX7tZj49tZaQhZ9ewH.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/d81K0RH8UX7tZj49tZaQhZ9ewH.jpg deleted file mode 100644 index 77d5fe66..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/d81K0RH8UX7tZj49tZaQhZ9ewH.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/fD8GuLHOOSSAG98VjwN3NhAmqAJ.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/fD8GuLHOOSSAG98VjwN3NhAmqAJ.jpg deleted file mode 100644 index 3c66e9a4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/fD8GuLHOOSSAG98VjwN3NhAmqAJ.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/fbWWkhe7GVSlhBljY89rJd01OnA.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/fbWWkhe7GVSlhBljY89rJd01OnA.jpg deleted file mode 100644 index 15a25098..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/fbWWkhe7GVSlhBljY89rJd01OnA.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/fdFVatb9pt5dfT9tDK8tikKWafS.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/fdFVatb9pt5dfT9tDK8tikKWafS.jpg deleted file mode 100644 index 60531535..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/fdFVatb9pt5dfT9tDK8tikKWafS.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/fjJc03z1cOlMxLZbC1qihsq3C37.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/fjJc03z1cOlMxLZbC1qihsq3C37.jpg deleted file mode 100644 index 989ce70d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/fjJc03z1cOlMxLZbC1qihsq3C37.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/hOntuFYAMwn9QaXT0AwLCPU79ce.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/hOntuFYAMwn9QaXT0AwLCPU79ce.jpg deleted file mode 100644 index 783d7043..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/hOntuFYAMwn9QaXT0AwLCPU79ce.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/hrTwOPWek7nkPJgFhh6inFu7vSS.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/hrTwOPWek7nkPJgFhh6inFu7vSS.jpg deleted file mode 100644 index 5f9dc239..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/hrTwOPWek7nkPJgFhh6inFu7vSS.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/hzu7OqTmNCT1xdCdmwBSf2yJvaO.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/hzu7OqTmNCT1xdCdmwBSf2yJvaO.jpg deleted file mode 100644 index 2b3d2015..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/hzu7OqTmNCT1xdCdmwBSf2yJvaO.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/iQLEawIP2593yrGy22GzLSpnfgl.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/iQLEawIP2593yrGy22GzLSpnfgl.jpg deleted file mode 100644 index 5965e06d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/iQLEawIP2593yrGy22GzLSpnfgl.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/j0GXzLelO1dOzI7d1WVIwQXhLz8.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/j0GXzLelO1dOzI7d1WVIwQXhLz8.jpg deleted file mode 100644 index 0921d96c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/j0GXzLelO1dOzI7d1WVIwQXhLz8.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/jUuUbPuMGonFT5E2pcs4alfqaCN.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/jUuUbPuMGonFT5E2pcs4alfqaCN.jpg deleted file mode 100644 index 7e03f82b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/jUuUbPuMGonFT5E2pcs4alfqaCN.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/lF06PTNYrlXeJ3DYApvOcPXJr87.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/lF06PTNYrlXeJ3DYApvOcPXJr87.jpg deleted file mode 100644 index 01f41d25..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/lF06PTNYrlXeJ3DYApvOcPXJr87.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/lUhVV9u5VGMCNdCAVhGzg8GFSdo.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/lUhVV9u5VGMCNdCAVhGzg8GFSdo.jpg deleted file mode 100644 index 8baff3f2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/lUhVV9u5VGMCNdCAVhGzg8GFSdo.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/lawJUdbLb1YW0niaYjiVGR9IBG8.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/lawJUdbLb1YW0niaYjiVGR9IBG8.jpg deleted file mode 100644 index 11d18b7a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/lawJUdbLb1YW0niaYjiVGR9IBG8.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/lfbyJWNWGbiYfU0Yr1tjn6Trd2V.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/lfbyJWNWGbiYfU0Yr1tjn6Trd2V.jpg deleted file mode 100644 index 64a37106..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/lfbyJWNWGbiYfU0Yr1tjn6Trd2V.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/lpEs9yDsMQlsbm0RVsvDIleCXcI.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/lpEs9yDsMQlsbm0RVsvDIleCXcI.jpg deleted file mode 100644 index 5ff2d6a1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/lpEs9yDsMQlsbm0RVsvDIleCXcI.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg deleted file mode 100644 index 2a4445ce..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/mF5YJxApTYKBVFJDrEC9IrJ5ooV.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg deleted file mode 100644 index 21dd5ffe..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/mW7xmtGV4y79kQGn0zkKVGDMAmw.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/mk5vpYLmER7asqOhEwVKXD3P3YE.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/mk5vpYLmER7asqOhEwVKXD3P3YE.jpg deleted file mode 100644 index b4189cd0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/mk5vpYLmER7asqOhEwVKXD3P3YE.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/mnKYU7HIsuRkx3YjdRO5FLXCdX5.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/mnKYU7HIsuRkx3YjdRO5FLXCdX5.jpg deleted file mode 100644 index 40aee939..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/mnKYU7HIsuRkx3YjdRO5FLXCdX5.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/msqe5A1q5YUUND0RhzB9dw1AEAO.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/msqe5A1q5YUUND0RhzB9dw1AEAO.jpg deleted file mode 100644 index afc0e401..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/msqe5A1q5YUUND0RhzB9dw1AEAO.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/nAxS5l9vb1C8ltCp9yAa0f6lazu.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/nAxS5l9vb1C8ltCp9yAa0f6lazu.jpg deleted file mode 100644 index 2e179e76..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/nAxS5l9vb1C8ltCp9yAa0f6lazu.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/nmgOd3X2Xn3jIp9OLCRJzLExRWN.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/nmgOd3X2Xn3jIp9OLCRJzLExRWN.jpg deleted file mode 100644 index 5a261d60..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/nmgOd3X2Xn3jIp9OLCRJzLExRWN.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/o7splcOIVAdbHnwohJOUAvXWVlk.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/o7splcOIVAdbHnwohJOUAvXWVlk.jpg deleted file mode 100644 index 4cdadf08..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/o7splcOIVAdbHnwohJOUAvXWVlk.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/oMHMLhoV3ry8aOfdvs1kxyowhNq.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/oMHMLhoV3ry8aOfdvs1kxyowhNq.jpg deleted file mode 100644 index 99b5bcd6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/oMHMLhoV3ry8aOfdvs1kxyowhNq.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/omWXGbeRR32MWuYhpnLOSqJ6xkO.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/omWXGbeRR32MWuYhpnLOSqJ6xkO.jpg deleted file mode 100644 index 1ee336bd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/omWXGbeRR32MWuYhpnLOSqJ6xkO.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/pVwR9hsEiHzVTo9giCjR3vIfa8W.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/pVwR9hsEiHzVTo9giCjR3vIfa8W.jpg deleted file mode 100644 index 4dbe126b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/pVwR9hsEiHzVTo9giCjR3vIfa8W.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/phEIvq0Ay2KjI5ewmVvpQ1a29GJ.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/phEIvq0Ay2KjI5ewmVvpQ1a29GJ.jpg deleted file mode 100644 index b673a5b2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/phEIvq0Ay2KjI5ewmVvpQ1a29GJ.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/qP5vQMIbLNvSsSFkEMLZRVt4ZW5.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/qP5vQMIbLNvSsSFkEMLZRVt4ZW5.jpg deleted file mode 100644 index ed34c316..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/qP5vQMIbLNvSsSFkEMLZRVt4ZW5.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/qiWu2gphSg64igYl3Nx0hEIOQF3.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/qiWu2gphSg64igYl3Nx0hEIOQF3.jpg deleted file mode 100644 index e5fd3f84..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/qiWu2gphSg64igYl3Nx0hEIOQF3.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/qwbjcyDN37OHiln4a6P4LP31o7F.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/qwbjcyDN37OHiln4a6P4LP31o7F.jpg deleted file mode 100644 index 4132aac4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/qwbjcyDN37OHiln4a6P4LP31o7F.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/r6s7dkYZUJpd00C7p9itT6Gp8XB.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/r6s7dkYZUJpd00C7p9itT6Gp8XB.jpg deleted file mode 100644 index 8ae18ac0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/r6s7dkYZUJpd00C7p9itT6Gp8XB.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/rDTxblyw0AwU5oSeksgP7GwMyrF.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/rDTxblyw0AwU5oSeksgP7GwMyrF.jpg deleted file mode 100644 index 8d6c9a83..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/rDTxblyw0AwU5oSeksgP7GwMyrF.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/rIqcwewXdkrDTMTwX56QPa7MNTC.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/rIqcwewXdkrDTMTwX56QPa7MNTC.jpg deleted file mode 100644 index 0b985c84..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/rIqcwewXdkrDTMTwX56QPa7MNTC.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/rLzduQpYJnZJKkXv3jRc7MDHwoI.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/rLzduQpYJnZJKkXv3jRc7MDHwoI.jpg deleted file mode 100644 index 7377c541..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/rLzduQpYJnZJKkXv3jRc7MDHwoI.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/rbxBK2m6fEq0oHeMoBUMVYkJEdJ.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/rbxBK2m6fEq0oHeMoBUMVYkJEdJ.jpg deleted file mode 100644 index f23bdc49..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/rbxBK2m6fEq0oHeMoBUMVYkJEdJ.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/sG6CWLdENCtzukNFkFYyPxqyRmS.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/sG6CWLdENCtzukNFkFYyPxqyRmS.jpg deleted file mode 100644 index 755a5276..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/sG6CWLdENCtzukNFkFYyPxqyRmS.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/sh4ANkKoIHgmHtkKQbJvF8y0Fjq.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/sh4ANkKoIHgmHtkKQbJvF8y0Fjq.jpg deleted file mode 100644 index c1a9d397..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/sh4ANkKoIHgmHtkKQbJvF8y0Fjq.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/tqFHQiJUycuob3ytvilaAWDlbQ8.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/tqFHQiJUycuob3ytvilaAWDlbQ8.jpg deleted file mode 100644 index 5b2de159..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/tqFHQiJUycuob3ytvilaAWDlbQ8.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/uL8yfcm0dCFYFQt052kczVQVvac.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/uL8yfcm0dCFYFQt052kczVQVvac.jpg deleted file mode 100644 index 4f1e2f21..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/uL8yfcm0dCFYFQt052kczVQVvac.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/ub2IuMWFNQGYghHTPq0lpmn2Ue0.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/ub2IuMWFNQGYghHTPq0lpmn2Ue0.jpg deleted file mode 100644 index 725372ad..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/ub2IuMWFNQGYghHTPq0lpmn2Ue0.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/ukDNHgvEgdU95eaw6pngiAPucv4.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/ukDNHgvEgdU95eaw6pngiAPucv4.jpg deleted file mode 100644 index 7ed17793..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/ukDNHgvEgdU95eaw6pngiAPucv4.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/v4sRxLpRpHEd9vbk43wm9VL0v0K.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/v4sRxLpRpHEd9vbk43wm9VL0v0K.jpg deleted file mode 100644 index 2118c505..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/v4sRxLpRpHEd9vbk43wm9VL0v0K.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/wWcby4ARYqr8d92ZdjyQcfaiJBW.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/wWcby4ARYqr8d92ZdjyQcfaiJBW.jpg deleted file mode 100644 index 3a609d1e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/wWcby4ARYqr8d92ZdjyQcfaiJBW.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/waayy0jpNbxLDQ8JO1uRDng8SLF.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/waayy0jpNbxLDQ8JO1uRDng8SLF.jpg deleted file mode 100644 index b650362b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/waayy0jpNbxLDQ8JO1uRDng8SLF.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/x0lmqgkmphvOCSTHk2oxsaxA9Ra.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/x0lmqgkmphvOCSTHk2oxsaxA9Ra.jpg deleted file mode 100644 index 3d3f33ee..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/x0lmqgkmphvOCSTHk2oxsaxA9Ra.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg deleted file mode 100644 index fba488ba..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/xKBAaPIa1c7tzZD3Y0MhBLv4hPE.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/xV9UrP8kZdxOiTI2efDFhp3t4iA.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/xV9UrP8kZdxOiTI2efDFhp3t4iA.jpg deleted file mode 100644 index eb015896..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/xV9UrP8kZdxOiTI2efDFhp3t4iA.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/xm2pYFBggiXSkFBV2I612i3fDd3.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/xm2pYFBggiXSkFBV2I612i3fDd3.jpg deleted file mode 100644 index bc482f04..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/xm2pYFBggiXSkFBV2I612i3fDd3.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/xx3As5SWcE8vYOKZgtjDjqmT3jc.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/xx3As5SWcE8vYOKZgtjDjqmT3jc.jpg deleted file mode 100644 index dd70f23a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/xx3As5SWcE8vYOKZgtjDjqmT3jc.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg deleted file mode 100644 index dd0796e7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/yU0OXeZ0K1ppe1o9SpHiAMHy1rc.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/yWZILsr2mBQBCG1LOcCYYshIbts.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/yWZILsr2mBQBCG1LOcCYYshIbts.jpg deleted file mode 100644 index 04d476bb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/yWZILsr2mBQBCG1LOcCYYshIbts.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/yxGsAKnrJZs2g6q4unNgkfm7NAA.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/yxGsAKnrJZs2g6q4unNgkfm7NAA.jpg deleted file mode 100644 index 6e7a492b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/yxGsAKnrJZs2g6q4unNgkfm7NAA.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/cast/ztxDIZvuPreUixO6ZPxwkCg2UlO.jpg b/app/src/debug/assets/wolfskin-titles/assets/cast/ztxDIZvuPreUixO6ZPxwkCg2UlO.jpg deleted file mode 100644 index 6883cc09..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/cast/ztxDIZvuPreUixO6ZPxwkCg2UlO.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg deleted file mode 100644 index f5b2f94c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg deleted file mode 100644 index cf571b93..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-all-hands-on-deck-class-1-a-2021-s5e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-announcement-2022-s1e7-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-announcement-2022-s1e7-backdrop.jpg deleted file mode 100644 index a874a0c0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-announcement-2022-s1e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-announcement-2022-s1e7-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-announcement-2022-s1e7-poster.jpg deleted file mode 100644 index df5dc498..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-announcement-2022-s1e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-1-2025-s1e1-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-episode-1-2025-s1e1-backdrop.jpg deleted file mode 100644 index f2519b04..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-1-2025-s1e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-1-2025-s1e1-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-episode-1-2025-s1e1-poster.jpg deleted file mode 100644 index 77febd79..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-1-2025-s1e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-2-2016-s1e2-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-episode-2-2016-s1e2-backdrop.jpg deleted file mode 100644 index 3f8bdeec..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-2-2016-s1e2-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-2-2016-s1e2-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-episode-2-2016-s1e2-poster.jpg deleted file mode 100644 index d29448bb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-episode-2-2016-s1e2-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-erickerhead-2026-s9e6-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-erickerhead-2026-s9e6-backdrop.jpg deleted file mode 100644 index 1c4c0ffc..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-erickerhead-2026-s9e6-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-erickerhead-2026-s9e6-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-erickerhead-2026-s9e6-poster.jpg deleted file mode 100644 index b95c4890..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-erickerhead-2026-s9e6-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg deleted file mode 100644 index ffff979b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-girls-interrupted-2007-s1e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-girls-interrupted-2007-s1e7-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-girls-interrupted-2007-s1e7-poster.jpg deleted file mode 100644 index 38428e76..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-girls-interrupted-2007-s1e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg deleted file mode 100644 index 57beacc1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-i-heart-ny-2002-s4e18-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-i-heart-ny-2002-s4e18-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-i-heart-ny-2002-s4e18-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-i-heart-ny-2002-s4e18-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-new-day-2021-s1e2-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-new-day-2021-s1e2-backdrop.jpg deleted file mode 100644 index 1f747323..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-new-day-2021-s1e2-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-new-day-2021-s1e2-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-new-day-2021-s1e2-poster.jpg deleted file mode 100644 index 2f23d1fa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-new-day-2021-s1e2-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg deleted file mode 100644 index 9d95ba77..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-no-honeymoon-2020-s1e8-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-no-honeymoon-2020-s1e8-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-no-honeymoon-2020-s1e8-poster.jpg deleted file mode 100644 index 49c61208..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-no-honeymoon-2020-s1e8-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-notapusy-2005-s3e4-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-notapusy-2005-s3e4-backdrop.jpg deleted file mode 100644 index ecb7e8cd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-notapusy-2005-s3e4-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-notapusy-2005-s3e4-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-notapusy-2005-s3e4-poster.jpg deleted file mode 100644 index 02b0dd43..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-notapusy-2005-s3e4-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-one-of-us-2024-s1e7-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-one-of-us-2024-s1e7-backdrop.jpg deleted file mode 100644 index 66bb4d00..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-one-of-us-2024-s1e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-one-of-us-2024-s1e7-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-one-of-us-2024-s1e7-poster.jpg deleted file mode 100644 index 00a07ca7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-one-of-us-2024-s1e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-remembrance-day-2026-s2e1-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-remembrance-day-2026-s2e1-backdrop.jpg deleted file mode 100644 index 23a242da..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-remembrance-day-2026-s2e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-remembrance-day-2026-s2e1-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-remembrance-day-2026-s2e1-poster.jpg deleted file mode 100644 index 930943b0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-remembrance-day-2026-s2e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg deleted file mode 100644 index 57beacc1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-sex-and-the-country-2001-s4e9-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-sex-and-the-country-2001-s4e9-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-sex-and-the-country-2001-s4e9-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-sex-and-the-country-2001-s4e9-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg deleted file mode 100644 index cd0aec39..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-south-american-biker-gangs-2026-s29e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg deleted file mode 100644 index d8568d54..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-south-american-biker-gangs-2026-s29e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg deleted file mode 100644 index b42ba953..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-the-breakup-part-2-2001-s1e17-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg deleted file mode 100644 index 23b668cb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-the-breakup-part-2-2001-s1e17-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg deleted file mode 100644 index 84944d39..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-the-man-who-would-be-vogue-2018-s2e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg deleted file mode 100644 index 4086bf17..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-the-man-who-would-be-vogue-2018-s2e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg deleted file mode 100644 index bc22e11a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-the-uncomfortability-2010-s1e2-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-the-uncomfortability-2010-s1e2-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-the-uncomfortability-2010-s1e2-poster.jpg deleted file mode 100644 index ae305043..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-the-uncomfortability-2010-s1e2-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-three-monsters-1992-s1e6-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-three-monsters-1992-s1e6-backdrop.jpg deleted file mode 100644 index ba65ca2d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-three-monsters-1992-s1e6-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-three-monsters-1992-s1e6-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-three-monsters-1992-s1e6-poster.jpg deleted file mode 100644 index f5843766..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-three-monsters-1992-s1e6-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-tomorrow-2024-s3e1-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-tomorrow-2024-s3e1-backdrop.jpg deleted file mode 100644 index 9c9cca57..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-tomorrow-2024-s3e1-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-tomorrow-2024-s3e1-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-tomorrow-2024-s3e1-poster.jpg deleted file mode 100644 index bbf0ab28..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-tomorrow-2024-s3e1-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg deleted file mode 100644 index 40ec3001..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-what-more-do-i-need-2008-s5e7-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg deleted file mode 100644 index 13f0f7b3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/episode-what-more-do-i-need-2008-s5e7-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg deleted file mode 100644 index f0d6f752..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg deleted file mode 100644 index b9b72894..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-101-dalmatians-ii-patch-s-london-adventure-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-a-bronx-tale-1993-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-a-bronx-tale-1993-backdrop.jpg deleted file mode 100644 index 15c11ff7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-a-bronx-tale-1993-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-a-bronx-tale-1993-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-a-bronx-tale-1993-poster.jpg deleted file mode 100644 index f7444271..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-a-bronx-tale-1993-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg deleted file mode 100644 index b386b136..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-a-silent-voice-the-movie-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-a-silent-voice-the-movie-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-a-silent-voice-the-movie-2016-poster.jpg deleted file mode 100644 index 580b22f4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-a-silent-voice-the-movie-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-absolution-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-absolution-2024-backdrop.jpg deleted file mode 100644 index eb28037c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-absolution-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-absolution-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-absolution-2024-poster.jpg deleted file mode 100644 index 3ce49112..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-absolution-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-across-the-hall-2009-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-across-the-hall-2009-backdrop.jpg deleted file mode 100644 index 6c1ee8ac..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-across-the-hall-2009-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-across-the-hall-2009-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-across-the-hall-2009-poster.jpg deleted file mode 100644 index 52362b4e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-across-the-hall-2009-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-addicted-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-addicted-2014-backdrop.jpg deleted file mode 100644 index 5c016976..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-addicted-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-addicted-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-addicted-2014-poster.jpg deleted file mode 100644 index e9a03c14..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-addicted-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-adulterers-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-adulterers-2016-backdrop.jpg deleted file mode 100644 index 331391ed..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-adulterers-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-adulterers-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-adulterers-2016-poster.jpg deleted file mode 100644 index 8b65745b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-adulterers-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-all-the-money-in-the-world-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-all-the-money-in-the-world-2017-backdrop.jpg deleted file mode 100644 index ac4b828a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-all-the-money-in-the-world-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-all-the-money-in-the-world-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-all-the-money-in-the-world-2017-poster.jpg deleted file mode 100644 index 8beb2f7f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-all-the-money-in-the-world-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-alone-with-her-2006-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-alone-with-her-2006-backdrop.jpg deleted file mode 100644 index ddf8f1dc..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-alone-with-her-2006-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-alone-with-her-2006-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-alone-with-her-2006-poster.jpg deleted file mode 100644 index 7ce6fa96..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-alone-with-her-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-american-gangster-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-american-gangster-2007-backdrop.jpg deleted file mode 100644 index cb5c5de7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-american-gangster-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-american-gangster-2007-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-american-gangster-2007-poster.jpg deleted file mode 100644 index 0f41d299..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-american-gangster-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-annie-hall-1977-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-annie-hall-1977-backdrop.jpg deleted file mode 100644 index 16d0b27d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-annie-hall-1977-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-annie-hall-1977-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-annie-hall-1977-poster.jpg deleted file mode 100644 index 9023a691..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-annie-hall-1977-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg deleted file mode 100644 index 2a99846d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ant-man-and-the-wasp-quantumania-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg deleted file mode 100644 index 030addf0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ant-man-and-the-wasp-quantumania-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-appleseed-alpha-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-appleseed-alpha-2014-backdrop.jpg deleted file mode 100644 index 82579b86..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-appleseed-alpha-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-appleseed-alpha-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-appleseed-alpha-2014-poster.jpg deleted file mode 100644 index 62bd0afa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-appleseed-alpha-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg deleted file mode 100644 index 6693cb6d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-aquaman-and-the-lost-kingdom-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg deleted file mode 100644 index 4550bdaa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-aquaman-and-the-lost-kingdom-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-arctic-dogs-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-arctic-dogs-2019-backdrop.jpg deleted file mode 100644 index 1794c99d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-arctic-dogs-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-arctic-dogs-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-arctic-dogs-2019-poster.jpg deleted file mode 100644 index 07552443..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-arctic-dogs-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-arrival-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-arrival-2016-backdrop.jpg deleted file mode 100644 index 605724e7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-arrival-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-arrival-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-arrival-2016-poster.jpg deleted file mode 100644 index fe224a15..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-arrival-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg deleted file mode 100644 index d3b12fe0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avatar-aang-the-last-airbender-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg deleted file mode 100644 index f57f1052..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avatar-aang-the-last-airbender-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-age-of-ultron-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-age-of-ultron-2015-backdrop.jpg deleted file mode 100644 index f739d620..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-age-of-ultron-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-age-of-ultron-2015-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-age-of-ultron-2015-poster.jpg deleted file mode 100644 index 300840ff..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-age-of-ultron-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg deleted file mode 100644 index 8163931a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-confidential-black-widow-punisher-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg deleted file mode 100644 index b096fff2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-confidential-black-widow-punisher-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-infinity-war-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-infinity-war-2018-backdrop.jpg deleted file mode 100644 index 90dc4672..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-infinity-war-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-infinity-war-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-infinity-war-2018-poster.jpg deleted file mode 100644 index 57df9298..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-avengers-infinity-war-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-awake-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-awake-2007-backdrop.jpg deleted file mode 100644 index 6ddd7f59..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-awake-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-awake-2007-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-awake-2007-poster.jpg deleted file mode 100644 index 32595897..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-awake-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-ninja-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-batman-ninja-2018-backdrop.jpg deleted file mode 100644 index 9b538d9f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-ninja-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-ninja-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-batman-ninja-2018-poster.jpg deleted file mode 100644 index 7f59127b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-ninja-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg deleted file mode 100644 index 9d385801..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-the-dark-knight-returns-part-1-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg deleted file mode 100644 index ce5623fe..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-the-dark-knight-returns-part-1-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg deleted file mode 100644 index b4fa63b9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-v-superman-dawn-of-justice-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg deleted file mode 100644 index f8115170..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-batman-v-superman-dawn-of-justice-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-adam-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-adam-2022-backdrop.jpg deleted file mode 100644 index 3232fb22..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-adam-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-adam-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-adam-2022-poster.jpg deleted file mode 100644 index dde008b5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-adam-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-bear-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-bear-2020-backdrop.jpg deleted file mode 100644 index 4b0b262f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-bear-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-bear-2020-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-bear-2020-poster.jpg deleted file mode 100644 index 61c8860f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-bear-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-2018-backdrop.jpg deleted file mode 100644 index f0b8de8a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-2018-poster.jpg deleted file mode 100644 index 6fdaa3d2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg deleted file mode 100644 index ac7ad532..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-wakanda-forever-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-wakanda-forever-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-wakanda-forever-2022-poster.jpg deleted file mode 100644 index d0219a75..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-black-panther-wakanda-forever-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-blame-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-blame-2017-backdrop.jpg deleted file mode 100644 index f39cf5ce..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-blame-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-blame-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-blame-2017-poster.jpg deleted file mode 100644 index 8d50255f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-blame-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg deleted file mode 100644 index f426d0ce..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-bleach-the-movie-fade-to-black-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg deleted file mode 100644 index d3619fbd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-bleach-the-movie-fade-to-black-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg deleted file mode 100644 index e49fe1d5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-butterfly-on-a-wheel-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-butterfly-on-a-wheel-2007-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-butterfly-on-a-wheel-2007-poster.jpg deleted file mode 100644 index b54cc5c7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-butterfly-on-a-wheel-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-captain-america-civil-war-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-captain-america-civil-war-2016-backdrop.jpg deleted file mode 100644 index 5700c4e7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-captain-america-civil-war-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-captain-america-civil-war-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-captain-america-civil-war-2016-poster.jpg deleted file mode 100644 index 426142ef..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-captain-america-civil-war-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-careful-what-you-wish-for-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-careful-what-you-wish-for-2015-backdrop.jpg deleted file mode 100644 index 988e20d5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-careful-what-you-wish-for-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-careful-what-you-wish-for-2015-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-careful-what-you-wish-for-2015-poster.jpg deleted file mode 100644 index b58ce600..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-careful-what-you-wish-for-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-castle-in-the-sky-1986-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-castle-in-the-sky-1986-backdrop.jpg deleted file mode 100644 index f785c549..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-castle-in-the-sky-1986-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-castle-in-the-sky-1986-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-castle-in-the-sky-1986-poster.jpg deleted file mode 100644 index a6a99fee..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-castle-in-the-sky-1986-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-chicago-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-chicago-2002-backdrop.jpg deleted file mode 100644 index 6b3d02a3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-chicago-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-chicago-2002-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-chicago-2002-poster.jpg deleted file mode 100644 index 1def9414..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-chicago-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-citizen-kane-1941-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-citizen-kane-1941-backdrop.jpg deleted file mode 100644 index d41a10ef..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-citizen-kane-1941-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-citizen-kane-1941-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-citizen-kane-1941-poster.jpg deleted file mode 100644 index f26414a7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-citizen-kane-1941-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-conclave-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-conclave-2024-backdrop.jpg deleted file mode 100644 index db0a7fcd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-conclave-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-conclave-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-conclave-2024-poster.jpg deleted file mode 100644 index c6aecefa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-conclave-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-crime-of-passion-1956-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-crime-of-passion-1956-backdrop.jpg deleted file mode 100644 index 6c70a452..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-crime-of-passion-1956-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-crime-of-passion-1956-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-crime-of-passion-1956-poster.jpg deleted file mode 100644 index 4acfa871..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-crime-of-passion-1956-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-deep-water-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-deep-water-2022-backdrop.jpg deleted file mode 100644 index bec98992..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-deep-water-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-deep-water-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-deep-water-2022-poster.jpg deleted file mode 100644 index 018d4f4b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-deep-water-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-derailed-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-derailed-2005-backdrop.jpg deleted file mode 100644 index e958449b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-derailed-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-derailed-2005-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-derailed-2005-poster.jpg deleted file mode 100644 index d93f3124..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-derailed-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-house-2011-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-dream-house-2011-backdrop.jpg deleted file mode 100644 index 29ecc103..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-house-2011-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-house-2011-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-dream-house-2011-poster.jpg deleted file mode 100644 index d3444637..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-house-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-lover-1994-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-dream-lover-1994-backdrop.jpg deleted file mode 100644 index 7f5a31ac..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-lover-1994-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-lover-1994-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-dream-lover-1994-poster.jpg deleted file mode 100644 index 3b563cf9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-dream-lover-1994-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-drop-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-drop-2025-backdrop.jpg deleted file mode 100644 index ee642530..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-drop-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-drop-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-drop-2025-poster.jpg deleted file mode 100644 index 4cf7d66b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-drop-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-eastern-promises-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-eastern-promises-2007-backdrop.jpg deleted file mode 100644 index 9096a0fa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-eastern-promises-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-eastern-promises-2007-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-eastern-promises-2007-poster.jpg deleted file mode 100644 index 01a26f3b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-eastern-promises-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg deleted file mode 100644 index 8edc02a6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-el-camino-a-breaking-bad-movie-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg deleted file mode 100644 index 7a62df45..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-el-camino-a-breaking-bad-movie-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-fatale-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-fatale-2020-backdrop.jpg deleted file mode 100644 index b13c33c6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-fatale-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-fatale-2020-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-fatale-2020-poster.jpg deleted file mode 100644 index dda9d622..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-fatale-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg deleted file mode 100644 index 3735ee63..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-force-of-nature-the-dry-2-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg deleted file mode 100644 index 93ba0d71..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-force-of-nature-the-dry-2-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-1995-backdrop.jpg deleted file mode 100644 index 716b8a87..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-1995-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-1995-poster.jpg deleted file mode 100644 index 58d221ba..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg deleted file mode 100644 index 4a9931ed..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-2-innocence-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg deleted file mode 100644 index ae262147..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ghost-in-the-shell-2-innocence-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-gosford-park-2001-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-gosford-park-2001-backdrop.jpg deleted file mode 100644 index 42c4cea6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-gosford-park-2001-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-gosford-park-2001-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-gosford-park-2001-poster.jpg deleted file mode 100644 index 6a6aa651..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-gosford-park-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-howl-s-moving-castle-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-howl-s-moving-castle-2004-backdrop.jpg deleted file mode 100644 index 0aeef2ba..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-howl-s-moving-castle-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-howl-s-moving-castle-2004-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-howl-s-moving-castle-2004-poster.jpg deleted file mode 100644 index 042a6c80..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-howl-s-moving-castle-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-in-the-heights-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-in-the-heights-2021-backdrop.jpg deleted file mode 100644 index 7cd4ee24..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-in-the-heights-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-in-the-heights-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-in-the-heights-2021-poster.jpg deleted file mode 100644 index e05bfdb0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-in-the-heights-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-kiki-s-delivery-service-1989-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-kiki-s-delivery-service-1989-backdrop.jpg deleted file mode 100644 index 2a108f0c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-kiki-s-delivery-service-1989-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-kiki-s-delivery-service-1989-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-kiki-s-delivery-service-1989-poster.jpg deleted file mode 100644 index 80a59b1e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-kiki-s-delivery-service-1989-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg deleted file mode 100644 index 38203b77..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg deleted file mode 100644 index 4c78fee9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-lemony-snicket-s-a-series-of-unfortunate-events-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-locke-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-locke-2014-backdrop.jpg deleted file mode 100644 index a24e64ab..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-locke-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-locke-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-locke-2014-poster.jpg deleted file mode 100644 index 94ebe97d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-locke-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-memories-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-memories-1995-backdrop.jpg deleted file mode 100644 index 2fcaeaad..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-memories-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-memories-1995-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-memories-1995-poster.jpg deleted file mode 100644 index f3725b42..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-memories-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg deleted file mode 100644 index da38be48..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-night-is-short-walk-on-girl-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg deleted file mode 100644 index c6fa6a26..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-night-is-short-walk-on-girl-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-panic-room-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-panic-room-2002-backdrop.jpg deleted file mode 100644 index ec877751..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-panic-room-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-panic-room-2002-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-panic-room-2002-poster.jpg deleted file mode 100644 index 0b2a0d30..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-panic-room-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ponyo-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ponyo-2008-backdrop.jpg deleted file mode 100644 index 8aa4ecbd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ponyo-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-ponyo-2008-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-ponyo-2008-poster.jpg deleted file mode 100644 index f548e831..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-ponyo-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-porco-rosso-1992-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-porco-rosso-1992-backdrop.jpg deleted file mode 100644 index 63d8112e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-porco-rosso-1992-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-porco-rosso-1992-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-porco-rosso-1992-poster.jpg deleted file mode 100644 index 4a6a2bd1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-porco-rosso-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-princess-mononoke-1997-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-princess-mononoke-1997-backdrop.jpg deleted file mode 100644 index 9486d2cf..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-princess-mononoke-1997-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-princess-mononoke-1997-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-princess-mononoke-1997-poster.jpg deleted file mode 100644 index 21dd9359..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-princess-mononoke-1997-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-prisoners-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-prisoners-2013-backdrop.jpg deleted file mode 100644 index 724008c5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-prisoners-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-prisoners-2013-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-prisoners-2013-poster.jpg deleted file mode 100644 index 411b149b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-prisoners-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-racing-stripes-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-racing-stripes-2005-backdrop.jpg deleted file mode 100644 index 1d332cec..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-racing-stripes-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-racing-stripes-2005-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-racing-stripes-2005-poster.jpg deleted file mode 100644 index cc2ef2ce..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-racing-stripes-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-secondhand-lions-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-secondhand-lions-2003-backdrop.jpg deleted file mode 100644 index 7ea43937..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-secondhand-lions-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-secondhand-lions-2003-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-secondhand-lions-2003-poster.jpg deleted file mode 100644 index 33408fa0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-secondhand-lions-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-shaft-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-shaft-2000-backdrop.jpg deleted file mode 100644 index 13f14f93..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-shaft-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-shaft-2000-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-shaft-2000-poster.jpg deleted file mode 100644 index 6b2b79ce..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-shaft-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg deleted file mode 100644 index d04685fe..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-sinbad-legend-of-the-seven-seas-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg deleted file mode 100644 index 428bb38b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-sinbad-legend-of-the-seven-seas-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-sleeping-with-other-people-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-sleeping-with-other-people-2015-backdrop.jpg deleted file mode 100644 index df5bb046..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-sleeping-with-other-people-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-sleeping-with-other-people-2015-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-sleeping-with-other-people-2015-poster.jpg deleted file mode 100644 index a5ffff1f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-sleeping-with-other-people-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-snowpiercer-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-snowpiercer-2013-backdrop.jpg deleted file mode 100644 index c1281a9c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-snowpiercer-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-snowpiercer-2013-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-snowpiercer-2013-poster.jpg deleted file mode 100644 index be1873fd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-snowpiercer-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-brand-new-day-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-brand-new-day-2026-backdrop.jpg deleted file mode 100644 index 2fdaadb4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-brand-new-day-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-brand-new-day-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-brand-new-day-2026-poster.jpg deleted file mode 100644 index 51fa155f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-brand-new-day-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-far-from-home-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-far-from-home-2019-backdrop.jpg deleted file mode 100644 index d09f655a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-far-from-home-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-far-from-home-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-far-from-home-2019-poster.jpg deleted file mode 100644 index 28521997..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-far-from-home-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-homecoming-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-homecoming-2017-backdrop.jpg deleted file mode 100644 index 6088fadf..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-homecoming-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-homecoming-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-homecoming-2017-poster.jpg deleted file mode 100644 index 97fc9c05..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-homecoming-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-no-way-home-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-no-way-home-2021-backdrop.jpg deleted file mode 100644 index a76afec9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-no-way-home-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-no-way-home-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-no-way-home-2021-poster.jpg deleted file mode 100644 index bc92a4cd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spider-man-no-way-home-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spirited-away-2001-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spirited-away-2001-backdrop.jpg deleted file mode 100644 index af91c123..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spirited-away-2001-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-spirited-away-2001-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-spirited-away-2001-poster.jpg deleted file mode 100644 index 5a79da62..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-spirited-away-2001-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-alto-knights-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-alto-knights-2025-backdrop.jpg deleted file mode 100644 index ea619c14..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-alto-knights-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-alto-knights-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-alto-knights-2025-poster.jpg deleted file mode 100644 index bb009cbf..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-alto-knights-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg deleted file mode 100644 index 7c5878b7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2-2014-poster.jpg deleted file mode 100644 index 4de2711f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2012-backdrop.jpg deleted file mode 100644 index 75ae9b64..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2012-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2012-poster.jpg deleted file mode 100644 index c1aec876..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-amazing-spider-man-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-animatrix-2003-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-animatrix-2003-backdrop.jpg deleted file mode 100644 index 7d67ccb0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-animatrix-2003-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-animatrix-2003-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-animatrix-2003-poster.jpg deleted file mode 100644 index 65553d47..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-animatrix-2003-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-avengers-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-avengers-2012-backdrop.jpg deleted file mode 100644 index 83636f25..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-avengers-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-avengers-2012-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-avengers-2012-poster.jpg deleted file mode 100644 index be87067c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-avengers-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-boy-and-the-heron-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-boy-and-the-heron-2023-backdrop.jpg deleted file mode 100644 index a19dc77b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-boy-and-the-heron-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-boy-and-the-heron-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-boy-and-the-heron-2023-poster.jpg deleted file mode 100644 index 094df64e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-boy-and-the-heron-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-breakfast-club-1985-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-breakfast-club-1985-backdrop.jpg deleted file mode 100644 index 9f979cd8..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-breakfast-club-1985-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-breakfast-club-1985-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-breakfast-club-1985-poster.jpg deleted file mode 100644 index d1823120..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-breakfast-club-1985-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-dry-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-dry-2021-backdrop.jpg deleted file mode 100644 index 2f016498..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-dry-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-dry-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-dry-2021-poster.jpg deleted file mode 100644 index a0379f2f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-dry-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg deleted file mode 100644 index 6e920467..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-girl-who-leapt-through-time-2006-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg deleted file mode 100644 index ded6cd2a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-girl-who-leapt-through-time-2006-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-1972-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-1972-backdrop.jpg deleted file mode 100644 index 8fbb8f2a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-1972-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-1972-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-1972-poster.jpg deleted file mode 100644 index 94111d9b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-1972-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-part-iii-1990-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-part-iii-1990-backdrop.jpg deleted file mode 100644 index d2080487..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-part-iii-1990-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-part-iii-1990-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-part-iii-1990-poster.jpg deleted file mode 100644 index abbe5e99..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-godfather-part-iii-1990-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-great-gatsby-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-great-gatsby-2013-backdrop.jpg deleted file mode 100644 index 7a4f8a32..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-great-gatsby-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-great-gatsby-2013-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-great-gatsby-2013-poster.jpg deleted file mode 100644 index e949fb3e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-great-gatsby-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg deleted file mode 100644 index dd755ee8..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-killing-of-a-sacred-deer-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg deleted file mode 100644 index 83464d4c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-killing-of-a-sacred-deer-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-lighthouse-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-lighthouse-2019-backdrop.jpg deleted file mode 100644 index 862c064a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-lighthouse-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-lighthouse-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-lighthouse-2019-poster.jpg deleted file mode 100644 index 15f26533..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-lighthouse-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg deleted file mode 100644 index 698f4ad2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-secret-life-of-pets-2-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg deleted file mode 100644 index fe50c2f8..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-secret-life-of-pets-2-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg deleted file mode 100644 index b9157b91..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-tale-of-the-princess-kaguya-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg deleted file mode 100644 index 882621cb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-tale-of-the-princess-kaguya-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-wild-robot-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-wild-robot-2024-backdrop.jpg deleted file mode 100644 index bb48460b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-wild-robot-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-wild-robot-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-wild-robot-2024-poster.jpg deleted file mode 100644 index 0894754d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-wild-robot-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg deleted file mode 100644 index cb5bfb28..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-nightmare-of-the-wolf-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg deleted file mode 100644 index 6db33db1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-nightmare-of-the-wolf-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg deleted file mode 100644 index aad8ea2c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-sirens-of-the-deep-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg deleted file mode 100644 index 8a54bc7f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-the-witcher-sirens-of-the-deep-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-thelma-louise-1991-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-thelma-louise-1991-backdrop.jpg deleted file mode 100644 index a35327a9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-thelma-louise-1991-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-thelma-louise-1991-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-thelma-louise-1991-poster.jpg deleted file mode 100644 index 5a4ff0ab..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-thelma-louise-1991-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-triangle-of-sadness-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-triangle-of-sadness-2022-backdrop.jpg deleted file mode 100644 index 7c9b445d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-triangle-of-sadness-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-triangle-of-sadness-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-triangle-of-sadness-2022-poster.jpg deleted file mode 100644 index aee092c6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-triangle-of-sadness-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-vice-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-vice-2018-backdrop.jpg deleted file mode 100644 index 2918db3a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-vice-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/movie-vice-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/movie-vice-2018-poster.jpg deleted file mode 100644 index 76855126..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/movie-vice-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-12-monkeys-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-12-monkeys-2015-backdrop.jpg deleted file mode 100644 index 3d2825f5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-12-monkeys-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-12-monkeys-2015-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-12-monkeys-2015-poster.jpg deleted file mode 100644 index 96e3596e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-12-monkeys-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-56-days-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-56-days-2026-backdrop.jpg deleted file mode 100644 index dcda667a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-56-days-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-56-days-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-56-days-2026-poster.jpg deleted file mode 100644 index 64ca587b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-56-days-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg deleted file mode 100644 index ce12fc71..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-a-knight-of-the-seven-kingdoms-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg deleted file mode 100644 index 79f1351c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-a-knight-of-the-seven-kingdoms-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-absolutely-fabulous-1992-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-absolutely-fabulous-1992-backdrop.jpg deleted file mode 100644 index 2df9722d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-absolutely-fabulous-1992-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-absolutely-fabulous-1992-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-absolutely-fabulous-1992-poster.jpg deleted file mode 100644 index afd371a9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-absolutely-fabulous-1992-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-accel-world-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-accel-world-2012-backdrop.jpg deleted file mode 100644 index 1f5f6bdc..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-accel-world-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-accel-world-2012-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-accel-world-2012-poster.jpg deleted file mode 100644 index 207b1dd1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-accel-world-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-accused-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-accused-2023-backdrop.jpg deleted file mode 100644 index d41c6397..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-accused-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-accused-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-accused-2023-poster.jpg deleted file mode 100644 index 74af010a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-accused-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-after-life-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-after-life-2019-backdrop.jpg deleted file mode 100644 index eaf9a4fa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-after-life-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-after-life-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-after-life-2019-poster.jpg deleted file mode 100644 index 4fae4e33..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-after-life-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-agatha-christie-s-marple-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-agatha-christie-s-marple-2004-backdrop.jpg deleted file mode 100644 index 0c52f2e6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-agatha-christie-s-marple-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-agatha-christie-s-marple-2004-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-agatha-christie-s-marple-2004-poster.jpg deleted file mode 100644 index ea7dda99..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-agatha-christie-s-marple-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-ahsoka-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-ahsoka-2023-backdrop.jpg deleted file mode 100644 index 7343fb32..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-ahsoka-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-ahsoka-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-ahsoka-2023-poster.jpg deleted file mode 100644 index a29d6cb4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-ahsoka-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-and-small-1978-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-and-small-1978-backdrop.jpg deleted file mode 100644 index 909ae1ba..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-and-small-1978-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-and-small-1978-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-and-small-1978-poster.jpg deleted file mode 100644 index 5b1666e6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-and-small-1978-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-small-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-small-2020-backdrop.jpg deleted file mode 100644 index d2bcc88d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-small-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-small-2020-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-small-2020-poster.jpg deleted file mode 100644 index b506f990..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-all-creatures-great-small-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-dad-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-dad-2005-backdrop.jpg deleted file mode 100644 index 75a1948b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-dad-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-dad-2005-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-dad-2005-poster.jpg deleted file mode 100644 index 1f9e239a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-dad-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-gods-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-gods-2017-backdrop.jpg deleted file mode 100644 index 4817c690..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-gods-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-gods-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-gods-2017-poster.jpg deleted file mode 100644 index c02c46da..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-gods-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-housewife-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-housewife-2016-backdrop.jpg deleted file mode 100644 index bf758ae0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-housewife-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-housewife-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-housewife-2016-poster.jpg deleted file mode 100644 index 5f92bf05..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-housewife-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-primeval-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-primeval-2025-backdrop.jpg deleted file mode 100644 index c3bfe5e1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-primeval-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-american-primeval-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-american-primeval-2025-poster.jpg deleted file mode 100644 index 7f10e6aa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-american-primeval-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-and-just-like-that-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-and-just-like-that-2021-backdrop.jpg deleted file mode 100644 index 59c981e5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-and-just-like-that-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-and-just-like-that-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-and-just-like-that-2021-poster.jpg deleted file mode 100644 index 9904c74b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-and-just-like-that-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-andor-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-andor-2022-backdrop.jpg deleted file mode 100644 index a874a0c0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-andor-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-andor-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-andor-2022-poster.jpg deleted file mode 100644 index df5dc498..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-andor-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-andromeda-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-andromeda-2000-backdrop.jpg deleted file mode 100644 index 199249d7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-andromeda-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-andromeda-2000-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-andromeda-2000-poster.jpg deleted file mode 100644 index 67b766b0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-andromeda-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-anne-with-an-e-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-anne-with-an-e-2017-backdrop.jpg deleted file mode 100644 index 15e325c9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-anne-with-an-e-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-anne-with-an-e-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-anne-with-an-e-2017-poster.jpg deleted file mode 100644 index 55d443b4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-anne-with-an-e-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg deleted file mode 100644 index d0e37959..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-aqua-teen-hunger-force-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-aqua-teen-hunger-force-2000-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-aqua-teen-hunger-force-2000-poster.jpg deleted file mode 100644 index e0c97321..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-aqua-teen-hunger-force-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg deleted file mode 100644 index 9300e20f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg deleted file mode 100644 index 6a8f9a24..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-arifureta-from-commonplace-to-world-s-strongest-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-arrow-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-arrow-2012-backdrop.jpg deleted file mode 100644 index b32f5aca..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-arrow-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-arrow-2012-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-arrow-2012-poster.jpg deleted file mode 100644 index adee3bb7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-arrow-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-atlantis-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-atlantis-2013-backdrop.jpg deleted file mode 100644 index 39ecc0d6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-atlantis-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-atlantis-2013-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-atlantis-2013-poster.jpg deleted file mode 100644 index 5cd5f098..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-atlantis-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-seven-havens-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-seven-havens-2026-backdrop.jpg deleted file mode 100644 index dacfd59e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-seven-havens-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-seven-havens-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-seven-havens-2026-poster.jpg deleted file mode 100644 index 61f459c6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-seven-havens-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2005-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2005-backdrop.jpg deleted file mode 100644 index f60120da..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2005-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2005-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2005-poster.jpg deleted file mode 100644 index 6f99263a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2005-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2024-backdrop.jpg deleted file mode 100644 index ad740218..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2024-poster.jpg deleted file mode 100644 index 9d46c2ff..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-avatar-the-last-airbender-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-awkward-2011-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-awkward-2011-backdrop.jpg deleted file mode 100644 index 1ca5e56c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-awkward-2011-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-awkward-2011-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-awkward-2011-poster.jpg deleted file mode 100644 index 8da6e8e2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-awkward-2011-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-back-to-life-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-back-to-life-2019-backdrop.jpg deleted file mode 100644 index 7ffef681..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-back-to-life-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-back-to-life-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-back-to-life-2019-poster.jpg deleted file mode 100644 index b00730a8..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-back-to-life-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-ballers-2015-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-ballers-2015-backdrop.jpg deleted file mode 100644 index c4685e84..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-ballers-2015-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-ballers-2015-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-ballers-2015-poster.jpg deleted file mode 100644 index f1c18f69..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-ballers-2015-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-baretta-1975-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-baretta-1975-backdrop.jpg deleted file mode 100644 index 4d61dfbe..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-baretta-1975-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-baretta-1975-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-baretta-1975-poster.jpg deleted file mode 100644 index ceef0c9d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-baretta-1975-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-based-on-a-true-story-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-based-on-a-true-story-2023-backdrop.jpg deleted file mode 100644 index b59c0e58..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-based-on-a-true-story-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-based-on-a-true-story-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-based-on-a-true-story-2023-poster.jpg deleted file mode 100644 index 386d3d90..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-based-on-a-true-story-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg deleted file mode 100644 index 69e567cb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-batman-the-brave-and-the-bold-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg deleted file mode 100644 index a864f50c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-batman-the-brave-and-the-bold-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-batwoman-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-batwoman-2019-backdrop.jpg deleted file mode 100644 index d04a1a20..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-batwoman-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-batwoman-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-batwoman-2019-poster.jpg deleted file mode 100644 index 7ca86592..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-batwoman-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-big-boys-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-big-boys-2022-backdrop.jpg deleted file mode 100644 index c7c91e2d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-big-boys-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-big-boys-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-big-boys-2022-poster.jpg deleted file mode 100644 index 93e1e86a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-big-boys-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-big-city-greens-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-big-city-greens-2018-backdrop.jpg deleted file mode 100644 index 381774e7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-big-city-greens-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-big-city-greens-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-big-city-greens-2018-poster.jpg deleted file mode 100644 index ef024946..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-big-city-greens-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-big-mouth-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-big-mouth-2017-backdrop.jpg deleted file mode 100644 index 760e0262..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-big-mouth-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-big-mouth-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-big-mouth-2017-poster.jpg deleted file mode 100644 index 4f535fec..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-big-mouth-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-black-lightning-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-black-lightning-2018-backdrop.jpg deleted file mode 100644 index 3a0854c9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-black-lightning-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-black-lightning-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-black-lightning-2018-poster.jpg deleted file mode 100644 index 733f5cbc..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-black-lightning-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-black-rabbit-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-black-rabbit-2025-backdrop.jpg deleted file mode 100644 index 48df9d46..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-black-rabbit-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-black-rabbit-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-black-rabbit-2025-poster.jpg deleted file mode 100644 index 3ff9aeed..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-black-rabbit-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-bleach-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-bleach-2004-backdrop.jpg deleted file mode 100644 index ef8eaaa3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-bleach-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-bleach-2004-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-bleach-2004-poster.jpg deleted file mode 100644 index b233e033..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-bleach-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-brassic-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-brassic-2019-backdrop.jpg deleted file mode 100644 index 86d3d806..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-brassic-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-brassic-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-brassic-2019-poster.jpg deleted file mode 100644 index 73b77df2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-brassic-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-breaking-bad-2008-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-breaking-bad-2008-backdrop.jpg deleted file mode 100644 index e4ac4ae3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-breaking-bad-2008-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-breaking-bad-2008-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-breaking-bad-2008-poster.jpg deleted file mode 100644 index c4f7217c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-breaking-bad-2008-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-californication-2007-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-californication-2007-backdrop.jpg deleted file mode 100644 index ffff979b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-californication-2007-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-californication-2007-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-californication-2007-poster.jpg deleted file mode 100644 index 38428e76..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-californication-2007-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-chainsaw-man-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-chainsaw-man-2022-backdrop.jpg deleted file mode 100644 index f1fbee3e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-chainsaw-man-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-chainsaw-man-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-chainsaw-man-2022-poster.jpg deleted file mode 100644 index a3779772..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-chainsaw-man-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg deleted file mode 100644 index 2920cf5a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-cyberpunk-edgerunners-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-cyberpunk-edgerunners-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-cyberpunk-edgerunners-2022-poster.jpg deleted file mode 100644 index 8d338532..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-cyberpunk-edgerunners-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-derry-girls-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-derry-girls-2018-backdrop.jpg deleted file mode 100644 index 76fae98c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-derry-girls-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-derry-girls-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-derry-girls-2018-poster.jpg deleted file mode 100644 index 56a8cd79..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-derry-girls-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-desperate-housewives-2004-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-desperate-housewives-2004-backdrop.jpg deleted file mode 100644 index 40ec3001..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-desperate-housewives-2004-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-desperate-housewives-2004-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-desperate-housewives-2004-poster.jpg deleted file mode 100644 index 0c4c38bc..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-desperate-housewives-2004-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-dune-prophecy-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-dune-prophecy-2024-backdrop.jpg deleted file mode 100644 index 5b940b9f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-dune-prophecy-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-dune-prophecy-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-dune-prophecy-2024-poster.jpg deleted file mode 100644 index eef2a4f3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-dune-prophecy-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-etoile-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-etoile-2025-backdrop.jpg deleted file mode 100644 index a8e9576c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-etoile-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-etoile-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-etoile-2025-poster.jpg deleted file mode 100644 index f078bc89..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-etoile-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-fallout-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-fallout-2024-backdrop.jpg deleted file mode 100644 index c9d6a190..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-fallout-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-fallout-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-fallout-2024-poster.jpg deleted file mode 100644 index 5e87a07d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-fallout-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-firefly-2002-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-firefly-2002-backdrop.jpg deleted file mode 100644 index fa3b7604..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-firefly-2002-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-firefly-2002-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-firefly-2002-poster.jpg deleted file mode 100644 index 9ad599b3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-firefly-2002-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-fleabag-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-fleabag-2016-backdrop.jpg deleted file mode 100644 index e5a83462..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-fleabag-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-fleabag-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-fleabag-2016-poster.jpg deleted file mode 100644 index bd3ccea5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-fleabag-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-gilmore-girls-2000-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-gilmore-girls-2000-backdrop.jpg deleted file mode 100644 index b42ba953..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-gilmore-girls-2000-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-gilmore-girls-2000-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-gilmore-girls-2000-poster.jpg deleted file mode 100644 index 23b668cb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-gilmore-girls-2000-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-high-potential-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-high-potential-2024-backdrop.jpg deleted file mode 100644 index 66bb4d00..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-high-potential-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-high-potential-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-high-potential-2024-poster.jpg deleted file mode 100644 index 00a07ca7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-high-potential-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-invincible-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-invincible-2021-backdrop.jpg deleted file mode 100644 index e8120387..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-invincible-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-invincible-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-invincible-2021-poster.jpg deleted file mode 100644 index c278e37e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-invincible-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-last-samurai-standing-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-last-samurai-standing-2025-backdrop.jpg deleted file mode 100644 index c85a210b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-last-samurai-standing-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-last-samurai-standing-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-last-samurai-standing-2025-poster.jpg deleted file mode 100644 index a9789909..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-last-samurai-standing-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-lord-of-the-flies-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-lord-of-the-flies-2026-backdrop.jpg deleted file mode 100644 index 1249a8f3..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-lord-of-the-flies-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-lord-of-the-flies-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-lord-of-the-flies-2026-poster.jpg deleted file mode 100644 index b1229ed8..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-lord-of-the-flies-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-love-death-robots-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-love-death-robots-2019-backdrop.jpg deleted file mode 100644 index 3694bfc4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-love-death-robots-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-love-death-robots-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-love-death-robots-2019-poster.jpg deleted file mode 100644 index 08449aa9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-love-death-robots-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-maniac-2018-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-maniac-2018-backdrop.jpg deleted file mode 100644 index 9d46de1d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-maniac-2018-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-maniac-2018-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-maniac-2018-poster.jpg deleted file mode 100644 index a1985217..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-maniac-2018-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-neon-genesis-evangelion-1995-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-neon-genesis-evangelion-1995-backdrop.jpg deleted file mode 100644 index d80e17c1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-neon-genesis-evangelion-1995-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-neon-genesis-evangelion-1995-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-neon-genesis-evangelion-1995-poster.jpg deleted file mode 100644 index f86cd698..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-neon-genesis-evangelion-1995-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-norsemen-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-norsemen-2016-backdrop.jpg deleted file mode 100644 index 58805e67..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-norsemen-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-norsemen-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-norsemen-2016-poster.jpg deleted file mode 100644 index e7206b88..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-norsemen-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-one-piece-2023-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-one-piece-2023-backdrop.jpg deleted file mode 100644 index 3fa4a282..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-one-piece-2023-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-one-piece-2023-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-one-piece-2023-poster.jpg deleted file mode 100644 index 286e6260..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-one-piece-2023-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-only-murders-in-the-building-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-only-murders-in-the-building-2021-backdrop.jpg deleted file mode 100644 index 0b768d54..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-only-murders-in-the-building-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-only-murders-in-the-building-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-only-murders-in-the-building-2021-poster.jpg deleted file mode 100644 index a6b147a4..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-only-murders-in-the-building-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-over-the-garden-wall-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-over-the-garden-wall-2014-backdrop.jpg deleted file mode 100644 index 43ac4428..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-over-the-garden-wall-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-over-the-garden-wall-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-over-the-garden-wall-2014-poster.jpg deleted file mode 100644 index 34b1ed84..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-over-the-garden-wall-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-pluribus-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-pluribus-2025-backdrop.jpg deleted file mode 100644 index e64c0268..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-pluribus-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-pluribus-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-pluribus-2025-poster.jpg deleted file mode 100644 index 1ea4711b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-pluribus-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-rick-and-morty-2013-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-rick-and-morty-2013-backdrop.jpg deleted file mode 100644 index 1c4c0ffc..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-rick-and-morty-2013-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-rick-and-morty-2013-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-rick-and-morty-2013-poster.jpg deleted file mode 100644 index b95c4890..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-rick-and-morty-2013-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-secret-level-2024-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-secret-level-2024-backdrop.jpg deleted file mode 100644 index 54d2a13e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-secret-level-2024-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-secret-level-2024-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-secret-level-2024-poster.jpg deleted file mode 100644 index 1b6e67f6..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-secret-level-2024-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-severance-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-severance-2022-backdrop.jpg deleted file mode 100644 index a24c03bd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-severance-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-severance-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-severance-2022-poster.jpg deleted file mode 100644 index 5a29d6f9..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-severance-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-sex-and-the-city-1998-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-sex-and-the-city-1998-backdrop.jpg deleted file mode 100644 index 57beacc1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-sex-and-the-city-1998-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-sex-and-the-city-1998-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-sex-and-the-city-1998-poster.jpg deleted file mode 100644 index e546cc6d..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-sex-and-the-city-1998-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-100-2014-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-100-2014-backdrop.jpg deleted file mode 100644 index 8d41cb22..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-100-2014-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-100-2014-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-100-2014-poster.jpg deleted file mode 100644 index 4d254cb0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-100-2014-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg deleted file mode 100644 index 53e1d593..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-abbott-and-costello-show-1952-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-abbott-and-costello-show-1952-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-abbott-and-costello-show-1952-poster.jpg deleted file mode 100644 index 7d6768ea..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-abbott-and-costello-show-1952-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-afterparty-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-afterparty-2022-backdrop.jpg deleted file mode 100644 index 72ca19db..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-afterparty-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-afterparty-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-afterparty-2022-poster.jpg deleted file mode 100644 index 9beacde2..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-afterparty-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bear-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-bear-2022-backdrop.jpg deleted file mode 100644 index 9c9cca57..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bear-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bear-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-bear-2022-poster.jpg deleted file mode 100644 index bbf0ab28..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bear-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-beast-in-me-2025-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-beast-in-me-2025-backdrop.jpg deleted file mode 100644 index e7727a79..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-beast-in-me-2025-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-beast-in-me-2025-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-beast-in-me-2025-poster.jpg deleted file mode 100644 index c78c5cf0..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-beast-in-me-2025-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bold-type-2017-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-bold-type-2017-backdrop.jpg deleted file mode 100644 index dd84deab..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bold-type-2017-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bold-type-2017-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-bold-type-2017-poster.jpg deleted file mode 100644 index f663eb8b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-bold-type-2017-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-boys-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-boys-2019-backdrop.jpg deleted file mode 100644 index a834d9fb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-boys-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-boys-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-boys-2019-poster.jpg deleted file mode 100644 index f67ff16f..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-boys-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-capture-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-capture-2019-backdrop.jpg deleted file mode 100644 index 42256fc1..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-capture-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-capture-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-capture-2019-poster.jpg deleted file mode 100644 index a4039011..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-capture-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-durrells-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-durrells-2016-backdrop.jpg deleted file mode 100644 index 3f8bdeec..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-durrells-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-durrells-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-durrells-2016-poster.jpg deleted file mode 100644 index d29448bb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-durrells-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-final-problem-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-final-problem-2026-backdrop.jpg deleted file mode 100644 index 61a310d8..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-final-problem-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-final-problem-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-final-problem-2026-poster.jpg deleted file mode 100644 index f0332826..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-final-problem-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-legend-of-korra-2012-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-legend-of-korra-2012-backdrop.jpg deleted file mode 100644 index ab6086bd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-legend-of-korra-2012-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-legend-of-korra-2012-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-legend-of-korra-2012-poster.jpg deleted file mode 100644 index a7086d5e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-legend-of-korra-2012-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg deleted file mode 100644 index 0b88fd4b..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg deleted file mode 100644 index dc387fcd..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-lord-of-the-rings-the-rings-of-power-2022-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-night-manager-2016-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-night-manager-2016-backdrop.jpg deleted file mode 100644 index 327aa449..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-night-manager-2016-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-night-manager-2016-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-night-manager-2016-poster.jpg deleted file mode 100644 index 7ab2dbf5..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-night-manager-2016-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-tatami-galaxy-2010-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-tatami-galaxy-2010-backdrop.jpg deleted file mode 100644 index a267b4cb..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-tatami-galaxy-2010-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-tatami-galaxy-2010-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-tatami-galaxy-2010-poster.jpg deleted file mode 100644 index 5324dc75..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-tatami-galaxy-2010-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-white-lotus-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-white-lotus-2021-backdrop.jpg deleted file mode 100644 index 1f747323..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-white-lotus-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-white-lotus-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-white-lotus-2021-poster.jpg deleted file mode 100644 index 2f23d1fa..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-white-lotus-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-witcher-2019-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-witcher-2019-backdrop.jpg deleted file mode 100644 index acca834e..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-witcher-2019-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-the-witcher-2019-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-the-witcher-2019-poster.jpg deleted file mode 100644 index f5eb8f59..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-the-witcher-2019-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-upload-2020-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-upload-2020-backdrop.jpg deleted file mode 100644 index f350a49c..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-upload-2020-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-upload-2020-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-upload-2020-poster.jpg deleted file mode 100644 index 547d3b7a..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-upload-2020-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-widow-s-bay-2026-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-widow-s-bay-2026-backdrop.jpg deleted file mode 100644 index 03b1c335..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-widow-s-bay-2026-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-widow-s-bay-2026-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-widow-s-bay-2026-poster.jpg deleted file mode 100644 index 8c02a0d7..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-widow-s-bay-2026-poster.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-yellowjackets-2021-backdrop.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-yellowjackets-2021-backdrop.jpg deleted file mode 100644 index 960c2d23..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-yellowjackets-2021-backdrop.jpg and /dev/null differ diff --git a/app/src/debug/assets/wolfskin-titles/assets/tv-yellowjackets-2021-poster.jpg b/app/src/debug/assets/wolfskin-titles/assets/tv-yellowjackets-2021-poster.jpg deleted file mode 100644 index c7e16a57..00000000 Binary files a/app/src/debug/assets/wolfskin-titles/assets/tv-yellowjackets-2021-poster.jpg and /dev/null differ diff --git a/app/src/debug/java/org/justinvisser/wolfskin/tv/DebugHome.kt b/app/src/debug/java/org/justinvisser/wolfskin/tv/DebugHome.kt deleted file mode 100644 index 691f67c4..00000000 --- a/app/src/debug/java/org/justinvisser/wolfskin/tv/DebugHome.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import org.justinvisser.wolfskin.design.HomeFixture -import org.justinvisser.wolfskin.design.HomeSnapshot - -/** Only debug builds may read the locked prototype household projection. */ -fun debugHomeSnapshot(context: Context, person: String): HomeSnapshot? = HomeFixture(context).snapshot(person) diff --git a/app/src/default/AndroidManifest.xml b/app/src/default/AndroidManifest.xml deleted file mode 100644 index c59612c7..00000000 --- a/app/src/default/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/app/src/default/res/mipmap-anydpi-v26/ic_banner.xml b/app/src/default/res/mipmap-anydpi-v26/ic_banner.xml deleted file mode 100644 index ec3f55e3..00000000 --- a/app/src/default/res/mipmap-anydpi-v26/ic_banner.xml +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/app/src/default/res/mipmap-hdpi/ic_launcher.webp b/app/src/default/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 4ac23262..00000000 Binary files a/app/src/default/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/default/res/mipmap-hdpi/ic_launcher_foreground.webp deleted file mode 100644 index de1628cc..00000000 Binary files a/app/src/default/res/mipmap-hdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/default/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index df5faba9..00000000 Binary files a/app/src/default/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-mdpi/ic_launcher.webp b/app/src/default/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 415cf194..00000000 Binary files a/app/src/default/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/default/res/mipmap-mdpi/ic_launcher_foreground.webp deleted file mode 100644 index d9df942a..00000000 Binary files a/app/src/default/res/mipmap-mdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/default/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index dea8ff64..00000000 Binary files a/app/src/default/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xhdpi/ic_banner.png b/app/src/default/res/mipmap-xhdpi/ic_banner.png deleted file mode 100644 index ee42b91e..00000000 Binary files a/app/src/default/res/mipmap-xhdpi/ic_banner.png and /dev/null differ diff --git a/app/src/default/res/mipmap-xhdpi/ic_banner_foreground.png b/app/src/default/res/mipmap-xhdpi/ic_banner_foreground.png deleted file mode 100644 index ee42b91e..00000000 Binary files a/app/src/default/res/mipmap-xhdpi/ic_banner_foreground.png and /dev/null differ diff --git a/app/src/default/res/mipmap-xhdpi/ic_launcher.webp b/app/src/default/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 924b40eb..00000000 Binary files a/app/src/default/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/default/res/mipmap-xhdpi/ic_launcher_foreground.webp deleted file mode 100644 index e218f8bd..00000000 Binary files a/app/src/default/res/mipmap-xhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/default/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 40674eff..00000000 Binary files a/app/src/default/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/default/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 6f5be441..00000000 Binary files a/app/src/default/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/default/res/mipmap-xxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 8c1935c7..00000000 Binary files a/app/src/default/res/mipmap-xxhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/default/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 48a4e286..00000000 Binary files a/app/src/default/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/default/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 1cf49fb6..00000000 Binary files a/app/src/default/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/default/res/mipmap-xxxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index afc41a8b..00000000 Binary files a/app/src/default/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/default/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/default/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 122a33e0..00000000 Binary files a/app/src/default/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/default/res/values-hu/strings.xml b/app/src/default/res/values-hu/strings.xml deleted file mode 100644 index fc653df6..00000000 --- a/app/src/default/res/values-hu/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Wolfskin alapรฉrtelmezett beรกllรญtรกsa - Wolfskin kompakt elrendezรฉs - A Wolfskin frissรผlt a %s verziรณra - diff --git a/app/src/default/res/values-pl/strings.xml b/app/src/default/res/values-pl/strings.xml deleted file mode 100644 index e334b3ba..00000000 --- a/app/src/default/res/values-pl/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - O Wolfskin - diff --git a/app/src/default/res/values/strings.xml b/app/src/default/res/values/strings.xml deleted file mode 100644 index dd5ba542..00000000 --- a/app/src/default/res/values/strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Wolfskin - Wolfskin - Wolfskin Default - Wolfskin Compact - Wolfskin updated to %s - diff --git a/app/src/firetv/AndroidManifest.xml b/app/src/firetv/AndroidManifest.xml deleted file mode 100644 index 8a762a28..00000000 --- a/app/src/firetv/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aad5c5b5..5be4d55a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,6 @@ android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> - { + val current = serverRepository.current.value + val destination = + if (current != null) { + SetupDestination.UserList(current.server) + } else { + SetupDestination.ServerList + } + setupNavigationManager.navigateTo(destination) + Timber.e("Error parsing intent: %s", result.message) + showToast(context, result.message) + return@withLock + } - try { - val result = handleIntent(intent) - if (result) { - Timber.i("appStart was handled by intent") - return@withLock + is IntentResult.Target -> { + val current = serverRepository.current.value + if (current != null) { + Timber.i("Received valid intent, switching to AppContent") + + if (result.addHomeToBackStack) { + navigationManager.reloadHome() + } else { + navigationManager.backStack.clear() + } + navigationManager.backStack.addAll(result.destinations) + + setupNavigationManager.navigateTo( + SetupDestination.AppContent(current), + ) + } else { + // This should never happen, but just reset app state if it does + setupNavigationManager.navigateTo(SetupDestination.ServerList) + Timber.e("Error parsing intent, no user is active") + showToast(context, "An error occurred parsing the intent") + } + return@withLock + } + + IntentResult.NoOp, + null, + -> { + // No-op, proceed below + } } } catch (ex: Exception) { Timber.e(ex, "Error parsing intent") } try { - handleAppStart() + val needUpgrade = appUpgradeHandler.needUpgrade() + if (needUpgrade) { + showToast( + context, + context.getString( + R.string.updated_toast, + appUpgradeHandler.currentVersion.toString(), + ), + ) + appUpgradeHandler.run() + } + appUpgradeHandler.copySubfont(false) + val prefs = + preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance() + val profileProtected = + serverRepository.current.value + ?.user + ?.isProtected == true + if (prefs.signInAutomatically && !profileProtected) { + val current = + serverRepository.restoreSession( + prefs.currentServerId?.toUUIDOrNull(), + prefs.currentUserId?.toUUIDOrNull(), + ) + if (current != null) { + if (current.user.isProtected) { + setupNavigationManager.navigateTo( + SetupDestination.UserList( + current.server, + ), + ) + } else { + // Restored + setupNavigationManager.navigateTo( + SetupDestination.AppContent( + current, + ), + ) + } + } else { + // Did not restore + setupNavigationManager.navigateTo(SetupDestination.ServerList) + } + } else { + setupNavigationManager.navigateTo(SetupDestination.Loading) + backdropService.clearBackdrop() + val currentServerId = prefs.currentServerId?.toUUIDOrNull() + if (currentServerId != null) { + val currentServer = + serverRepository.serverDao.getServer(currentServerId)?.server + if (currentServer != null) { + setupNavigationManager.navigateTo( + SetupDestination.UserList( + currentServer, + ), + ) + } else { + setupNavigationManager.navigateTo(SetupDestination.ServerList) + } + } else { + setupNavigationManager.navigateTo(SetupDestination.ServerList) + } + } } catch (ex: Exception) { Timber.e(ex, "Error during appStart") setupNavigationManager.navigateTo(SetupDestination.ServerList) @@ -487,103 +574,6 @@ class MainActivityViewModel } } } - - /** - * Handle an [Intent], navigating if needed - * - * @return true if the intent was handled, false if normal app start should proceed - */ - @VisibleForTesting - internal suspend fun handleIntent(intent: Intent?): Boolean { - val result = intent?.let { intentService.parseIntent(intent) } - when (result) { - is IntentResult.Error -> { - val current = serverRepository.current.value - val destination = - if (current != null) { - SetupDestination.UserList(current.server) - } else { - SetupDestination.ServerList - } - setupNavigationManager.navigateTo(destination) - Timber.e("Error parsing intent: %s", result.message) - showToast(context, result.message) - return true - } - - is IntentResult.Target -> { - val current = serverRepository.current.value - if (current != null) { - Timber.i("Received valid intent, switching to AppContent") - - if (result.addHomeToBackStack) { - navigationManager.reloadHome() - } else { - navigationManager.backStack.clear() - } - navigationManager.backStack.addAll(result.destinations) - - setupNavigationManager.navigateTo( - SetupDestination.AppContent(current), - ) - } else { - // This should never happen, but just reset app state if it does - setupNavigationManager.navigateTo(SetupDestination.ServerList) - Timber.e("Error parsing intent, no user is active") - showToast(context, "An error occurred parsing the intent") - } - return true - } - - IntentResult.NoOp, - null, - -> { - // Not handled - return false - } - } - } - - private suspend fun handleAppUpgrade() { - val needUpgrade = appUpgradeHandler.needUpgrade() - if (needUpgrade) { - showToast( - context, - context.getString( - R.string.updated_toast, - appUpgradeHandler.currentVersion.toString(), - ), - ) - appUpgradeHandler.run() - } - appUpgradeHandler.copySubfont(false) - } - - /** - * Handle starting the app including auto sign in and navigating to the right starting page - */ - @VisibleForTesting - internal suspend fun handleAppStart() { - val signInAutomatically = preferences.data.first().signInAutomatically - val currentUser = serverRepository.current.value - if (signInAutomatically && currentUser != null && !currentUser.user.isProtected) { - // Hot reload - Timber.v("Hot reload restoring state") - setupNavigationManager.navigateTo( - SetupDestination.AppContent(currentUser), - ) - } else { - val restoredSession = - if (signInAutomatically) { - Timber.v("Sign in automatically") - serverRepository.restoreLastSession() - } else { - serverRepository.getMostRecentServer() - } - Timber.d("Restore last session result=%s", restoredSession) - setupNavigationManager.navigateTo(restoredSession.destination) - } - } } private val Destination?.isPlayback: Boolean diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt index fcaa3165..26dda908 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt @@ -34,7 +34,7 @@ import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.ScreensaverService import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.ui.components.AppScreensaver -import org.justinvisser.wolfskin.tv.WolfskinApp +import com.github.damontecres.wholphin.ui.nav.ApplicationContent import com.github.damontecres.wholphin.ui.setup.SwitchServerContent import com.github.damontecres.wholphin.ui.setup.SwitchUserContent import com.github.damontecres.wholphin.ui.util.InterfaceCustomization @@ -111,7 +111,7 @@ fun MainContent( } if (showContent) { - WolfskinApp( + ApplicationContent( user = current.user, server = current.server, navigationManager = navigationManager, diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt index 6ec9d4b4..f027bbae 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt @@ -122,7 +122,5 @@ class WholphinApplication : private set val minimumServerVersion: ServerVersion = Jellyfin.minimumVersion - - val newMinimumServerVersion: ServerVersion = ServerVersion(12, 0, 0) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt index 3bc17f3c..57d4a447 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt @@ -22,7 +22,6 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.services.ScreensaverService @@ -34,7 +33,9 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.util.ProvideLocalClock import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first import okhttp3.OkHttpClient +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -74,10 +75,8 @@ class WholphinDreamService : lifecycleRegistry.currentState = Lifecycle.State.CREATED lifecycleScope.launchDefault { if (serverRepository.current.value == null) { - val result = serverRepository.restoreLastSession() - if (result !is RestoredSession.Success) { - Timber.w("Could not restore user session") - } + val prefs = preferencesDataStore.data.first() + serverRepository.restoreSession(prefs.currentServerId.toUUIDOrNull(), prefs.currentUserId.toUUIDOrNull()) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt index 45af2d65..6a691723 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt @@ -19,7 +19,6 @@ import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice import com.github.damontecres.wholphin.data.model.RememberedTab import com.github.damontecres.wholphin.data.model.SeerrServer import com.github.damontecres.wholphin.data.model.SeerrUser -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice import com.github.damontecres.wholphin.ui.components.ViewOptions import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind @@ -49,9 +48,8 @@ import java.util.UUID SeerrServer::class, SeerrUser::class, RememberedTab::class, - SeriesTrackChoice::class, ], - version = 41, + version = 35, exportSchema = true, autoMigrations = [ AutoMigration(3, 4), @@ -70,8 +68,6 @@ import java.util.UUID AutoMigration(32, 33), AutoMigration(33, 34), AutoMigration(34, 35), - AutoMigration(35, 40), - AutoMigration(40, 41), ], ) @TypeConverters(Converters::class) @@ -91,8 +87,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun playbackEffectDao(): PlaybackEffectDao abstract fun rememberedTabDao(): RememberedTabDao - - abstract fun seriesTrackChoiceDao(): SeriesTrackChoiceDao } class Converters { diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt index 702ecfdb..c3afbc33 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt @@ -3,12 +3,9 @@ package com.github.damontecres.wholphin.data import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemTrackModification -import com.github.damontecres.wholphin.data.model.RelativeMediaStream -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType +import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.StreamChoiceResult import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.util.WholphinDispatchers import kotlinx.coroutines.withContext @@ -31,7 +28,7 @@ class ItemPlaybackRepository constructor( val serverRepository: ServerRepository, val itemPlaybackDao: ItemPlaybackDao, - private val seriesTrackChoiceDao: SeriesTrackChoiceDao, + private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao, private val streamChoiceService: StreamChoiceService, ) { /** @@ -48,79 +45,54 @@ class ItemPlaybackRepository ): ChosenStreams? = serverRepository.currentUser?.let { user -> val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId) - return getChosenItemFromPlayback(item, itemPlayback, prefs) + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) + Timber.v("For ${item.id}: itemPlayback=${itemPlayback != null}, plc=${plc != null}") + return getChosenItemFromPlayback(item, itemPlayback, plc, prefs) } - suspend fun getChosenItemFromPlayback( - item: BaseItem, - itemPlayback: ItemPlayback?, - prefs: UserPreferences, - ): ChosenStreams? { - val audioStc = - streamChoiceService.getSeriesTrackChoices( - item.data, - SeriesTrackChoiceType.AUDIO, - ) - val subtitleStc = - streamChoiceService.getSeriesTrackChoices( - item.data, - SeriesTrackChoiceType.SUBTITLE, - ) - return getChosenItemFromPlayback(item, itemPlayback, audioStc, subtitleStc, prefs) - } - /** * Get the chosen source & stream for the given item * - * This will be either the explicit choice from the provided [ItemPlayback] & [SeriesTrackChoice] or the default choices if they are null + * This will be either the explicit choice from the provided [ItemPlayback] & [PlaybackLanguageChoice] or the default choices if they are null */ fun getChosenItemFromPlayback( item: BaseItem, itemPlayback: ItemPlayback?, - audioStc: List, - subtitleStc: List, + plc: PlaybackLanguageChoice?, prefs: UserPreferences, ): ChosenStreams? { - Timber.v( - "For %s: itemPlayback=%s, audioStc=%s, subtitleStc=%s", - item.id, - itemPlayback != null, - audioStc.isNotEmpty(), - subtitleStc.isNotEmpty(), - ) val source = item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback?.sourceId } ?: streamChoiceService.chooseSource(item.data.mediaSources.orEmpty()) if (source != null) { - val audioResult = + val audioStream = streamChoiceService.chooseAudioStream( candidates = source.mediaStreams ?.filter { it.type == MediaStreamType.AUDIO } .orEmpty(), itemPlayback = itemPlayback, - stc = audioStc, + playbackLanguageChoice = plc, prefs = prefs, ) val subtitleStream = streamChoiceService.chooseSubtitleStream( - audioStreamLang = audioResult.stream?.language, + audioStreamLang = audioStream?.language, candidates = source.mediaStreams ?.filter { it.type == MediaStreamType.SUBTITLE } .orEmpty(), itemPlayback = itemPlayback, - stc = subtitleStc, + playbackLanguageChoice = plc, prefs = prefs, ) return ChosenStreams( itemPlayback = itemPlayback, - audioStc = audioStc, - subtitleStc = subtitleStc, + plc = plc, itemId = item.id, source = source, videoStream = source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }, - audioStream = audioResult, + audioStream = audioStream, subtitleStream = subtitleStream, subtitlesDisabled = itemPlayback?.subtitleIndex == TrackIndex.DISABLED, ) @@ -184,36 +156,26 @@ class ItemPlaybackRepository Timber.v("Saving track selection %s", toSave) toSave = saveItemPlayback(toSave) val seriesId = item.data.seriesId - if (seriesId != null || item.data.parentId != null) { + if (seriesId != null && (trackIndex >= 0 || trackIndex == TrackIndex.DISABLED)) { if (type == MediaStreamType.AUDIO) { - val stream = - RelativeMediaStream.from(trackIndex, source.mediaStreams.orEmpty()) + val stream = source.mediaStreams?.first { it.index == trackIndex } if (stream?.language != null) { - streamChoiceService.saveSeriesTrackChoice( - item.data, - stream.type, - stream, - ) + streamChoiceService.updateAudio(item.data, stream.language!!) } } else if (type == MediaStreamType.SUBTITLE) { if (trackIndex == TrackIndex.DISABLED) { - streamChoiceService.saveDisabledSeriesTrackChoice( + streamChoiceService.updateSubtitles( item.data, - MediaStreamType.SUBTITLE, - ) - } else if (trackIndex == TrackIndex.ONLY_FORCED) { - streamChoiceService.saveOnlyForcedSeriesTrackChoice( - item.data, - MediaStreamType.SUBTITLE, + subtitleLang = null, + subtitlesDisabled = true, ) } else { - val stream = - RelativeMediaStream.from(trackIndex, source.mediaStreams.orEmpty()) + val stream = source.mediaStreams?.firstOrNull { it.index == trackIndex } if (stream?.language != null) { - streamChoiceService.saveSeriesTrackChoice( + streamChoiceService.updateSubtitles( item.data, - stream.type, - stream, + stream.language!!, + subtitlesDisabled = false, ) } } @@ -278,10 +240,9 @@ class ItemPlaybackRepository */ suspend fun deleteChosenStreams(chosenStreams: ChosenStreams?) { Timber.d("deleteChosenStreams: %s", chosenStreams) - val stc = chosenStreams?.audioStc.orEmpty() + chosenStreams?.subtitleStc.orEmpty() - if (stc.isNotEmpty()) { - Timber.d("Deleting %s", stc) - seriesTrackChoiceDao.delete(stc) + chosenStreams?.plc?.let { + Timber.d("Deleting %s", it) + playbackLanguageChoiceDao.delete(it) } chosenStreams?.itemPlayback?.let { Timber.d("Deleting %s", it) @@ -295,12 +256,11 @@ class ItemPlaybackRepository */ data class ChosenStreams( val itemPlayback: ItemPlayback?, - val audioStc: List, - val subtitleStc: List, + val plc: PlaybackLanguageChoice?, val itemId: UUID, val source: MediaSourceInfo, val videoStream: MediaStream?, - val audioStream: StreamChoiceResult, - val subtitleStream: StreamChoiceResult, + val audioStream: MediaStream?, + val subtitleStream: MediaStream?, val subtitlesDisabled: Boolean, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/MostRecentServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/MostRecentServer.kt deleted file mode 100644 index 5139fb73..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/data/MostRecentServer.kt +++ /dev/null @@ -1,175 +0,0 @@ -package com.github.damontecres.wholphin.data - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import com.github.damontecres.wholphin.data.MostRecentServer.None -import com.github.damontecres.wholphin.data.MostRecentServer.Server -import com.github.damontecres.wholphin.data.MostRecentServer.ServerAndUser -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.services.SetupDestination -import com.github.damontecres.wholphin.ui.isNotNullOrBlank -import com.github.damontecres.wholphin.ui.toServerString -import org.jellyfin.sdk.model.serializer.toUUIDOrNull -import java.util.UUID - -/** - * Represents the status of the most recently used server - * - * This can be saved/retrieved from a [MostRecentServerProvider] - */ -sealed interface MostRecentServer { - /** - * There is no recently used server - */ - data object None : MostRecentServer - - /** - * The most recently used server without a recent user - */ - data class Server( - val serverId: UUID, - val serverUrl: String, - ) : MostRecentServer - - /** - * Most recently used server and user - */ - data class ServerAndUser( - val serverId: UUID, - val serverUrl: String, - val userId: UUID, - val accessToken: String, - ) : MostRecentServer { - override fun toString(): String = "ServerAndUser(serverId=$serverId, serverUrl=$serverUrl, userId=$userId)" - } - - companion object { - fun from(current: CurrentUser?): MostRecentServer = - if (current != null && current.user.accessToken.isNotNullOrBlank()) { - ServerAndUser(current.server.id, current.server.url, current.user.id, current.user.accessToken) - } else if (current != null) { - Server(current.server.id, current.server.url) - } else { - None - } - } -} - -interface MostRecentServerProvider { - /** - * Get the [MostRecentServer] from this provider - */ - fun get(): MostRecentServer - - /** - * Save the [CurrentUser] as the most recent server - */ - suspend fun save(current: CurrentUser) - - /** - * Clear the most recent server and user - */ - suspend fun clear() - - /** - * CLear the most recent user, leaving the server - */ - suspend fun clearUser() -} - -/** - * A [MostRecentServerProvider] that stores in [SharedPreferences] - */ -class MostRecentServerSharedPreferences( - context: Context, -) : MostRecentServerProvider { - private val prefs = getServerSharedPreferences(context) - - override fun get(): MostRecentServer { - val serverId = prefs.getString(SERVER_ID_KEY, null)?.toUUIDOrNull() - val serverUrl = prefs.getString(SERVER_URL_KEY, null) - val userId = prefs.getString(USER_ID_KEY, null)?.toUUIDOrNull() - val accessToken = prefs.getString(USER_TOKEN_KEY, null) - return if (serverId != null && serverUrl.isNotNullOrBlank() && userId != null && accessToken.isNotNullOrBlank()) { - ServerAndUser(serverId, serverUrl, userId, accessToken) - } else if (serverId != null && serverUrl.isNotNullOrBlank()) { - Server(serverId, serverUrl) - } else { - None - } - } - - override suspend fun save(current: CurrentUser) { - prefs.edit(true) { - putString(SERVER_ID_KEY, current.server.id.toServerString()) - putString(SERVER_URL_KEY, current.server.url) - putString(USER_ID_KEY, current.user.id.toServerString()) - putString(USER_TOKEN_KEY, current.user.accessToken) - } - } - - override suspend fun clear() { - prefs.edit(true) { - remove(SERVER_ID_KEY) - remove(SERVER_URL_KEY) - remove(USER_ID_KEY) - remove(USER_TOKEN_KEY) - } - } - - override suspend fun clearUser() { - prefs.edit(true) { - remove(USER_ID_KEY) - remove(USER_TOKEN_KEY) - } - } - - private fun getServerSharedPreferences(context: Context): SharedPreferences = - context.getSharedPreferences( - "${context.packageName}_server", - Context.MODE_PRIVATE, - ) - - companion object { - private const val SERVER_ID_KEY = "current.server.id" - private const val SERVER_URL_KEY = "current.server.url" - private const val USER_ID_KEY = "current.user.id" - private const val USER_TOKEN_KEY = "current.user.accessToken" - } -} - -/** - * The result of restoring a session - */ -sealed interface RestoredSession { - val destination: SetupDestination - - /** - * Session could not be restored - */ - data object None : RestoredSession { - override val destination: SetupDestination - get() = SetupDestination.ServerList - } - - /** - * Only the server should be restored, so a user still need to be chosen - */ - data class ServerOnly( - val server: JellyfinServer, - ) : RestoredSession { - override val destination: SetupDestination - get() = SetupDestination.UserList(server) - } - - /** - * Session was restored successfully - */ - data class Success( - val currentUser: CurrentUser, - ) : RestoredSession { - override val destination: SetupDestination - get() = SetupDestination.AppContent(currentUser) - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/PlaybackLanguageChoiceDao.kt b/app/src/main/java/com/github/damontecres/wholphin/data/PlaybackLanguageChoiceDao.kt index 06835381..5f8c16e6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/PlaybackLanguageChoiceDao.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/PlaybackLanguageChoiceDao.kt @@ -8,27 +8,17 @@ import androidx.room.Query import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice import java.util.UUID -@Deprecated("Use SeriesTrackChoiceDao") @Dao interface PlaybackLanguageChoiceDao { - @Deprecated("Use SeriesTrackChoiceDao") @Query("SELECT * FROM PlaybackLanguageChoice WHERE userId=:userId AND seriesId=:seriesId") suspend fun get( userId: Int, seriesId: UUID, ): PlaybackLanguageChoice? - @Deprecated("Use SeriesTrackChoiceDao") @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(plc: PlaybackLanguageChoice): Long - @Deprecated("Use SeriesTrackChoiceDao") @Delete fun delete(plc: PlaybackLanguageChoice) - - @Query("SELECT * FROM PlaybackLanguageChoice ORDER BY userId, seriesId LIMIT :limit OFFSET :offset") - suspend fun getAll( - limit: Int, - offset: Int, - ): List } diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/SeriesTrackChoiceDao.kt b/app/src/main/java/com/github/damontecres/wholphin/data/SeriesTrackChoiceDao.kt deleted file mode 100644 index b3126723..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/data/SeriesTrackChoiceDao.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.github.damontecres.wholphin.data - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType -import java.util.UUID - -@Dao -interface SeriesTrackChoiceDao { - @Query( - "SELECT * from SeriesTrackChoice WHERE userId=:userId AND type=:type AND (parentId=:seasonId OR parentId=:seriesId) ORDER BY parentId=:seasonId DESC", - ) - suspend fun get( - userId: Int, - seasonId: UUID, - seriesId: UUID, - type: SeriesTrackChoiceType, - ): List - - @Query("SELECT * from SeriesTrackChoice WHERE userId=:userId AND type=:type AND parentId=:seasonId") - suspend fun getBySeasonId( - userId: Int, - seasonId: UUID, - type: SeriesTrackChoiceType, - ): List - - @Query("SELECT * from SeriesTrackChoice WHERE userId=:userId AND type=:type AND parentId=:seriesId") - suspend fun getBySeriesId( - userId: Int, - seriesId: UUID, - type: SeriesTrackChoiceType, - ): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(stc: SeriesTrackChoice): Long - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(stc: List): List - - @Delete - suspend fun delete(stc: List) - - @Query("SELECT * from SeriesTrackChoice WHERE userId=:userId") - suspend fun getAll(userId: Int): List - - @Query("SELECT count(*) from SeriesTrackChoice") - suspend fun count(): Int -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt index 2c24c8f3..5deba4c3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt @@ -1,10 +1,15 @@ package com.github.damontecres.wholphin.data import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.datastore.core.DataStore import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.ServerUserConfig +import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.services.hilt.IoDispatcher +import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.WholphinDispatchers import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher @@ -44,8 +49,8 @@ class ServerRepository val jellyfin: Jellyfin, val serverDao: JellyfinServerDao, val apiClient: ApiClient, + val userPreferencesDataStore: DataStore, @param:IoDispatcher private val ioDispatcher: CoroutineDispatcher, - private val mostRecentServerProvider: MostRecentServerProvider, ) { private var _current = MutableStateFlow(null) val current: StateFlow = _current @@ -72,8 +77,6 @@ class ServerRepository } } - val serverPluginInstalled = MutableStateFlow(false) - /** * Adds a server to the app database and updated the [ApiClient] to the server's URL * @@ -85,11 +88,10 @@ class ServerRepository } apiClient.update(baseUrl = server.url, accessToken = null) _current.value = null - mostRecentServerProvider.clearUser() } /** - * Saves the server & user to the app database and updates the [ApiClient] to use this server & user + * Saves the server & User to the app database and updates the [ApiClient] to use this server & user */ suspend fun changeUser( server: JellyfinServer, @@ -117,101 +119,70 @@ class ServerRepository ) serverDao.addOrUpdateServer(updatedServer) updatedUser = serverDao.addOrUpdateUser(updatedUser) - + userPreferencesDataStore.updateData { + it + .toBuilder() + .apply { + currentServerId = updatedServer.id.toServerString() + currentUserId = updatedUser.id.toServerString() + }.build() + } val currentUser = CurrentUser(updatedServer, updatedUser) - mostRecentServerProvider.save(currentUser) - _current.value = currentUser - _currentUserDto.value = ServerUserConfig(userDto) + withContext(WholphinDispatchers.Main) { + _current.value = currentUser + _currentUserDto.value = ServerUserConfig(userDto) + } + getServerSharedPreferences(context).edit(true) { + putString(SERVER_URL_KEY, updatedServer.url) + putString(ACCESS_TOKEN_KEY, updatedUser.accessToken) + } return@withContext currentUser } /** - * Try to restore the most recent session + * Restores a session for the given server & user such as when the app reopens * - * Does not check if auto sign-in is enabled, but does check if the user profile is protected + * If user has a PIN, this returns false */ - suspend fun restoreLastSession(): RestoredSession { - val recentServer = mostRecentServerProvider.get() - Timber.d("restoreLastSession: recentServer=%s", recentServer) - val result = - when (recentServer) { - MostRecentServer.None -> { - RestoredSession.None - } - - is MostRecentServer.Server -> { - val server = serverDao.getServer(recentServer.serverId) - if (server != null) { - RestoredSession.ServerOnly(server.server) - } else { - RestoredSession.None - } - } - - is MostRecentServer.ServerAndUser -> { - val currentUser = tryChangeUser(recentServer.serverId, recentServer.userId) - if (currentUser != null) { - if (currentUser.user.isProtected) { - RestoredSession.ServerOnly(currentUser.server) - } else { - RestoredSession.Success(currentUser) - } - } else { - getMostRecentServerInternal(recentServer) - } - } - } - return result - } - - /** - * Get the most recently used server, if any - */ - suspend fun getMostRecentServer(): RestoredSession = getMostRecentServerInternal(mostRecentServerProvider.get()) - - suspend fun getMostRecentServerInternal(recentServer: MostRecentServer): RestoredSession { - val recentServer = recentServer ?: mostRecentServerProvider.get() - val serverId = - when (recentServer) { - MostRecentServer.None -> null - is MostRecentServer.Server -> recentServer.serverId - is MostRecentServer.ServerAndUser -> recentServer.serverId - } - val server = serverId?.let { serverDao.getServer(serverId)?.server } - return if (server != null) { - RestoredSession.ServerOnly(server) - } else { - RestoredSession.None - } - } - - /** - * Try to change to the given server & user. If the user's profile is protected, returns null - * - * @return the server/user or null if the session could not be restored - */ - suspend fun tryChangeUser( + suspend fun restoreSession( serverId: UUID?, userId: UUID?, - ): CurrentUser? = - withContext(ioDispatcher) { - return@withContext if (serverId == null || userId == null) { - _current.value = null - null + ): CurrentUser? { + if (serverId == null || userId == null) { + _current.value = null + return null + } + val serverAndUsers = + withContext(ioDispatcher) { + serverDao.getServer(serverId) + } + if (serverAndUsers != null) { + val current = _current.value + if (current != null && current.server.id == serverId && current.user.id == userId) { + Timber.v("Restoring session for current user, so shortcut") + apiClient.update( + baseUrl = current.server.url, + accessToken = current.user.accessToken, + ) + return current } else { - val serverAndUsers = serverDao.getServer(serverId) - if (serverAndUsers != null) { - val user = serverAndUsers.users.firstOrNull { it.id == userId } - if (user != null && !user.isProtected) { - changeUser(serverAndUsers.server, user) - } else { - null - } - } else { - null + val user = serverAndUsers.users.firstOrNull { it.id == userId } + if (user != null) { + return changeUser(serverAndUsers.server, user) } } } + return null + } + + suspend fun fetchLastUsedServer(serverId: UUID?): JellyfinServer? = + withContext(ioDispatcher) { + serverId?.let { serverDao.getServer(serverId)?.server } + } + + fun closeSession() { + _current.value = null + } /** * Given a successful [AuthenticationResult], switch to the user that just authenticated @@ -273,7 +244,13 @@ class ServerRepository withContext(WholphinDispatchers.Main) { _current.value = null } - mostRecentServerProvider.clearUser() + userPreferencesDataStore.updateData { + it + .toBuilder() + .apply { + currentUserId = "" + }.build() + } apiClient.update(accessToken = null) } withContext(ioDispatcher) { @@ -286,7 +263,14 @@ class ServerRepository withContext(WholphinDispatchers.Main) { _current.value = null } - mostRecentServerProvider.clear() + userPreferencesDataStore.updateData { + it + .toBuilder() + .apply { + currentServerId = "" + currentUserId = "" + }.build() + } apiClient.update(baseUrl = null, accessToken = null) } withContext(ioDispatcher) { @@ -295,7 +279,14 @@ class ServerRepository } suspend fun switchServerOrUser() { - mostRecentServerProvider.clear() + userPreferencesDataStore.updateData { + it + .toBuilder() + .apply { + currentServerId = "" + currentUserId = "" + }.build() + } } suspend fun updateUserAuth( @@ -344,6 +335,17 @@ class ServerRepository if (currentUser?.id == userDto.id) ServerUserConfig(userDto) else it } } + + companion object { + fun getServerSharedPreferences(context: Context): SharedPreferences = + context.getSharedPreferences( + "${context.packageName}_server", + Context.MODE_PRIVATE, + ) + + const val SERVER_URL_KEY = "current.server" + const val ACCESS_TOKEN_KEY = "current.accessToken" + } } @Serializable diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index 256ccd65..de3a8f5e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -1,5 +1,6 @@ package com.github.damontecres.wholphin.data.model +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.text.AnnotatedString @@ -31,6 +32,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.extensions.ticks +import java.util.Locale import java.util.UUID import kotlin.time.Duration @@ -66,7 +68,6 @@ data class BaseItem( BaseItemKind.EPISODE -> listOf(data.seasonEpisode, name).joinNotBlank(" - ") BaseItemKind.SERIES -> data.seriesProductionYears BaseItemKind.AUDIO -> listOf(data.album, artistsString).joinNotBlank(" - ") - BaseItemKind.MUSIC_ALBUM -> listOf(data.albumArtist ?: artistsString, data.productionYear?.toString()).joinNotBlank(" - ") else -> data.productionYear?.toString() } } @@ -233,6 +234,26 @@ data class BaseItem( append(it) } }, + communityRating = + data.communityRating?.let { + buildAnnotatedString { + dot() + append(String.format(Locale.getDefault(), "%.1f", it)) + appendInlineContent(id = "star") + } + }, + criticRating = + data.criticRating?.let { + buildAnnotatedString { + dot() + append("${it.toInt()}%") + if (it >= 60f) { + appendInlineContent(id = "fresh") + } else { + appendInlineContent(id = "rotten") + } + } + }, ), ) @@ -348,6 +369,12 @@ data class BaseItemUi( data class QuickDetailsData( val basic: AnnotatedString, val officialRating: AnnotatedString? = null, + val criticRating: AnnotatedString? = null, + val communityRating: AnnotatedString? = null, + /** + * Show [criticRating] (Rotten Tomatoes) before [communityRating] (IMDb/TMDB) like the PWA, instead of the Jellyfin order + */ + val criticRatingFirst: Boolean = false, ) /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt index 91228969..3746092b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt @@ -9,7 +9,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.VideoType -import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest import org.jellyfin.sdk.model.api.request.GetArtistsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetPersonsRequest @@ -156,22 +155,6 @@ data class GetItemsFilter( }, ) - fun applyTo(req: GetAlbumArtistsRequest) = - req.copy( - minCommunityRating = minCommunityRating, - isFavorite = favorite, - genreIds = genres, - personIds = persons, - studioIds = studios, - tags = tags, - officialRatings = officialRatings, - years = - buildSet { - years?.letNotEmpty(::addAll) - decades?.forEach { addAll(it..<(it + 10)) } - }, - ) - /** * Merge another [GetItemsFilter] onto this one, replacing only unset values */ diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt index 39b82bc8..4914248f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt @@ -1,7 +1,4 @@ -@file:UseSerializers( - UUIDSerializer::class, - DateTimeSerializer::class, -) +@file:UseSerializers(UUIDSerializer::class) package com.github.damontecres.wholphin.data.model @@ -10,15 +7,11 @@ import com.github.damontecres.wholphin.ui.AspectRatio import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.components.ViewOptionImageType import com.github.damontecres.wholphin.ui.data.SortAndDirection -import kotlinx.datetime.LocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemSortBy -import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest -import org.jellyfin.sdk.model.serializer.DateTimeSerializer import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID @@ -181,45 +174,19 @@ sealed interface HomeRowConfig { } /** - * An arbitrary [HomeGetItemsFilter] allowing to query for most anything + * An arbitrary [GetItemsRequest] allowing to query for anything */ @Serializable @SerialName("GetItems") data class GetItems( val name: String, - val filter: HomeGetItemsFilter, + val getItems: GetItemsRequest, override val viewOptions: HomeRowViewOptions = HomeRowViewOptions(), ) : HomeRowConfig { override fun updateViewOptions(viewOptions: HomeRowViewOptions): GetItems = this.copy(viewOptions = viewOptions) } - - /** - * Fetch items from an arbitrary Jellyfin endpoint that returns a QueryResult. - * Lets third-party plugins (e.g. home-sections) contribute rows without Wholphin needing - * specific knowledge of them. - * - * Headers/Query are a list of [KeyValueEntry] (not a Map) because the server-side plugin - * config is persisted as XML, which doesn't support IDictionary. - */ - @Serializable - @SerialName("CustomEndpoint") - data class CustomEndpoint( - val endpoint: String, - val title: String, - val headers: List? = null, - val query: List? = null, - override val viewOptions: HomeRowViewOptions = HomeRowViewOptions(), - ) : HomeRowConfig { - override fun updateViewOptions(viewOptions: HomeRowViewOptions): CustomEndpoint = this.copy(viewOptions = viewOptions) - } } -@Serializable -data class KeyValueEntry( - val key: String, - val value: String, -) - /** * Root class for home page settings * @@ -274,54 +241,3 @@ data class HomeRowViewOptions( ) } } - -/** - * Subset of [GetItemsFilter] - */ -@Serializable -data class HomeGetItemsFilter( - val sortBy: List? = null, - val sortOrder: List? = null, - val ids: List? = null, - val minCommunityRating: Double? = null, - val minCriticRating: Double? = null, - val recursive: Boolean? = null, - val searchTerm: String? = null, - val parentId: UUID? = null, - val includeItemTypes: List? = null, - val isFavorite: Boolean? = null, - val isPlayed: Boolean? = null, - val officialRatings: Collection? = null, - val tags: List? = null, - val years: List? = null, - val personIds: List? = null, - val minOfficialRating: String? = null, - val hasOfficialRating: Boolean? = null, - val studioIds: List? = null, - val genreIds: List? = null, - val minPremiereDate: LocalDateTime? = null, - val maxPremiereDate: LocalDateTime? = null, -) { - fun asGetItemsRequest() = - GetItemsRequest( - sortBy = sortBy, - sortOrder = sortOrder, - ids = ids, - minCommunityRating = minCommunityRating, - minCriticRating = minCriticRating, - recursive = recursive, - searchTerm = searchTerm, - parentId = parentId, - includeItemTypes = includeItemTypes, - isFavorite = isFavorite, - isPlayed = isPlayed, - officialRatings = officialRatings, - tags = tags, - years = years, - personIds = personIds, - minOfficialRating = minOfficialRating, - hasOfficialRating = hasOfficialRating, - studioIds = studioIds, - genreIds = genreIds, - ) -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt index 6c5a2a58..4c54311c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -13,7 +13,6 @@ import androidx.room.Relation import com.github.damontecres.wholphin.data.ZonedDateTimeSerializer import com.github.damontecres.wholphin.preferences.SubtitleModePreference import com.github.damontecres.wholphin.preferences.UserProfileSettings -import com.github.damontecres.wholphin.services.HomePageSettingsSource import com.github.damontecres.wholphin.ui.isNotNullOrBlank import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers @@ -69,7 +68,6 @@ data class JellyfinUser( val lastUsed: ZonedDateTime? = null, val uiLanguage: String? = null, @Embedded val appPreferences: JellyfinUserPreferences = JellyfinUserPreferences(), - @Embedded(prefix = "c_") val config: JellyfinUserConfig = JellyfinUserConfig(), ) { val hasPin: Boolean get() = pin.isNotNullOrBlank() @@ -79,17 +77,9 @@ data class JellyfinUser( "JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, lastUsed=$lastUsed, " + "accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()}), " + "requireLogin=$requireLogin, lastUsed=$lastUsed, uiLanguage=$uiLanguage, " + - "appPreferences=$appPreferences, config=$config" - - fun updateConfig(copy: (JellyfinUserConfig) -> JellyfinUserConfig): JellyfinUser = this.copy(config = copy.invoke(this.config)) + "appPreferences=$appPreferences" } -@Serializable -data class JellyfinUserConfig( - @ColumnInfo(defaultValue = "UNSET") - val homeSettingsSource: HomePageSettingsSource = HomePageSettingsSource.UNSET, -) - /** * Represents the relationship between [JellyfinServer] and its [JellyfinUser] */ diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt index a20a2dcd..4e639009 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt @@ -35,7 +35,7 @@ data class Person( name = dto.name, role = personRole(context, dto.role, dto.type), type = dto.type, - imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY, tag = dto.primaryImageTag), + imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), favorite = false, ) @@ -50,7 +50,7 @@ data class Person( name = dto.name, role = personRole(context, dto.role, dto.type), type = dto.type, - imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY, tag = dto.primaryImageTag), + imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), favorite = favorite, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt index f5507a50..fb54dfcd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt @@ -10,14 +10,9 @@ import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID /** - * DEPRECATED - * * Stores the language choices for a series so they can be applied automatically to other episodes * without the user needing to explicitly choose the tracks - * - * @see SeriesTrackChoice */ -@Deprecated("Use SeriesTrackChoice") @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/RelativeMediaStream.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/RelativeMediaStream.kt deleted file mode 100644 index cd2fece7..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/RelativeMediaStream.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.github.damontecres.wholphin.data.model - -import com.github.damontecres.wholphin.ui.indexOfFirstOrNull -import org.jellyfin.sdk.model.api.MediaStream -import org.jellyfin.sdk.model.api.MediaStreamType - -/** - * A [MediaStream] with its relative track index, its position within a specific stream type - */ -data class RelativeMediaStream( - val relativeIndex: Int, - val stream: MediaStream, -) { - val type: MediaStreamType get() = stream.type - val language: String? get() = stream.language - val isForced: Boolean get() = stream.isForced - val isExternal: Boolean get() = stream.isExternal - val isDefault: Boolean get() = stream.isDefault - val isHearingImpaired: Boolean get() = stream.isHearingImpaired - - companion object { - fun getRelativeIndex( - index: Int, - streams: List, - ): Int? { - val stream = streams.firstOrNull { it.index == index } ?: return null - return getRelativeIndex(stream, streams) - } - - fun getRelativeIndex( - stream: MediaStream, - streams: List, - ): Int? = - streams - .filter { it.type == stream.type && it.isExternal == stream.isExternal } - .sortedBy { it.index } - .indexOfFirstOrNull { it.index == stream.index } - - fun from( - index: Int, - streams: List, - ): RelativeMediaStream? { - val stream = streams.firstOrNull { it.index == index } ?: return null - val relativeIndex = getRelativeIndex(stream, streams) ?: return null - return RelativeMediaStream(relativeIndex, stream) - } - } -} - -/** - * Maps a list of [MediaStream] into [RelativeMediaStream] - */ -fun List.asRelative(): List { - val map = this.groupBy { Pair(it.type, it.isExternal) } - return map.values - .flatMap { typedStreams -> - typedStreams.sortedBy { it.index }.mapIndexed { index, stream -> - RelativeMediaStream(index, stream) - } - }.sortedBy { it.stream.index } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt index 918384d4..aafdebf7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt @@ -54,11 +54,3 @@ fun SeerrUserConfig?.hasPermission(permission: SeerrPermission): Boolean { permission.parents.any { hasPermission(it) } || SeerrPermission.ADMIN.hasPermission(permissions) } - -val SeerrUserConfig?.canRequestMovies: Boolean - get() = - hasPermission(SeerrPermission.REQUEST_MOVIE) || hasPermission(SeerrPermission.REQUEST_4K_MOVIE) - -val SeerrUserConfig?.canRequestTv: Boolean - get() = - hasPermission(SeerrPermission.REQUEST_TV) || hasPermission(SeerrPermission.REQUEST_4K_TV) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPluginConfig.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPluginConfig.kt deleted file mode 100644 index 1ff7212b..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPluginConfig.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.damontecres.wholphin.data.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -sealed interface SeerrPluginLogin { - @SerialName("None") - @Serializable - data object None : SeerrPluginLogin - - @SerialName("ApiKey") - @Serializable - data class ApiKey( - val apiKey: String, - ) : SeerrPluginLogin - - @SerialName("Local") - @Serializable - data class Local( - val local: SeerrPluginLocalLogin, - ) : SeerrPluginLogin -} - -@Serializable -data class SeerrPluginLocalLogin( - val username: String, - val password: String, -) - -@Serializable -data class SeerrPluginSettings( - val version: Int = 1, - val serverUrl: String? = null, - val login: SeerrPluginLogin? = null, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeriesTrackChoice.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeriesTrackChoice.kt deleted file mode 100644 index 5dd313d6..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeriesTrackChoice.kt +++ /dev/null @@ -1,199 +0,0 @@ -@file:UseSerializers(UUIDSerializer::class) - -package com.github.damontecres.wholphin.data.model - -import androidx.room.Entity -import androidx.room.ForeignKey -import com.github.damontecres.wholphin.services.isSigns -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.jellyfin.sdk.model.api.MediaStream -import org.jellyfin.sdk.model.api.MediaStreamType -import org.jellyfin.sdk.model.serializer.UUIDSerializer -import java.util.UUID - -/** - * Stores the user's chosen track for a media item. This is by type, ie Audio vs Subtitle tracks - * - * Additional information about the chosen track is stored to compare against tracks in other media - */ -@Entity( - foreignKeys = [ - ForeignKey( - entity = JellyfinUser::class, - parentColumns = arrayOf("rowId"), - childColumns = arrayOf("userId"), - onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE, - ), - ], - primaryKeys = ["userId", "parentId", "type"], -) -@Serializable -data class SeriesTrackChoice( - val userId: Int, - /** - * Season or Series - */ - val parentId: UUID, - /** - * Type of track, ie audio or subtitle - */ - val type: SeriesTrackChoiceType, - /** - * Origin of the [parentId] - */ - val parentType: TrackChoiceParentType = TrackChoiceParentType.UNKNOWN, - val itemId: UUID? = null, - val language: String? = null, - val activation: ActivationFlag = ActivationFlag.ACTIVATED, - /** - * Flags for the track such as external or forced - * - * @see [TrackFlag] - */ - val trackFlags: Int = 0, - val codec: String? = null, - /** - * The track index based on its position within all streams of the same type - * - * @see [RelativeMediaStream] - */ - val relativeTrackIndex: Int? = null, - val title: String? = null, - val audioChannels: Int? = null, -) { - constructor( - userId: Int, - itemId: UUID, - parentId: UUID, - type: SeriesTrackChoiceType, - parentType: TrackChoiceParentType, - stream: RelativeMediaStream?, - activation: ActivationFlag, - ) : this( - userId = userId, - parentId = parentId, - parentType = parentType, - type = type, - itemId = itemId, - language = stream?.language, - activation = activation, - trackFlags = stream?.let { TrackFlag.calculateTrackFlags(stream.stream) } ?: 0, - codec = stream?.stream?.codec, - relativeTrackIndex = stream?.relativeIndex, - title = stream?.stream?.title, - audioChannels = stream?.stream?.channels, - ) - - companion object { - /** - * Convert the old [PlaybackLanguageChoice] into [SeriesTrackChoice]s - */ - fun from(plc: PlaybackLanguageChoice): List = - buildList { - if (plc.subtitleLanguage != null) { - add( - SeriesTrackChoice( - userId = plc.userId, - parentId = plc.seriesId, - parentType = TrackChoiceParentType.DEPRECATED, - type = SeriesTrackChoiceType.SUBTITLE, - itemId = plc.itemId, - language = plc.subtitleLanguage, - activation = if (plc.subtitlesDisabled == true) ActivationFlag.DISABLED else ActivationFlag.ACTIVATED, - trackFlags = 0, - codec = null, - relativeTrackIndex = null, - title = null, - audioChannels = null, - ), - ) - } - if (plc.audioLanguage != null) { - add( - SeriesTrackChoice( - userId = plc.userId, - parentId = plc.seriesId, - parentType = TrackChoiceParentType.DEPRECATED, - type = SeriesTrackChoiceType.AUDIO, - itemId = plc.itemId, - language = plc.audioLanguage, - activation = ActivationFlag.ACTIVATED, - trackFlags = 0, - codec = null, - relativeTrackIndex = null, - title = null, - audioChannels = null, - ), - ) - } - } - } -} - -enum class SeriesTrackChoiceType( - val mediaStreamType: MediaStreamType, -) { - AUDIO(MediaStreamType.AUDIO), - SUBTITLE(MediaStreamType.SUBTITLE), -} - -/** - * Flags for a track such as forced or defect - */ -enum class TrackFlag( - val flag: Int, - val hasFlag: (MediaStream) -> Boolean, -) { - DEFAULT(flag = 1, hasFlag = { it.isDefault }), - FORCED(flag = 2, hasFlag = { it.isForced }), - SDH(flag = 4, hasFlag = { it.isHearingImpaired }), - EXTERNAL(flag = 8, hasFlag = { it.isExternal }), - SIGNS(flag = 16, hasFlag = { isSigns(it) }), - ; - - fun within(flag: Int) = flag and this.flag == this.flag - - companion object { - fun SeriesTrackChoice.has(flag: TrackFlag) = flag.within(trackFlags) - - fun Iterable.calculateFlag() = fold(0) { flag, trackFlag -> flag or trackFlag.flag } - - fun calculateTrackFlags(track: MediaStream): Int = TrackFlag.entries.filter { it.hasFlag.invoke(track) }.calculateFlag() - } -} - -/** - * How was the track activated - */ -enum class ActivationFlag { - ACTIVATED, - DISABLED, - ONLY_FORCED, -} - -/** - * Where the [SeriesTrackChoice.parentId] comes from - */ -enum class TrackChoiceParentType { - /** - * Parent ID is from the series - */ - SERIES, - - /** - * Parent ID is from the season - */ - SEASON, - - /** - * Parent ID is from the deprecated [PlaybackLanguageChoice] - */ - DEPRECATED, - - /** - * Parent ID origin is unknown - */ - UNKNOWN, -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt index 581a4aa2..1085a8a6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt @@ -1,6 +1,10 @@ package com.github.damontecres.wholphin.data.model +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import com.github.damontecres.wholphin.ui.dot import java.util.Locale /** @@ -39,32 +43,16 @@ data class RtScore( ) /** - * The badge drawn in front of a rating's number. One per PWA icon. - */ -enum class RatingBadge { - RT_FRESH, - RT_ROTTEN, - RT_UPRIGHT, - RT_SPILLED, - - /** Rotten Tomatoes without a recognised label, drawn muted */ - RT_UNKNOWN, - IMDB, - TMDB, -} - -/** - * One displayable rating: a badge and its number + * One displayable rating */ @Immutable sealed interface RatingEntry { val text: String - val badge: RatingBadge /** - * An episode's own score rather than its series', which is drawn with a label so it is not mistaken for the series + * The id of the inline content (see [com.github.damontecres.wholphin.ui.components.rememberQuickDetailsContentMap]) for this entry's badge */ - val isEpisode: Boolean get() = false + val inlineContentId: String data class RottenTomatoes( val score: RtScore, @@ -72,14 +60,14 @@ sealed interface RatingEntry { val critics: Boolean, ) : RatingEntry { override val text: String get() = "${score.percent}%" - override val badge: RatingBadge + override val inlineContentId: String get() = when (score.mark) { - RtMark.FRESH -> RatingBadge.RT_FRESH - RtMark.ROTTEN -> RatingBadge.RT_ROTTEN - RtMark.UPRIGHT -> RatingBadge.RT_UPRIGHT - RtMark.SPILLED -> RatingBadge.RT_SPILLED - RtMark.UNKNOWN -> RatingBadge.RT_UNKNOWN + RtMark.FRESH -> INLINE_RT_FRESH + RtMark.ROTTEN -> INLINE_RT_ROTTEN + RtMark.UPRIGHT -> INLINE_RT_UPRIGHT + RtMark.SPILLED -> INLINE_RT_SPILLED + RtMark.UNKNOWN -> INLINE_RT_UNKNOWN } } @@ -87,25 +75,25 @@ sealed interface RatingEntry { val score: Double, ) : RatingEntry { override val text: String get() = formatScore(score) - override val badge: RatingBadge get() = RatingBadge.IMDB + override val inlineContentId: String get() = INLINE_IMDB } data class Tmdb( val score: Double, ) : RatingEntry { override val text: String get() = formatScore(score) - override val badge: RatingBadge get() = RatingBadge.TMDB - } - - data class EpisodeTmdb( - val score: Double, - ) : RatingEntry { - override val text: String get() = formatScore(score) - override val badge: RatingBadge get() = RatingBadge.TMDB - override val isEpisode: Boolean get() = true + override val inlineContentId: String get() = INLINE_TMDB } } +const val INLINE_RT_FRESH = "fresh" +const val INLINE_RT_ROTTEN = "rotten" +const val INLINE_RT_UPRIGHT = "rt_upright" +const val INLINE_RT_SPILLED = "rt_spilled" +const val INLINE_RT_UNKNOWN = "rt_unknown" +const val INLINE_IMDB = "imdb" +const val INLINE_TMDB = "tmdb" + private fun formatScore(score: Double) = String.format(Locale.getDefault(), "%.1f", score) /** @@ -120,10 +108,8 @@ data class SourceRatings( val rtAudience: RtScore? = null, val imdb: Double? = null, val tmdb: Double? = null, - /** The TMDB user score of the episode itself, for episodes */ - val episodeTmdb: Double? = null, ) { - val isEmpty: Boolean get() = rtCritics == null && rtAudience == null && imdb == null && tmdb == null && episodeTmdb == null + val isEmpty: Boolean get() = rtCritics == null && rtAudience == null && imdb == null && tmdb == null /** * Rotten Tomatoes entries in PWA order: critics then audience @@ -143,40 +129,12 @@ data class SourceRatings( listOfNotNull( imdb?.let { RatingEntry.Imdb(it) }, tmdb?.let { RatingEntry.Tmdb(it) }, - episodeTmdb?.let { RatingEntry.EpisodeTmdb(it) }, ) - /** - * These ratings of a series with the TMDB score of one of its episodes. Rotten Tomatoes stays for context, but the - * series' own TMDB score is dropped so there is a single TMDB number, which is the episode's. - * - * @param episodeScore from [episodeScore], if null these ratings are unchanged - */ - fun withEpisode(episodeScore: Double?): SourceRatings = if (episodeScore == null) this else copy(tmdb = null, episodeTmdb = episodeScore) - - /** - * The entries to draw in PWA order (Rotten Tomatoes, then IMDb or TMDB). - * - * Rotten Tomatoes is controlled by the critic rating display setting and IMDb/TMDB by the community rating setting. - */ - fun visibleEntries( - community: Boolean, - critic: Boolean, - ): List = (if (critic) criticEntries else emptyList()) + (if (community) communityEntries else emptyList()) - companion object { const val MAX_PERCENT = 100.0 const val MAX_SCORE = 10.0 - /** - * An episode's TMDB user score by the PWA's rules for a user score: only if observed, within 0-10, and with at least one vote - * (no votes or no vote count is unrated, never 0) - */ - fun episodeScore( - score: Number?, - votes: Long?, - ): Double? = if (votes == null) null else userScore(score, votes) - /** * Builds ratings using the PWA's rules: * - Only observed scores are kept. A score outside its range (RT 0-100, IMDb/TMDB 0-10) or with a negative vote count is dropped @@ -229,3 +187,38 @@ data class SourceRatings( } } } + +/** + * Appends each entry as a dot, its badge, and its score + */ +fun AnnotatedString.Builder.appendRatings(entries: List) { + entries.forEach { + dot() + appendInlineContent(it.inlineContentId) + append(" ") + append(it.text) + } +} + +/** + * Replaces the Jellyfin community & critic ratings with these ratings. + * + * IMDb/TMDB are shown with the community rating setting, Rotten Tomatoes with the critic rating setting. + * If there are no ratings, the Jellyfin values are kept. + */ +fun QuickDetailsData.withSourceRatings(ratings: SourceRatings?): QuickDetailsData = + if (ratings == null || ratings.isEmpty) { + this + } else { + copy( + communityRating = + ratings.communityEntries.takeIf { it.isNotEmpty() }?.let { + buildAnnotatedString { appendRatings(it) } + }, + criticRating = + ratings.criticEntries.takeIf { it.isNotEmpty() }?.let { + buildAnnotatedString { appendRatings(it) } + }, + criticRatingFirst = true, + ) + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index b6d9426c..188b7020 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -9,7 +9,6 @@ import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.WholphinApplication import com.github.damontecres.wholphin.services.UpdateChecker -import com.github.damontecres.wholphin.ui.formatMaxBitrate import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.ConditionalPreferences import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup @@ -346,7 +345,7 @@ sealed interface AppPreference { const val MEGA_BIT = 1024 * 1024L const val DEFAULT_BITRATE = 100 * MEGA_BIT - val bitrateValues = + private val bitrateValues = listOf( 500 * 1024L, 750 * 1024L, @@ -379,7 +378,11 @@ sealed interface AppPreference { summarizer = { value -> if (value != null) { val v = bitrateValues.getOrNull(value.toInt()) ?: DEFAULT_BITRATE - formatMaxBitrate(v) + if (v < MEGA_BIT) { + "${v / 1024} kbps" + } else { + "${v / MEGA_BIT} Mbps" + } } else { null } @@ -453,69 +456,15 @@ sealed interface AppPreference { summaryOff = R.string.disabled, ) - val Hdr10Override = - AppChoicePreference( - title = R.string.hdr10_override, - defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO, - getter = { it.playbackPreferences.overrides.hdr10Override }, + val DirectPlayDoviProfile7 = + AppSwitchPreference( + title = R.string.force_dovi_profile_7, + defaultValue = false, + getter = { it.playbackPreferences.overrides.directPlayDolbyVisionEL }, setter = { prefs, value -> - prefs.updatePlaybackOverrides { hdr10Override = value } + prefs.updatePlaybackOverrides { directPlayDolbyVisionEL = value } }, - displayValues = R.array.hdr_override_modes, - indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO }, - valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 }, - ) - - val Hdr10PlusOverride = - AppChoicePreference( - title = R.string.hdr10_plus_override, - defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO, - getter = { it.playbackPreferences.overrides.hdr10PlusOverride }, - setter = { prefs, value -> - prefs.updatePlaybackOverrides { hdr10PlusOverride = value } - }, - displayValues = R.array.hdr_override_modes, - indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO }, - valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 }, - ) - - val DoviProfile5Override = - AppChoicePreference( - title = R.string.dovi_profile_5, - defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO, - getter = { it.playbackPreferences.overrides.doviProfile5Override }, - setter = { prefs, value -> - prefs.updatePlaybackOverrides { doviProfile5Override = value } - }, - displayValues = R.array.hdr_override_modes, - indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO }, - valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 }, - ) - - val DoviProfile7Override = - AppChoicePreference( - title = R.string.dovi_profile_7, - defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO, - getter = { it.playbackPreferences.overrides.doviProfile7Override }, - setter = { prefs, value -> - prefs.updatePlaybackOverrides { doviProfile7Override = value } - }, - displayValues = R.array.hdr_override_modes, - indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO }, - valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 }, - ) - - val DoviProfile8Override = - AppChoicePreference( - title = R.string.dovi_profile_8, - defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO, - getter = { it.playbackPreferences.overrides.doviProfile8Override }, - setter = { prefs, value -> - prefs.updatePlaybackOverrides { doviProfile8Override = value } - }, - displayValues = R.array.hdr_override_modes, - indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO }, - valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 }, + summary = R.string.force_dovi_profile_7_summary, ) val DecodeAv1 = @@ -557,7 +506,7 @@ sealed interface AppPreference { val ThemeColors = AppChoicePreference( title = R.string.app_theme, - defaultValue = AppThemeColors.PURPLE, + defaultValue = AppThemeColors.WOLFSKIN, getter = { it.interfacePreferences.appThemeColors }, setter = { prefs, value -> prefs.updateInterfacePreferences { appThemeColors = value } @@ -647,19 +596,6 @@ sealed interface AppPreference { destination = Destination.Settings(PreferenceScreenOption.ADVANCED), ) - val WolfskinScreens = - AppSwitchPreference( - title = R.string.wolfskin_screens, - defaultValue = true, - getter = { org.justinvisser.wolfskin.tv.WolfskinScreenPreference.enabled }, - setter = { prefs, value -> - org.justinvisser.wolfskin.tv.WolfskinScreenPreference.enabled = value - prefs - }, - summaryOn = R.string.enabled, - summaryOff = R.string.disabled, - ) - val UserProfileSettings = AppDestinationPreference( title = R.string.override_user_profile_settings, @@ -983,12 +919,6 @@ sealed interface AppPreference { destination = Destination.Settings(PreferenceScreenOption.MPV), ) - val HdrFormatSettings = - AppDestinationPreference( - title = R.string.hdr_formats, - destination = Destination.Settings(PreferenceScreenOption.HDR_FORMATS), - ) - val MpvHardwareDecoding = AppSwitchPreference( title = R.string.mpv_hardware_decoding, @@ -1253,17 +1183,8 @@ private val ExoPlayerSettings = AppPreference.Ac3Supported, AppPreference.AssSubtitleMode, AppPreference.DirectPlayPgs, + AppPreference.DirectPlayDoviProfile7, AppPreference.DecodeAv1, - AppPreference.HdrFormatSettings, - ) - -private val HdrFormatSettings = - listOf( - AppPreference.Hdr10Override, - AppPreference.Hdr10PlusOverride, - AppPreference.DoviProfile5Override, - AppPreference.DoviProfile7Override, - AppPreference.DoviProfile8Override, ) val ExoPlayerPreferences = @@ -1274,14 +1195,6 @@ val ExoPlayerPreferences = ), ) -val HdrFormatPreferences = - listOf( - PreferenceGroup( - title = R.string.hdr_formats, - preferences = HdrFormatSettings, - ), - ) - private val MpvSettings = listOf( AppPreference.MpvHardwareDecoding, @@ -1319,7 +1232,6 @@ val advancedPreferences = title = R.string.ui_interface, preferences = listOf( - AppPreference.WolfskinScreens, AppPreference.ShowClock, AppPreference.BackdropStylePref, AppPreference.ShowLogos, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 85f8aade..cb47ca94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -186,8 +186,6 @@ class AppPreferencesSerializer disableAudioOffload = ExperimentalPreference.DisableAudioOffload.defaultValue preferDolbyVisionOverHdr10Plus = ExperimentalPreference.PreferDolbyVisionOverHdr10Plus.defaultValue - convertDolbyVisionProfile7 = - ExperimentalPreference.ConvertDolbyVisionProfile7.defaultValue }.build() }.build() diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt index 46c75636..0647a900 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt @@ -48,17 +48,6 @@ object ExperimentalPreference { summary = R.string.prefer_dovi_over_hdr10_plus_summary, ) - val ConvertDolbyVisionProfile7 = - AppSwitchPreference( - title = R.string.convert_dovi_profile_7, - defaultValue = false, - getter = { it.experimentalPreferences.convertDolbyVisionProfile7 }, - setter = { prefs, value -> - prefs.updateExperimentalPreferences { convertDolbyVisionProfile7 = value } - }, - summary = R.string.convert_dovi_profile_7_summary, - ) - val PreferAc3ForSurround = AppSwitchPreference( title = R.string.prefer_ac3_for_surround, @@ -104,7 +93,6 @@ val experimentalPreferences = listOf( ExperimentalPreference.VideoTunneling, ExperimentalPreference.PreferDolbyVisionOverHdr10Plus, - ExperimentalPreference.ConvertDolbyVisionProfile7, ExperimentalPreference.PreferAc3ForSurround, ExperimentalPreference.DisableAudioOffload, ), diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/HdrFormat.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/HdrFormat.kt deleted file mode 100644 index df0b6823..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/HdrFormat.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.damontecres.wholphin.preferences - -import com.github.damontecres.wholphin.util.profile.WholphinVideoRangeType - -// Adapted from https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/preference/constant/HdrFormat.kt - -/** - * HDR formats that the user can override in the device profile. - */ -enum class HdrFormat( - val getter: (AppPreferences) -> HdrOverrideMode, - val rangeTypeNames: (jellyfinTenEleven: Boolean) -> Set, -) { - HDR10( - getter = { it.playbackPreferences.overrides.hdr10Override }, - rangeTypeNames = { setOf(WholphinVideoRangeType.HDR10.serialName) }, - ), - HDR10_PLUS( - getter = { it.playbackPreferences.overrides.hdr10PlusOverride }, - rangeTypeNames = { setOf(WholphinVideoRangeType.HDR10_PLUS.serialName) }, - ), - DOVI_PROFILE_5( - getter = { it.playbackPreferences.overrides.doviProfile5Override }, - rangeTypeNames = { setOf(WholphinVideoRangeType.DOVI.serialName) }, - ), - DOVI_PROFILE_7( - getter = { it.playbackPreferences.overrides.doviProfile7Override }, - rangeTypeNames = { jellyfinTenEleven -> - if (jellyfinTenEleven) { - setOf(WholphinVideoRangeType.DOVI_WITH_EL.serialName, WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName) - } else { - emptySet() - } - }, - ), - DOVI_PROFILE_8( - getter = { it.playbackPreferences.overrides.doviProfile8Override }, - rangeTypeNames = { jellyfinTenEleven -> - buildSet { - add(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName) - if (jellyfinTenEleven) add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName) - } - }, - ), -} - -/** - * Collects the video range type names for every [HdrFormat] whose preference is currently set to [mode] - */ -fun AppPreferences.getHdrRangeTypesFor( - mode: HdrOverrideMode, - jellyfinTenEleven: Boolean, -): Set = - HdrFormat.entries - .filter { it.getter(this) == mode } - .flatMapTo(mutableSetOf()) { it.rangeTypeNames(jellyfinTenEleven) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 17312378..4910110a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -6,20 +6,14 @@ import android.os.Build import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.preference.PreferenceManager -import com.github.damontecres.wholphin.data.CurrentUser import com.github.damontecres.wholphin.data.JellyfinServerDao -import com.github.damontecres.wholphin.data.MostRecentServerProvider -import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao import com.github.damontecres.wholphin.data.RememberedTabDao import com.github.damontecres.wholphin.data.SeerrServerDao -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.RememberedTab -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.ExperimentalPreference -import com.github.damontecres.wholphin.preferences.HdrOverrideMode import com.github.damontecres.wholphin.preferences.ScreensaverPreference import com.github.damontecres.wholphin.preferences.update import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences @@ -42,7 +36,6 @@ import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl import com.github.damontecres.wholphin.util.Version import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber @@ -63,9 +56,6 @@ class AppUpgradeHandler private val seerrServerDao: SeerrServerDao, private val serverDao: JellyfinServerDao, private val rememberedTabDao: RememberedTabDao, - private val mostRecentServerProvider: MostRecentServerProvider, - private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao, - private val seriesTrackChoiceDao: SeriesTrackChoiceDao, ) { val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0) val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!) @@ -452,59 +442,5 @@ class AppUpgradeHandler } } } - - if (current.isEqualOrBefore(Version.fromString("1.0.8-0-g0"))) { - try { - val prefs = appPreferences.data.firstOrNull() - val serverId = prefs?.currentServerId?.toUUIDOrNull() - val userId = prefs?.currentUserId?.toUUIDOrNull() - if (serverId != null && userId != null) { - val server = serverDao.getServer(serverId) - val user = server?.users?.firstOrNull { it.id == userId } - if (server != null && user != null) { - Timber.i("Migrating most recent server") - mostRecentServerProvider.save(CurrentUser(server.server, user)) - } - } - } catch (ex: Exception) { - Timber.e(ex, "Error during migration for most recent server") - } - appPreferences.updateData { - it.update { - currentServerId = "" - currentUserId = "" - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("1.0.10"))) { - // directPlayDolbyVisionEL was replaced by doviProfile7Override; carry the setting forward - appPreferences.updateData { - if (it.playbackPreferences.overrides.directPlayDolbyVisionEL) { - it.updatePlaybackOverrides { - doviProfile7Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE - directPlayDolbyVisionEL = false - } - } else { - it - } - } - } - - if (previous.isLessThan(Version.fromString("1.0.10-0-g0"))) { - val count = seriesTrackChoiceDao.count() - if (count == 0) { - var offset = 0 - while (true) { - val plcs = playbackLanguageChoiceDao.getAll(100, offset) - if (plcs.isEmpty()) break - offset += plcs.size - - val stcs = plcs.flatMap(SeriesTrackChoice::from) - Timber.i("Migrating %s PLC to %s STC", plcs.size, stcs.size) - seriesTrackChoiceDao.save(stcs) - } - } - } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt index 13ef5f70..b9b15f97 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt @@ -4,12 +4,8 @@ import android.content.Context import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AssPlaybackMode import com.github.damontecres.wholphin.preferences.ExperimentalPreferences -import com.github.damontecres.wholphin.preferences.HdrFormat -import com.github.damontecres.wholphin.preferences.HdrOverrideMode import com.github.damontecres.wholphin.preferences.PlaybackOverrides import com.github.damontecres.wholphin.preferences.enabled -import com.github.damontecres.wholphin.preferences.getHdrRangeTypesFor -import com.github.damontecres.wholphin.util.DolbyVisionProfile7Conversion import com.github.damontecres.wholphin.util.WholphinDispatchers import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest import com.github.damontecres.wholphin.util.profile.createDeviceProfile @@ -65,22 +61,9 @@ class DeviceProfileService downMixAudio = newConfig.overrides.downmixStereo, assDirectPlay = newConfig.overrides.assPlaybackMode != AssPlaybackMode.ASS_TRANSCODE, pgsDirectPlay = newConfig.overrides.directPlayPgs, + dolbyVisionELDirectPlay = newConfig.overrides.directPlayDolbyVisionEL, decodeAv1 = prefs.overrides.decodeAv1, preferAc3ForSurround = appPrefs.experimentalPreferences.enabled { preferAc3Surround }, - forceEnabledHdr = - buildSet { - appPrefs - .getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, newConfig.jellyfinTenEleven) - .let(::addAll) - // A profile 7 track the player converts to 8.1 is one the server need not transcode - if (DolbyVisionProfile7Conversion.isWanted(newConfig.experimental, mediaCodecCapabilitiesTest)) { - HdrFormat.DOVI_PROFILE_7 - .rangeTypeNames(newConfig.jellyfinTenEleven) - .let(::addAll) - } - }, - forceDisabledHdr = - appPrefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, newConfig.jellyfinTenEleven), jellyfinTenEleven = newConfig.jellyfinTenEleven, maxResolution = newConfig.overrides.maxResolution, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeDataService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeDataService.kt deleted file mode 100644 index fe462b98..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeDataService.kt +++ /dev/null @@ -1,765 +0,0 @@ -package com.github.damontecres.wholphin.services - -import androidx.core.net.toUri -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.HomeRowConfig -import com.github.damontecres.wholphin.data.model.ServerUserConfig -import com.github.damontecres.wholphin.data.model.createGenreDestination -import com.github.damontecres.wholphin.data.model.createStudioDestination -import com.github.damontecres.wholphin.preferences.HomePagePreferences -import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient -import com.github.damontecres.wholphin.ui.HomeItemFields -import com.github.damontecres.wholphin.ui.ProgramItemFields -import com.github.damontecres.wholphin.ui.components.getGenreImageMap -import com.github.damontecres.wholphin.ui.formatTypeName -import com.github.damontecres.wholphin.ui.main.settings.Library -import com.github.damontecres.wholphin.ui.playback.getTypeFor -import com.github.damontecres.wholphin.ui.toBaseItems -import com.github.damontecres.wholphin.ui.util.ResArgStringProvider -import com.github.damontecres.wholphin.ui.util.ResProviderStringProvider -import com.github.damontecres.wholphin.ui.util.ResStringProvider -import com.github.damontecres.wholphin.ui.util.StringStringProvider -import com.github.damontecres.wholphin.util.ApiRequestPager -import com.github.damontecres.wholphin.util.GetArtistsHandler -import com.github.damontecres.wholphin.util.GetGenresRequestHandler -import com.github.damontecres.wholphin.util.GetItemsRequestHandler -import com.github.damontecres.wholphin.util.GetLiveTvChannelsRequestHandler -import com.github.damontecres.wholphin.util.GetPersonsHandler -import com.github.damontecres.wholphin.util.GetProgramsDtoHandler -import com.github.damontecres.wholphin.util.GetRecordingsRequestHandler -import com.github.damontecres.wholphin.util.GetStudiosRequestHandler -import com.github.damontecres.wholphin.util.HomeRowLoadingState -import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.decodeFromStream -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.exception.InvalidStatusException -import org.jellyfin.sdk.api.client.extensions.liveTvApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.model.DateTime -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.GetProgramsDto -import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.ItemSortBy -import org.jellyfin.sdk.model.api.SortOrder -import org.jellyfin.sdk.model.api.request.GetArtistsRequest -import org.jellyfin.sdk.model.api.request.GetGenresRequest -import org.jellyfin.sdk.model.api.request.GetItemsRequest -import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest -import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest -import org.jellyfin.sdk.model.api.request.GetPersonsRequest -import org.jellyfin.sdk.model.api.request.GetRecordingsRequest -import org.jellyfin.sdk.model.api.request.GetStudiosRequest -import timber.log.Timber -import java.time.LocalDateTime -import javax.inject.Inject -import javax.inject.Singleton - -@OptIn(ExperimentalSerializationApi::class) -@Singleton -class HomeDataService - @Inject - constructor( - private val api: ApiClient, - @param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient, - private val serverRepository: ServerRepository, - private val latestNextUpService: LatestNextUpService, - private val imageUrlService: ImageUrlService, - private val suggestionService: SuggestionService, - ) { - /** - * Fetch the data from the server for a given [HomeRowConfig] - */ - suspend fun fetchDataForRow( - row: HomeRowConfig, - scope: CoroutineScope, - prefs: HomePagePreferences, - userDto: ServerUserConfig, - libraries: List, - limit: Int = prefs.maxItemsPerRow, - isRefresh: Boolean, - usePaging: Boolean = false, - ): HomeRowLoadingState = - when (row) { - is HomeRowConfig.ContinueWatching -> { - val resume = - latestNextUpService.getResume( - userDto.id, - limit, - true, - row.viewOptions.useSeries, - ) - - Success( - title = ResStringProvider(R.string.continue_watching), - items = resume, - viewOptions = row.viewOptions, - rowType = row, - showViewMore = resume.size >= limit, - ) - } - - is HomeRowConfig.NextUp -> { - val nextUp = - latestNextUpService.getNextUp( - userDto.id, - limit, - prefs.enableRewatchingNextUp, - false, - prefs.maxDaysNextUp, - row.viewOptions.useSeries, - ) - - Success( - title = ResStringProvider(R.string.next_up), - items = nextUp, - viewOptions = row.viewOptions, - rowType = row, - showViewMore = nextUp.size >= limit, - ) - } - - is HomeRowConfig.ContinueWatchingCombined -> { - val resume = - latestNextUpService.getResume( - userDto.id, - limit, - true, - row.viewOptions.useSeries, - ) - val nextUp = - latestNextUpService.getNextUp( - userDto.id, - limit, - prefs.enableRewatchingNextUp, - false, - prefs.maxDaysNextUp, - row.viewOptions.useSeries, - ) - val combined = latestNextUpService.buildCombined(resume, nextUp) - - Success( - title = ResStringProvider(R.string.continue_watching), - items = combined.take(limit), - viewOptions = row.viewOptions, - rowType = row, - showViewMore = combined.size >= limit, - ) - } - - is HomeRowConfig.Genres -> { - val request = - GetGenresRequest( - parentId = row.parentId, - userId = userDto.id, - limit = limit, - ) - val items = - GetGenresRequestHandler - .execute(api, request) - .content.items - val genreIds = items.map { it.id } - val genreImages = - getGenreImageMap( - api = api, - userId = serverRepository.currentUser?.id, - scope = scope, - imageUrlService = imageUrlService, - genres = genreIds, - parentId = row.parentId, - includeItemTypes = null, - cardWidthPx = null, - useCache = isRefresh, - ) - val library = - libraries - .firstOrNull { it.itemId == row.parentId } - - val title = - library?.name?.let { ResArgStringProvider(R.string.genres_in, it) } - ?: ResStringProvider(R.string.genres) - val genres = - items.map { - BaseItem( - it, - false, - genreImages[it.id], - createGenreDestination( - genreId = it.id, - genreName = it.name ?: "", - parentId = row.parentId, - parentName = library?.name, - includeItemTypes = library?.includeItemTypes, - collectionType = - library?.collectionType - ?: CollectionType.UNKNOWN, - ), - ) - } - - Success( - title, - genres, - viewOptions = row.viewOptions, - rowType = row, - showViewMore = genres.size >= limit, - ) - } - - is HomeRowConfig.Studios -> { - val request = - GetStudiosRequest( - parentId = row.parentId, - userId = userDto.id, - limit = limit, - includeItemTypes = listOf(BaseItemKind.SERIES), - ) - val items = - GetStudiosRequestHandler - .execute(api, request) - .content.items - val library = - libraries - .firstOrNull { it.itemId == row.parentId } - val title = - library?.name?.let { ResArgStringProvider(R.string.studios_in, it) } - ?: ResStringProvider(R.string.studios) - val studios = - items.map { - val imageUrl = - imageUrlService.getItemImageUrl( - itemId = it.id, - imageType = ImageType.THUMB, - tag = it.imageTags?.get(ImageType.THUMB), - ) - BaseItem( - it, - false, - imageUrl, - createStudioDestination( - studioId = it.id, - name = it.name ?: "", - parentId = row.parentId, - parentName = library?.name, - includeItemTypes = library?.includeItemTypes, - ), - ) - } - - Success( - title, - studios, - rowType = row, - viewOptions = row.viewOptions, - showViewMore = studios.size >= limit, - ) - } - - is HomeRowConfig.RecentlyAdded -> { - val library = libraries.firstOrNull { it.itemId == row.parentId } - val title = getRecentlyAddedTitle(library?.name) - val request = - GetLatestMediaRequest( - fields = library.itemFields, - imageTypeLimit = 1, - parentId = row.parentId, - groupItems = true, - limit = limit, - isPlayed = null, // Server will handle user's preference - ) - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem(it, row.viewOptions.useSeries) } - .let { - Success( - title, - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - latest - } - - is HomeRowConfig.RecentlyReleased -> { - val library = libraries.firstOrNull { it.itemId == row.parentId } - val title = - library?.name?.let { - ResArgStringProvider(R.string.recently_released_in, it) - } ?: ResStringProvider(R.string.recently_released) - val request = - GetItemsRequest( - parentId = row.parentId, - limit = limit, - sortBy = - listOf( - ItemSortBy.PREMIERE_DATE, - ItemSortBy.SERIES_SORT_NAME, - ItemSortBy.AIRED_EPISODE_ORDER, - ), - sortOrder = - listOf( - SortOrder.DESCENDING, - SortOrder.ASCENDING, - SortOrder.DESCENDING, - ), - fields = library.itemFields, - recursive = true, - maxPremiereDate = LocalDateTime.now(), - isUnaired = false, - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - GetItemsRequestHandler - .execute(api, request) - .content.items - .map { BaseItem(it, row.viewOptions.useSeries) } - }.let { - Success( - title, - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.ByParent -> { - val library = libraries.firstOrNull { it.itemId == row.parentId } - val request = - GetItemsRequest( - userId = userDto.id, - parentId = row.parentId, - recursive = row.recursive, - sortBy = - row.sort?.let { - buildList { - if (it.sort == ItemSortBy.RANDOM) { - add(ItemSortBy.SORT_NAME) - add(ItemSortBy.RANDOM) - } else { - add(it.sort) - if (it.sort != ItemSortBy.SORT_NAME) { - add(ItemSortBy.SORT_NAME) - } - } - } - }, - sortOrder = - row.sort?.let { - buildList { - if (it.sort == ItemSortBy.RANDOM) { - add(SortOrder.ASCENDING) - add(it.direction) - } else { - add(it.direction) - if (it.sort != ItemSortBy.SORT_NAME) { - add(SortOrder.ASCENDING) - } - } - } - }, - limit = limit, - fields = library.itemFields, - ) - - // Not using getItemName because we want to throw the 404 - val title = - api.userLibraryApi - .getItem( - userId = serverRepository.currentUser?.id, - itemId = row.parentId, - ).content.name - ?.let { StringStringProvider(it) } - ?: ResStringProvider(R.string.collection) - if (usePaging) { - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - GetItemsRequestHandler - .execute(api, request) - .content.items - .map { BaseItem(it, row.viewOptions.useSeries) } - }.let { - Success( - title, - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.GetItems -> { - val request = - row.filter.asGetItemsRequest().copy( - userId = userDto.id, - limit = limit, - fields = HomeItemFieldsBoxSets, - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - GetItemsRequestHandler - .execute(api, request) - .content.items - .map { BaseItem(it, row.viewOptions.useSeries) } - }.let { - Success( - StringStringProvider(row.name), - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.Favorite -> { - val title = - ResProviderStringProvider( - R.string.favorite_items_title, - ResStringProvider(formatTypeName(row.kind)), - ) - val resultList = - when (row.kind) { - BaseItemKind.PERSON -> { - val request = - GetPersonsRequest( - userId = userDto.id, - limit = limit, - fields = HomeItemFields, - isFavorite = true, - enableImages = true, - enableImageTypes = listOf(ImageType.PRIMARY), - ) - - GetPersonsHandler.execute(api, request).toBaseItems() - } - - BaseItemKind.MUSIC_ARTIST -> { - val request = - GetArtistsRequest( - userId = userDto.id, - limit = limit, - fields = HomeItemFields, - isFavorite = true, - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetArtistsHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - GetArtistsHandler - .execute(api, request) - .toBaseItems(row.viewOptions.useSeries) - } - } - - else -> { - val fields = - if (row.kind == BaseItemKind.BOX_SET) { - HomeItemFieldsBoxSets - } else { - HomeItemFields - } - val request = - GetItemsRequest( - userId = userDto.id, - recursive = true, - limit = limit, - fields = fields, - includeItemTypes = listOf(row.kind), - isFavorite = true, - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - GetItemsRequestHandler - .execute(api, request) - .toBaseItems(row.viewOptions.useSeries) - } - } - } - resultList.let { - Success( - title, - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.Recordings -> { - val request = - GetRecordingsRequest( - userId = userDto.id, - isInProgress = true, - fields = HomeItemFields, - limit = limit, - enableImages = true, - enableUserData = true, - enableTotalRecordCount = false, - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetRecordingsRequestHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - api.liveTvApi - .getRecordings(request) - .content.items - .map { BaseItem(it, row.viewOptions.useSeries) } - }.let { - Success( - ResStringProvider(R.string.active_recordings), - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.TvPrograms -> { - val request = - GetProgramsDto( - userId = userDto.id, - fields = ProgramItemFields, - limit = limit, - enableUserData = true, - enableImages = true, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO), - imageTypeLimit = 1, - isAiring = true, - minEndDate = DateTime.now().plusMinutes(1), - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetProgramsDtoHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - api.liveTvApi - .getPrograms(request) - .content.items - .map { BaseItem(it, row.viewOptions.useSeries) } - }.let { - Success( - ResStringProvider(R.string.watch_live), - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.TvChannels -> { - val request = - GetLiveTvChannelsRequest( - userId = userDto.id, - fields = HomeItemFields, - limit = limit, - enableImages = true, - ) - if (usePaging) { - ApiRequestPager( - api, - request, - GetLiveTvChannelsRequestHandler, - scope, - useSeriesForPrimary = row.viewOptions.useSeries, - ).init() - } else { - api.liveTvApi - .getLiveTvChannels(request) - .toBaseItems(api, row.viewOptions.useSeries) - }.let { - Success( - ResStringProvider(R.string.channels), - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } - } - - is HomeRowConfig.Suggestions -> { - val library = - api.userLibraryApi - .getItem(itemId = row.parentId) - .content - val title = ResArgStringProvider(R.string.suggestions_for, library.name ?: "") - val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType) - if (itemKind != null) { - val suggestions = - suggestionService - .getSuggestionsFlow(row.parentId, itemKind) - .firstOrNull() - when (suggestions) { - SuggestionsResource.Empty -> { - Success( - title, - listOf(), - row.viewOptions, - rowType = row, - ) - } - - is SuggestionsResource.Success -> { - Success( - title, - suggestions.items, - row.viewOptions, - rowType = row, - showViewMore = suggestions.items.size >= limit, - ) - } - - SuggestionsResource.Loading, - null, - -> { - HomeRowLoadingState.Loading(title) - } - } - } else { - HomeRowLoadingState.Error( - title = title, - message = "Unsupported type ${library.collectionType}", - ) - } - } - - is HomeRowConfig.CustomEndpoint -> { - val title = StringStringProvider(row.title) - try { - val items = - fetchCustomEndpointItems(row) - .map { BaseItem(it, row.viewOptions.useSeries) } - Success(title, items, row.viewOptions, rowType = row) - } catch (ex: Exception) { - Timber.w(ex, "Custom endpoint %s failed", row.endpoint) - HomeRowLoadingState.Error(title, exception = ex) - } - } - } - - private suspend fun fetchCustomEndpointItems(row: HomeRowConfig.CustomEndpoint): List { - val base = - api.baseUrl?.toHttpUrlOrNull() - ?: throw IllegalStateException("Jellyfin baseUrl not set") - val endpointUri = row.endpoint.toUri() - if (!endpointUri.isRelative) { - throw IllegalArgumentException("Custom endpoint must be an path relative to Jellyfin baseUrl: ${row.endpoint}") - } - val params = - buildMap { - serverRepository.currentUser - ?.id - ?.toString() - ?.let { put("userId", it) } - row.query?.forEach { put(it.key, it.value) } - } - val resolved = - base - .resolve(row.endpoint) - ?.newBuilder() - ?.apply { - params.forEach { (k, v) -> addQueryParameter(k, v) } - }?.build() - ?: throw IllegalStateException("Could not resolve endpoint: ${row.endpoint}") - val request = - Request - .Builder() - .url(resolved) - .get() - .apply { - row.headers?.forEach { header(it.key, it.value) } - }.build() - val response = - authOkHttpClient - .newCall(request) - .execute() - return response.use { - if (!it.isSuccessful) { - throw InvalidStatusException(it.code, null) - } - val queryResult = - HomeSettingsService.jsonParser.decodeFromStream(it.body.byteStream()) - Timber.v("Got %s items from custom endpoint", queryResult.items.size) - queryResult.items - } - } - } - -private val Library?.itemFields: List - get() = - if (this?.type == BaseItemKind.COLLECTION_FOLDER && this.collectionType == CollectionType.BOXSETS) { - // Get child count to show in the header - HomeItemFieldsBoxSets - } else { - HomeItemFields - } - -private val Library.includeItemTypes: List? - get() = - collectionType.let { collectionType -> - getTypeFor(collectionType)?.let { type -> listOf(type) } - } - -private val HomeItemFieldsBoxSets get() = HomeItemFields + listOf(ItemFields.CHILD_COUNT) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index 1c05640e..c070b37e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -2,27 +2,48 @@ package com.github.damontecres.wholphin.services import android.content.Context import androidx.annotation.StringRes -import androidx.annotation.VisibleForTesting import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.HomePageSettings import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowViewOptions -import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.SUPPORTED_HOME_PAGE_SETTINGS_VERSION +import com.github.damontecres.wholphin.data.model.ServerUserConfig +import com.github.damontecres.wholphin.data.model.createGenreDestination +import com.github.damontecres.wholphin.data.model.createStudioDestination import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration +import com.github.damontecres.wholphin.preferences.HomePagePreferences import com.github.damontecres.wholphin.ui.AspectRatio import com.github.damontecres.wholphin.ui.Cards +import com.github.damontecres.wholphin.ui.HomeItemFields +import com.github.damontecres.wholphin.ui.ProgramItemFields +import com.github.damontecres.wholphin.ui.components.getGenreImageMap import com.github.damontecres.wholphin.ui.formatTypeName +import com.github.damontecres.wholphin.ui.main.settings.Library +import com.github.damontecres.wholphin.ui.playback.getTypeFor +import com.github.damontecres.wholphin.ui.toBaseItems import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.util.ResArgStringProvider import com.github.damontecres.wholphin.ui.util.ResProviderStringProvider import com.github.damontecres.wholphin.ui.util.ResStringProvider import com.github.damontecres.wholphin.ui.util.StringProvider import com.github.damontecres.wholphin.ui.util.StringStringProvider +import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.GetArtistsHandler +import com.github.damontecres.wholphin.util.GetGenresRequestHandler +import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.GetLiveTvChannelsRequestHandler +import com.github.damontecres.wholphin.util.GetPersonsHandler +import com.github.damontecres.wholphin.util.GetProgramsDtoHandler +import com.github.damontecres.wholphin.util.GetRecordingsRequestHandler +import com.github.damontecres.wholphin.util.GetStudiosRequestHandler +import com.github.damontecres.wholphin.util.HomeRowLoadingState +import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -34,15 +55,32 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.liveTvApi import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import org.jellyfin.sdk.model.DateTime import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType +import org.jellyfin.sdk.model.api.GetProgramsDto +import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.UserDto +import org.jellyfin.sdk.model.api.request.GetArtistsRequest +import org.jellyfin.sdk.model.api.request.GetGenresRequest +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest +import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest +import org.jellyfin.sdk.model.api.request.GetRecordingsRequest +import org.jellyfin.sdk.model.api.request.GetStudiosRequest import timber.log.Timber import java.io.File +import java.time.LocalDateTime import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.cancellation.CancellationException /** * Handles getting home page settings and data @@ -54,11 +92,21 @@ class HomeSettingsService @param:ApplicationContext private val context: Context, private val api: ApiClient, private val serverRepository: ServerRepository, + private val userPreferencesService: UserPreferencesService, private val navDrawerService: NavDrawerService, + private val latestNextUpService: LatestNextUpService, + private val imageUrlService: ImageUrlService, + private val suggestionService: SuggestionService, private val displayPreferencesService: DisplayPreferencesService, - private val serverPluginApi: ServerPluginApi, - private val serverDao: JellyfinServerDao, ) { + @OptIn(ExperimentalSerializationApi::class) + val jsonParser = + Json { + isLenient = true + ignoreUnknownKeys = true + allowTrailingComma = true + } + /** * The current home page settings */ @@ -140,8 +188,9 @@ class HomeSettingsService /** * Decodes [HomePageSettings] from a [JsonElement] skipping any unknown/unparsable rows + * + * This is public only for testing */ - @VisibleForTesting fun decode(element: JsonElement): HomePageSettings { val version = element.jsonObject["version"]?.jsonPrimitive?.intOrNull if (version == null || version > SUPPORTED_HOME_PAGE_SETTINGS_VERSION) { @@ -162,42 +211,6 @@ class HomeSettingsService return HomePageSettings(rows, version) } - private suspend fun tryLoad(block: suspend () -> HomePageSettings?): HomePageSettings? = - try { - block.invoke() - } catch (ex: Exception) { - Timber.w(ex, "Error loading settings") - null - } - - suspend fun fetchSettingsFor( - userId: UUID, - source: HomePageSettingsSource, - ): HomePageResolvedSettings? { - val (resolvedSource, settings) = - when (source) { - HomePageSettingsSource.UNSET -> loadInOrder(userId) - HomePageSettingsSource.LOCAL -> source to loadFromLocal(userId) - HomePageSettingsSource.SERVER_PROFILE -> source to loadFromServer(userId) - HomePageSettingsSource.PLUGIN -> source to serverPluginApi.fetchHomePageSettings() - HomePageSettingsSource.DEFAULT -> source to createDefault(userId).asHomePageSettings() - HomePageSettingsSource.WEB_CONFIG -> source to parseFromWebConfig(userId)?.asHomePageSettings() - } - Timber.i( - "fetch home settings: resolvedSource=%s, userId=%s, found=%s", - resolvedSource, - userId, - settings != null, - ) - return settings?.let { - val resolvedRows = - settings.rows.mapIndexed { index, config -> - resolve(index, config) - } - HomePageResolvedSettings(userId, resolvedSource, resolvedRows) - } - } - /** * Loads [HomePageSettings] into [currentSettings] * @@ -205,70 +218,55 @@ class HomeSettingsService * * Does not persist either the server nor default */ - suspend fun loadCurrentSettings(user: JellyfinUser) { - currentSettings.update { HomePageResolvedSettings.EMPTY } - - Timber.v("Getting setting for %s", user.id) + suspend fun loadCurrentSettings(userId: UUID) { + Timber.v("Getting setting for %s", userId) + // User local then server/remote otherwise create a default val settings = try { - fetchSettingsFor(user.id, user.config.homeSettingsSource) - } catch (ex: CancellationException) { - throw ex + val local = loadFromLocal(userId) + Timber.v("Found local? %s", local != null) + local } catch (ex: Exception) { - Timber.e(ex, "Error loading settings for %s", user.config.homeSettingsSource) + Timber.w(ex, "Error loading local settings") + // TODO show toast? + null + } ?: try { + val remote = loadFromServer(userId) + Timber.v("Found remote? %s", remote != null) + remote + } catch (ex: Exception) { + Timber.w(ex, "Error loading remote settings") null } - Timber.v("Found settings: %s", settings != null) - val resolvedSettings = settings ?: createDefault(user.id) + val resolvedSettings = + if (settings != null) { + Timber.v("Found settings") + // Resolve + val resolvedRows = + settings.rows.mapIndexed { index, config -> + resolve(index, config) + } + HomePageResolvedSettings(userId, resolvedRows) + } else { + createDefault(userId) + } + currentSettings.update { resolvedSettings } } - /** - * Tries to load settings from local->server->plugin - */ - private suspend fun loadInOrder(userId: UUID): Pair { - Timber.v("loadInOrder: local") - var settings = - HomePageSettingsSource.LOCAL to - tryLoad { - loadFromLocal(userId) - } - - if (settings.second == null) { - Timber.v("loadInOrder: server") - settings = - HomePageSettingsSource.SERVER_PROFILE to - tryLoad { - loadFromServer(userId) - } - } - - if (settings.second == null && serverRepository.serverPluginInstalled.value) { - Timber.v("loadInOrder: plugin") - settings = HomePageSettingsSource.PLUGIN to - tryLoad { - serverPluginApi.fetchHomePageSettings() - } - } - return settings - } - /** * Resolve the settings and set them to be the current settings */ suspend fun updateCurrent( - source: HomePageSettingsSource, + userId: UUID, settings: HomePageSettings, ) { val resolvedRows = settings.rows.mapIndexed { index, config -> resolve(index, config) } - currentSettings.update { it.copy(rows = resolvedRows) } - serverRepository.currentUser?.let { user -> - val toSave = user.updateConfig { it.copy(homeSettingsSource = source) } - serverDao.updateUser(toSave) - } + val resolvedSettings = HomePageResolvedSettings(userId, resolvedRows) + currentSettings.update { resolvedSettings } } /** @@ -318,7 +316,7 @@ class HomeSettingsService ), ) val rowConfig = continueWatchingRow + includedIds - return HomePageResolvedSettings(userId, HomePageSettingsSource.DEFAULT, rowConfig) + return HomePageResolvedSettings(userId, rowConfig) } /** @@ -434,7 +432,7 @@ class HomeSettingsService null } }.flatten() - HomePageResolvedSettings(userId, HomePageSettingsSource.WEB_CONFIG, rowConfigs) + HomePageResolvedSettings(userId, rowConfigs) } else { null } @@ -562,14 +560,6 @@ class HomeSettingsService config, ) } - - is HomeRowConfig.CustomEndpoint -> { - HomeRowConfigDisplay( - id = id, - title = StringStringProvider(config.title), - config, - ) - } } private suspend fun getItemName( @@ -595,16 +585,631 @@ class HomeSettingsService ResStringProvider(R.string.unknown) } + /** + * Fetch the data from the server for a given [HomeRowConfig] + */ + suspend fun fetchDataForRow( + row: HomeRowConfig, + scope: CoroutineScope, + prefs: HomePagePreferences, + userDto: ServerUserConfig, + libraries: List, + limit: Int = prefs.maxItemsPerRow, + isRefresh: Boolean, + usePaging: Boolean = false, + ): HomeRowLoadingState = + when (row) { + is HomeRowConfig.ContinueWatching -> { + val resume = + latestNextUpService.getResume( + userDto.id, + limit, + true, + row.viewOptions.useSeries, + ) + + Success( + title = ResStringProvider(R.string.continue_watching), + items = resume, + viewOptions = row.viewOptions, + rowType = row, + showViewMore = resume.size >= limit, + ) + } + + is HomeRowConfig.NextUp -> { + val nextUp = + latestNextUpService.getNextUp( + userDto.id, + limit, + prefs.enableRewatchingNextUp, + false, + prefs.maxDaysNextUp, + row.viewOptions.useSeries, + ) + + Success( + title = ResStringProvider(R.string.next_up), + items = nextUp, + viewOptions = row.viewOptions, + rowType = row, + showViewMore = nextUp.size >= limit, + ) + } + + is HomeRowConfig.ContinueWatchingCombined -> { + val resume = + latestNextUpService.getResume( + userDto.id, + limit, + true, + row.viewOptions.useSeries, + ) + val nextUp = + latestNextUpService.getNextUp( + userDto.id, + limit, + prefs.enableRewatchingNextUp, + false, + prefs.maxDaysNextUp, + row.viewOptions.useSeries, + ) + val combined = latestNextUpService.buildCombined(resume, nextUp) + + Success( + title = ResStringProvider(R.string.continue_watching), + items = combined.take(limit), + viewOptions = row.viewOptions, + rowType = row, + showViewMore = combined.size >= limit, + ) + } + + is HomeRowConfig.Genres -> { + val request = + GetGenresRequest( + parentId = row.parentId, + userId = userDto.id, + limit = limit, + ) + val items = + GetGenresRequestHandler + .execute(api, request) + .content.items + val genreIds = items.map { it.id } + val genreImages = + getGenreImageMap( + api = api, + userId = serverRepository.currentUser?.id, + scope = scope, + imageUrlService = imageUrlService, + genres = genreIds, + parentId = row.parentId, + includeItemTypes = null, + cardWidthPx = null, + useCache = isRefresh, + ) + val library = + libraries + .firstOrNull { it.itemId == row.parentId } + + val title = + library?.name?.let { ResArgStringProvider(R.string.genres_in, it) } + ?: ResStringProvider(R.string.genres) + val genres = + items.map { + BaseItem( + it, + false, + genreImages[it.id], + createGenreDestination( + genreId = it.id, + genreName = it.name ?: "", + parentId = row.parentId, + parentName = library?.name, + includeItemTypes = + library?.collectionType?.let { + getTypeFor(it)?.let { + listOf(it) + } + }, + collectionType = + library?.collectionType + ?: CollectionType.UNKNOWN, + ), + ) + } + + Success( + title, + genres, + viewOptions = row.viewOptions, + rowType = row, + showViewMore = genres.size >= limit, + ) + } + + is HomeRowConfig.Studios -> { + val request = + GetStudiosRequest( + parentId = row.parentId, + userId = userDto.id, + limit = limit, + includeItemTypes = listOf(BaseItemKind.SERIES), + ) + val items = + GetStudiosRequestHandler + .execute(api, request) + .content.items + val library = + libraries + .firstOrNull { it.itemId == row.parentId } + val title = + library?.name?.let { ResArgStringProvider(R.string.studios_in, it) } + ?: ResStringProvider(R.string.studios) + val studios = + items.map { + val imageUrl = + imageUrlService.getItemImageUrl( + itemId = it.id, + imageType = ImageType.THUMB, + ) + BaseItem( + it, + false, + imageUrl, + createStudioDestination( + studioId = it.id, + name = it.name ?: "", + parentId = row.parentId, + parentName = library?.name, + includeItemTypes = + library?.collectionType?.let { + getTypeFor(it)?.let { + listOf(it) + } + }, + ), + ) + } + + Success( + title, + studios, + viewOptions = row.viewOptions, + showViewMore = studios.size >= limit, + ) + } + + is HomeRowConfig.RecentlyAdded -> { + val library = libraries.firstOrNull { it.itemId == row.parentId } + val title = getRecentlyAddedTitle(library?.name) + val request = + GetLatestMediaRequest( + fields = library.itemFields, + imageTypeLimit = 1, + parentId = row.parentId, + groupItems = true, + limit = limit, + isPlayed = null, // Server will handle user's preference + ) + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { BaseItem(it, row.viewOptions.useSeries) } + .let { + Success( + title, + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + latest + } + + is HomeRowConfig.RecentlyReleased -> { + val library = libraries.firstOrNull { it.itemId == row.parentId } + val title = + library?.name?.let { + ResArgStringProvider(R.string.recently_released_in, it) + } ?: ResStringProvider(R.string.recently_released) + val request = + GetItemsRequest( + parentId = row.parentId, + limit = limit, + sortBy = + listOf( + ItemSortBy.PREMIERE_DATE, + ItemSortBy.SERIES_SORT_NAME, + ItemSortBy.AIRED_EPISODE_ORDER, + ), + sortOrder = + listOf( + SortOrder.DESCENDING, + SortOrder.ASCENDING, + SortOrder.DESCENDING, + ), + fields = library.itemFields, + recursive = true, + maxPremiereDate = LocalDateTime.now(), + isUnaired = false, + ) + if (usePaging) { + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + GetItemsRequestHandler + .execute(api, request) + .content.items + .map { BaseItem.from(it, api, row.viewOptions.useSeries) } + }.let { + Success( + title, + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.ByParent -> { + val library = libraries.firstOrNull { it.itemId == row.parentId } + val request = + GetItemsRequest( + userId = userDto.id, + parentId = row.parentId, + recursive = row.recursive, + sortBy = + row.sort?.let { + buildList { + if (it.sort == ItemSortBy.RANDOM) { + add(ItemSortBy.SORT_NAME) + add(ItemSortBy.RANDOM) + } else { + add(it.sort) + if (it.sort != ItemSortBy.SORT_NAME) { + add(ItemSortBy.SORT_NAME) + } + } + } + }, + sortOrder = + row.sort?.let { + buildList { + if (it.sort == ItemSortBy.RANDOM) { + add(SortOrder.ASCENDING) + add(it.direction) + } else { + add(it.direction) + if (it.sort != ItemSortBy.SORT_NAME) { + add(SortOrder.ASCENDING) + } + } + } + }, + limit = limit, + fields = library.itemFields, + ) + + // Not using getItemName because we want to throw the 404 + val title = + api.userLibraryApi + .getItem( + userId = serverRepository.currentUser?.id, + itemId = row.parentId, + ).content.name + ?.let { StringStringProvider(it) } + ?: ResStringProvider(R.string.collection) + if (usePaging) { + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + GetItemsRequestHandler + .execute(api, request) + .content.items + .map { BaseItem(it, row.viewOptions.useSeries) } + }.let { + Success( + title, + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.GetItems -> { + val request = + row.getItems.let { + if (it.limit == null) { + it.copy( + userId = userDto.id, + limit = limit, + ) + } else { + it.copy( + userId = userDto.id, + ) + } + } + if (usePaging) { + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + GetItemsRequestHandler + .execute(api, request) + .content.items + .map { BaseItem(it, row.viewOptions.useSeries) } + }.let { + Success( + StringStringProvider(row.name), + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.Favorite -> { + val title = + ResProviderStringProvider( + R.string.favorite_items_title, + ResStringProvider(formatTypeName(row.kind)), + ) + val resultList = + when (row.kind) { + BaseItemKind.PERSON -> { + val request = + GetPersonsRequest( + userId = userDto.id, + limit = limit, + fields = HomeItemFields, + isFavorite = true, + enableImages = true, + enableImageTypes = listOf(ImageType.PRIMARY), + ) + + GetPersonsHandler.execute(api, request).toBaseItems() + } + + BaseItemKind.MUSIC_ARTIST -> { + val request = + GetArtistsRequest( + userId = userDto.id, + limit = limit, + fields = HomeItemFields, + isFavorite = true, + ) + if (usePaging) { + ApiRequestPager( + api, + request, + GetArtistsHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + GetArtistsHandler + .execute(api, request) + .toBaseItems(row.viewOptions.useSeries) + } + } + + else -> { + val fields = + if (row.kind == BaseItemKind.BOX_SET) { + HomeItemFieldsBoxSets + } else { + HomeItemFields + } + val request = + GetItemsRequest( + userId = userDto.id, + recursive = true, + limit = limit, + fields = fields, + includeItemTypes = listOf(row.kind), + isFavorite = true, + ) + if (usePaging) { + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + GetItemsRequestHandler + .execute(api, request) + .toBaseItems(row.viewOptions.useSeries) + } + } + } + resultList.let { + Success( + title, + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.Recordings -> { + val request = + GetRecordingsRequest( + userId = userDto.id, + isInProgress = true, + fields = HomeItemFields, + limit = limit, + enableImages = true, + enableUserData = true, + enableTotalRecordCount = false, + ) + if (usePaging) { + ApiRequestPager( + api, + request, + GetRecordingsRequestHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + api.liveTvApi + .getRecordings(request) + .content.items + .map { BaseItem(it, row.viewOptions.useSeries) } + }.let { + Success( + ResStringProvider(R.string.active_recordings), + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.TvPrograms -> { + val request = + GetProgramsDto( + userId = userDto.id, + fields = ProgramItemFields, + limit = limit, + enableUserData = true, + enableImages = true, + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO), + imageTypeLimit = 1, + isAiring = true, + minEndDate = DateTime.now().plusMinutes(1), + ) + if (usePaging) { + ApiRequestPager( + api, + request, + GetProgramsDtoHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + api.liveTvApi + .getPrograms(request) + .content.items + .map { BaseItem(it, row.viewOptions.useSeries) } + }.let { + Success( + ResStringProvider(R.string.watch_live), + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.TvChannels -> { + val request = + GetLiveTvChannelsRequest( + userId = userDto.id, + fields = HomeItemFields, + limit = limit, + enableImages = true, + ) + if (usePaging) { + ApiRequestPager( + api, + request, + GetLiveTvChannelsRequestHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + api.liveTvApi + .getLiveTvChannels(request) + .toBaseItems(api, row.viewOptions.useSeries) + }.let { + Success( + ResStringProvider(R.string.channels), + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } + } + + is HomeRowConfig.Suggestions -> { + val library = + api.userLibraryApi + .getItem(itemId = row.parentId) + .content + val title = ResArgStringProvider(R.string.suggestions_for, library.name ?: "") + val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType) + if (itemKind != null) { + val suggestions = + suggestionService + .getSuggestionsFlow(row.parentId, itemKind) + .firstOrNull() + when (suggestions) { + SuggestionsResource.Empty -> { + Success( + title, + listOf(), + row.viewOptions, + rowType = row, + ) + } + + is SuggestionsResource.Success -> { + Success( + title, + suggestions.items, + row.viewOptions, + rowType = row, + showViewMore = suggestions.items.size >= limit, + ) + } + + SuggestionsResource.Loading, + null, + -> { + HomeRowLoadingState.Loading(title) + } + } + } else { + HomeRowLoadingState.Error( + title = title, + message = "Unsupported type ${library.collectionType}", + ) + } + } + } + companion object { const val CUSTOM_PREF_ID = "home_settings" - - @OptIn(ExperimentalSerializationApi::class) - val jsonParser = - Json { - isLenient = true - ignoreUnknownKeys = true - allowTrailingComma = true - } } } @@ -624,14 +1229,10 @@ data class HomeRowConfigDisplay( */ data class HomePageResolvedSettings( val userId: UUID, - val source: HomePageSettingsSource, val rows: List, ) { - fun asHomePageSettings(): HomePageSettings = HomePageSettings(rows.map { it.config }, SUPPORTED_HOME_PAGE_SETTINGS_VERSION) - companion object { - val EMPTY = - HomePageResolvedSettings(UUID.randomUUID(), HomePageSettingsSource.UNSET, emptyList()) + val EMPTY = HomePageResolvedSettings(UUID.randomUUID(), emptyList()) } } @@ -665,6 +1266,17 @@ fun getRecentlyAddedTitle(name: String?): StringProvider = name?.let { ResArgStringProvider(R.string.recently_added_in, it) } ?: ResStringProvider(R.string.recently_added) +private val Library?.itemFields: List + get() = + if (this?.type == BaseItemKind.COLLECTION_FOLDER && this.collectionType == CollectionType.BOXSETS) { + // Get child count to show in the header + HomeItemFieldsBoxSets + } else { + HomeItemFields + } + +private val HomeItemFieldsBoxSets get() = HomeItemFields + listOf(ItemFields.CHILD_COUNT) + fun viewOptionsForCollectionType(collectionType: CollectionType?): HomeRowViewOptions = when (collectionType) { CollectionType.MUSIC, @@ -703,14 +1315,3 @@ fun viewOptionsForCollectionType(collectionType: CollectionType?): HomeRowViewOp HomeRowViewOptions() } } - -enum class HomePageSettingsSource( - @param:StringRes val stringResId: Int, -) { - UNSET(R.string.unknown), - LOCAL(R.string.home_settings_source_local), - SERVER_PROFILE(R.string.home_settings_source_server_profile), - PLUGIN(R.string.home_settings_source_plugin), - DEFAULT(R.string.home_settings_source_unset), - WEB_CONFIG(R.string.home_settings_source_web), -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt index 967629da..6b71d683 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt @@ -35,11 +35,6 @@ class ImageUrlService parentBackdropId: UUID? = null, fillWidth: Int? = null, fillHeight: Int? = null, - seriesPrimaryTag: String? = null, - parentLogoId: UUID? = null, - parentLogoTag: String? = null, - parentThumbTag: String? = null, - parentBackdropTags: List = emptyList(), ): String? = when (imageType) { ImageType.LOGO -> { @@ -49,7 +44,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = parentLogoTag.takeIf { parentLogoId == seriesId }, ) } else { getItemImageUrl( @@ -57,7 +51,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = imageTags[imageType], ) } } @@ -70,7 +63,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = parentBackdropTags.firstOrNull().takeIf { parentBackdropId == seriesId }, ) } else if (backdropTags.isNotEmpty()) { getItemImageUrl( @@ -78,7 +70,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = backdropTags.first(), ) } else { null @@ -95,7 +86,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = parentThumbTag, ) } else if (useSeriesForPrimary && parentBackdropId != null && (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON) @@ -106,7 +96,6 @@ class ImageUrlService imageType = ImageType.BACKDROP, fillWidth = fillWidth, fillHeight = fillHeight, - tag = parentBackdropTags.firstOrNull(), ) } else if (parentThumbId != null && itemType == BaseItemKind.SEASON && imageType !in imageTags) { getItemImageUrl( @@ -114,7 +103,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = parentThumbTag, ) } else if (useSeriesForPrimary && parentThumbId == null && @@ -127,7 +115,6 @@ class ImageUrlService imageType = ImageType.PRIMARY, fillWidth = fillWidth, fillHeight = fillHeight, - tag = imageTags[ImageType.PRIMARY], ) } else if (imageType !in imageTags && backdropTags.isNotEmpty()) { // If no thumb, use backdrop if available @@ -136,7 +123,6 @@ class ImageUrlService imageType = ImageType.BACKDROP, fillWidth = fillWidth, fillHeight = fillHeight, - tag = backdropTags.first(), ) } else { getItemImageUrl( @@ -144,7 +130,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = imageTags[imageType], ) } } @@ -160,7 +145,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = seriesPrimaryTag.takeIf { imageType == ImageType.PRIMARY }, ) } else if (seriesId != null && itemType == BaseItemKind.SEASON && imageType !in imageTags) { getItemImageUrl( @@ -168,7 +152,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = seriesPrimaryTag.takeIf { imageType == ImageType.PRIMARY }, ) } else { getItemImageUrl( @@ -176,7 +159,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = imageTags[imageType], ) } } @@ -187,7 +169,6 @@ class ImageUrlService imageType = imageType, fillWidth = fillWidth, fillHeight = fillHeight, - tag = imageTags[imageType], ) } } @@ -212,11 +193,6 @@ class ImageUrlService backdropTags = item.data.backdropImageTags.orEmpty(), fillWidth = fillWidth, fillHeight = fillHeight, - seriesPrimaryTag = item.data.seriesPrimaryImageTag, - parentLogoId = item.data.parentLogoItemId, - parentLogoTag = item.data.parentLogoImageTag, - parentThumbTag = item.data.parentThumbImageTag, - parentBackdropTags = item.data.parentBackdropImageTags.orEmpty(), ) } else { null diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt index 4fda8260..2dd0cde5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt @@ -2,8 +2,6 @@ package com.github.damontecres.wholphin.services import android.app.SearchManager import android.content.Intent -import androidx.annotation.VisibleForTesting -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds @@ -116,7 +114,6 @@ class IntentService return IntentResult.Target(destinations, true) } - @VisibleForTesting internal suspend fun prepare(intent: Intent): IntentResult? { val appPrefs = userPreferencesService.flow.first().appPreferences @@ -124,33 +121,39 @@ class IntentService val serverId = intent.getStringParam(INTENT_SERVER_ID)?.toUUIDOrNull() return if (userId != null && serverId != null) { Timber.v("Intent switches user") - val user = serverRepository.tryChangeUser(serverId, userId) - if (user != null) { + val user = serverRepository.serverDao.getUser(serverId, userId) + if (user != null && !user.isProtected) { + serverRepository.restoreSession(serverId, userId) + ?: IntentResult.Error("Error restoring user") null } else { - IntentResult.Error("Error restoring user") + IntentResult.Error("Cannot switch to specified user") } - } else if (appPrefs.signInAutomatically) { - Timber.v("No current user, so restoring last") - val result = serverRepository.restoreLastSession() - Timber.v("Restored result=%s", result) - when (result) { - RestoredSession.None, - is RestoredSession.ServerOnly, - -> { - IntentResult.Error("Could not auto sign-in, specify a server & user") - } + } else { + val profileProtected = + serverRepository.current.value + ?.user + ?.isProtected == true + if (appPrefs.signInAutomatically && !profileProtected) { + Timber.v("No current user, so restoring last") + val userId = appPrefs.currentUserId?.toUUIDOrNull() + val serverId = appPrefs.currentServerId?.toUUIDOrNull() - is RestoredSession.Success -> { - if (result.currentUser.user.isProtected) { + if (userId != null && serverId != null) { + val current = + serverRepository.restoreSession(serverId, userId) + ?: return IntentResult.Error("Error restoring user") + if (current.user.isProtected) { IntentResult.Error("Could not auto sign-in, user is protected") } else { null } + } else { + IntentResult.Error("Could not auto sign-in, specify a server & user") } + } else { + IntentResult.Error("Auto sign-in not enabled or user is protected") } - } else { - IntentResult.Error("Auto sign-in not enabled") } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt index dbaa219e..4ce6583f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt @@ -6,7 +6,7 @@ package com.github.damontecres.wholphin.services import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.ui.HomeSlimItemFields +import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.util.LocalDateTimeSerializer import com.github.damontecres.wholphin.util.WholphinDispatchers import kotlinx.coroutines.async @@ -58,7 +58,7 @@ class LatestNextUpService val request = GetResumeItemsRequest( userId = userId, - fields = HomeSlimItemFields, + fields = SlimItemFields, limit = limit, mediaTypes = listOf(MediaType.VIDEO), excludeItemTypes = if (!includeEpisodes) listOf(BaseItemKind.EPISODE) else null, @@ -90,7 +90,7 @@ class LatestNextUpService val request = GetNextUpRequest( userId = userId, - fields = HomeSlimItemFields, + fields = SlimItemFields, imageTypeLimit = 1, parentId = null, limit = limit, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpWorker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpWorker.kt index a60bf7c4..01ed9dd2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpWorker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpWorker.kt @@ -1,7 +1,6 @@ package com.github.damontecres.wholphin.services import android.content.Context -import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.hilt.work.HiltWorker import androidx.lifecycle.lifecycleScope @@ -14,7 +13,6 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.ui.collectLatestIn import com.github.damontecres.wholphin.util.ExceptionHandler @@ -54,8 +52,12 @@ class LatestNextUpWorker try { if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) { // Not active - val result = serverRepository.restoreLastSession() - if (result !is RestoredSession.Success) { + var currentUser = serverRepository.current.value + if (currentUser == null) { + serverRepository.restoreSession(serverId, userId) + currentUser = serverRepository.current.value + } + if (currentUser == null) { Timber.w("No user found during run") return Result.failure() } @@ -89,7 +91,7 @@ class LatestNextUpSchedulerService "SuggestionsSchedulerService requires an AppCompatActivity context, but received: ${context::class.java.name}", ) - @VisibleForTesting + // Exposed for testing internal var dispatcher: CoroutineDispatcher = WholphinDispatchers.IO init { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index 16ac4edd..ec6e9d2c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -122,11 +122,7 @@ class MusicService mutex.withLock { if (mediaSession == null) { Timber.i("Starting music MediaSession") - mediaSession = - MediaSession - .Builder(context, player) - .setCallback(TrustAllControllersCallback) - .build() + mediaSession = MediaSession.Builder(context, player).build() activityTracker = TrackActivityPlaybackListener(api, player) { state.value.currentItemId?.let { itemId -> @@ -331,7 +327,6 @@ class MusicService imageUrlService.getItemImageUrl( itemId = albumId, imageType = ImageType.PRIMARY, - tag = audio.data.albumPrimaryImageTag, ) } return MediaItem diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index a2fd0eb7..9b16718d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -24,7 +24,6 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.video.MediaCodecVideoRenderer import androidx.media3.exoplayer.video.VideoRendererEventListener import androidx.media3.extractor.DefaultExtractorsFactory -import androidx.media3.extractor.ExtractorsFactory import androidx.media3.session.MediaSession import com.github.damontecres.wholphin.mpv.MpvPlayer import com.github.damontecres.wholphin.preferences.AppPreferences @@ -33,15 +32,9 @@ import com.github.damontecres.wholphin.preferences.MediaExtensionStatus import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.get import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient -import com.github.damontecres.wholphin.util.BitstreamFilter import com.github.damontecres.wholphin.util.BitstreamFilteringCodecAdapterFactory -import com.github.damontecres.wholphin.util.DolbyVisionProfile7Conversion -import com.github.damontecres.wholphin.util.DolbyVisionProfile7ExtractorsFactory -import com.github.damontecres.wholphin.util.DolbyVisionProfile7Filter import com.github.damontecres.wholphin.util.Hdr10PlusMaskingFilter import com.github.damontecres.wholphin.util.WholphinDispatchers -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture import dagger.hilt.android.qualifiers.ApplicationContext import io.github.peerless2012.ass.media.AssHandler import io.github.peerless2012.ass.media.factory.AssRenderersFactory @@ -64,7 +57,6 @@ class PlayerFactory constructor( @param:ApplicationContext private val context: Context, @param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient, - private val deviceProfileService: DeviceProfileService, ) { @Volatile var currentPlayer: Player? = null @@ -102,16 +94,10 @@ class PlayerFactory val preferDolbyVision = appPreferences.experimentalPreferences .get { preferDolbyVisionOverHdr10Plus } ?: false - val convertDolbyVisionProfile7 = - DolbyVisionProfile7Conversion.isWanted( - appPreferences.experimentalPreferences, - deviceProfileService.mediaCodecCapabilitiesTest, - ) Timber.v( - "extensions=%s, assPlaybackMode=%s, convertDolbyVisionProfile7=%s", + "extensions=%s, assPlaybackMode=%s", extensions, prefs.overrides.assPlaybackMode, - convertDolbyVisionProfile7, ) val rendererMode = when (extensions) { @@ -122,18 +108,9 @@ class PlayerFactory } val dataSourceFactory = DefaultDataSource.Factory(context) val extractorsFactory = createExtractorsFactory() - - // Applied outermost so that a profile 7 track is advertised as 8.1 whichever - // extractor, media3's own or libass's Matroska replacement, produced it - fun ExtractorsFactory.forDolbyVision(): ExtractorsFactory = - if (convertDolbyVisionProfile7) DolbyVisionProfile7ExtractorsFactory(this) else this var renderersFactory: RenderersFactory = - WholphinRenderersFactory( - context, - decodeAv1, - preferDolbyVision, - convertDolbyVisionProfile7, - ).setEnableDecoderFallback(true) + WholphinRenderersFactory(context, decodeAv1, preferDolbyVision) + .setEnableDecoderFallback(true) .setExtensionRendererMode(rendererMode) val mediaSourceFactory = @@ -149,16 +126,15 @@ class PlayerFactory renderersFactory = AssRenderersFactory(assHandler, renderersFactory) DefaultMediaSourceFactory( dataSourceFactory, - extractorsFactory - .withAssMkvSupport( - assSubtitleParserFactory, - assHandler, - ).forDolbyVision(), + extractorsFactory.withAssMkvSupport( + assSubtitleParserFactory, + assHandler, + ), ).setSubtitleParserFactory(assSubtitleParserFactory) } else { DefaultMediaSourceFactory( dataSourceFactory, - extractorsFactory.forDolbyVision(), + extractorsFactory, ) } val disableAudioOffload = @@ -184,9 +160,8 @@ class PlayerFactory AudioAttributes .Builder() .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) - .setUsage(C.USAGE_MEDIA) .build(), - true, + false, ) } } @@ -271,29 +246,9 @@ class PlayerFactory fun createMediaSession(player: Player) = MediaSession .Builder(context, player) - .setCallback(TrustAllControllersCallback) .build() } -/** - * Since media3 1.11, untrusted controllers (e.g. Google Home's Cast bridge, `com.google.android.apps.mediashell`) - * only get read-only player commands by default, so play/pause from them is silently dropped. - * Restore the pre-1.11 behavior of granting all player commands to every controller. - */ -@OptIn(UnstableApi::class) -object TrustAllControllersCallback : MediaSession.Callback { - override fun onConnectAsync( - session: MediaSession, - controller: MediaSession.ControllerInfo, - ): ListenableFuture = - Futures.immediateFuture( - MediaSession.ConnectionResult - .AcceptedResultBuilder(session, controller) - .setAvailablePlayerCommands(MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS) - .build(), - ) -} - val Player.isReleased: Boolean get() { return when (this) { @@ -313,7 +268,6 @@ class WholphinRenderersFactory( context: Context, private val av1Enabled: Boolean, private val preferDolbyVisionOverHdr10Plus: Boolean = false, - private val convertDolbyVisionProfile7: Boolean = false, ) : DefaultRenderersFactory(context) { @OptIn(ExperimentalApi::class) override fun buildVideoRenderers( @@ -326,16 +280,12 @@ class WholphinRenderersFactory( allowedVideoJoiningTimeMs: Long, out: ArrayList, ) { - // The conversion goes first: it may shorten the access unit, and the masking then only - // has to walk what is left of it - val bitstreamFilters = - buildList { - if (convertDolbyVisionProfile7) add(DolbyVisionProfile7Filter()) - if (preferDolbyVisionOverHdr10Plus) add(Hdr10PlusMaskingFilter()) - } val videoCodecAdapterFactory = - if (bitstreamFilters.isNotEmpty()) { - BitstreamFilteringCodecAdapterFactory(codecAdapterFactory, bitstreamFilters) + if (preferDolbyVisionOverHdr10Plus) { + BitstreamFilteringCodecAdapterFactory( + codecAdapterFactory, + listOf(Hdr10PlusMaskingFilter()), + ) } else { codecAdapterFactory } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt index 08d31a9d..bc59f211 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt @@ -9,19 +9,12 @@ import com.github.damontecres.wholphin.data.model.SourceRatings import com.google.common.cache.CacheBuilder import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemKind import timber.log.Timber -import java.util.Optional -import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton @@ -35,24 +28,7 @@ data class TmdbVote( ) /** - * The Seerr title whose ratings are shown for a Jellyfin item - */ -data class RatingsTarget( - val type: SeerrItemType, - val tmdbId: Int, -) - -/** - * Fetches [SourceRatings] for a title from Seerr, the same source the Wolfskin PWA uses. This is the only way ratings - * reach the UI, see [com.github.damontecres.wholphin.ui.components.rememberSourceRatings]. - * - * Which Jellyfin items have ratings follows the PWA, which shows ratings for a title and nothing else: - * - movies show their own ratings - * - series show their own ratings - * - seasons show their series' ratings, as the PWA does for the title page a season belongs to - * - episodes show their series' Rotten Tomatoes ratings for context and their own TMDB user score, from the season - * Seerr reports (one request for all episodes of the season). The series' TMDB score is not shown next to it. - * - everything else (collections, music, photos, programs, ...) has none + * Fetches [SourceRatings] for a title from Seerr, the same source the Wolfskin PWA uses * * Movies use `ratingscombined` (RT critics, RT audience, IMDb), series use `tv/{id}/ratings` (RT). TMDB comes from * `voteAverage`/`voteCount` and is only requested when no other source has a score. @@ -65,7 +41,6 @@ class SeerrRatingsService @Inject constructor( private val seerrService: SeerrService, - private val api: ApiClient, ) { private data class Key( val type: SeerrItemType, @@ -79,196 +54,43 @@ class SeerrRatingsService .expireAfterWrite(CACHE_HOURS, TimeUnit.HOURS) .build() - private data class SeasonKey( - val tmdbId: Int, - val season: Int, - ) - - /** - * The TMDB vote of each episode (by episode number) of a series' season - */ - private val seasonCache = - CacheBuilder - .newBuilder() - .maximumSize(CACHE_SIZE) - .expireAfterWrite(CACHE_HOURS, TimeUnit.HOURS) - .build>() - - /** - * The TMDB id of a Jellyfin movie or series, empty if it has none. Items such as an episode on the home page - * don't carry their series' provider ids, so this is how they are found. - */ - private val tmdbIds = - CacheBuilder - .newBuilder() - .maximumSize(CACHE_SIZE) - .expireAfterWrite(CACHE_HOURS, TimeUnit.HOURS) - .build>() - fun cached( type: SeerrItemType, tmdbId: Int, ): SourceRatings? = cache.getIfPresent(Key(type, tmdbId)) - /** - * The Jellyfin item whose provider ids give the TMDB id, null if the item never has ratings. - * An episode's or season's own provider ids are for the episode or season, so those use the series. - */ - private fun ownerId(item: BaseItem): UUID? = - when (item.type) { - BaseItemKind.MOVIE, BaseItemKind.SERIES -> item.id - BaseItemKind.SEASON, BaseItemKind.EPISODE -> item.data.seriesId - else -> null - } - - /** - * The TMDB id if it is known without a request: the item's own provider ids or an earlier lookup. - * Present but empty means the item definitely has no TMDB id, null means unknown. - */ - private fun knownTmdbId( - item: BaseItem, - ownerId: UUID, - ): Optional? = - item.data.providerIds - ?.takeIf { ownerId == item.id && it.isNotEmpty() } - ?.let { Optional.ofNullable(it["Tmdb"]?.toIntOrNull()) } - ?: tmdbIds.getIfPresent(ownerId) - - private fun type(item: BaseItem): SeerrItemType? = - when (item.type) { - BaseItemKind.MOVIE -> SeerrItemType.MOVIE - BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE -> SeerrItemType.TV - else -> null - } - - private fun knownTarget(item: BaseItem): RatingsTarget? { - val type = type(item) ?: return null - val owner = ownerId(item) ?: return null - return knownTmdbId(item, owner)?.orElse(null)?.let { RatingsTarget(type, it) } - } - - /** - * Finds the [RatingsTarget] for an item, asking Jellyfin for the provider ids if they are not at hand. - * Null if the item has no ratings, no TMDB id or the lookup failed. - */ - suspend fun resolveTarget(item: BaseItem): RatingsTarget? { - val type = type(item) ?: return null - val owner = ownerId(item) ?: return null - val tmdb = - knownTmdbId(item, owner) ?: try { - Optional - .ofNullable( - api.userLibraryApi - .getItem(owner) - .content.providerIds - ?.get("Tmdb") - ?.toIntOrNull(), - ).also { tmdbIds.put(owner, it) } - } catch (ex: CancellationException) { - throw ex - } catch (ex: Exception) { - Timber.w(ex, "Error fetching provider ids for %s", owner) - return null + private fun target(item: BaseItem): Pair? { + val type = + when (item.type) { + BaseItemKind.MOVIE -> SeerrItemType.MOVIE + BaseItemKind.SERIES -> SeerrItemType.TV + else -> return null } - return tmdb.orElse(null)?.let { RatingsTarget(type, it) } + val tmdbId = + item.data.providerIds + ?.get("Tmdb") + ?.toIntOrNull() ?: return null + return type to tmdbId } /** - * The already cached ratings for an item, used to seed a screen's state so it doesn't flash empty + * The already cached ratings for a Jellyfin movie or series, used to seed a screen's initial state so it + * doesn't show the Jellyfin values first */ - fun cachedFor(item: BaseItem): SourceRatings? { - val target = knownTarget(item) ?: return null - val series = cached(target.type, target.tmdbId) ?: return null - if (item.type != BaseItemKind.EPISODE) return series - val season = item.data.parentIndexNumber - val episode = item.data.indexNumber - if (season == null || episode == null) return series - val votes = seasonCache.getIfPresent(SeasonKey(target.tmdbId, season)) ?: return null - return series.withEpisode(episodeScore(votes[episode])) - } - - private fun episodeScore(vote: TmdbVote?) = vote?.let { SourceRatings.episodeScore(it.average, it.count) } + fun cachedFor(item: BaseItem): SourceRatings? = target(item)?.let { (type, id) -> cached(type, id) } /** - * Ratings for a Jellyfin item. Follows whether Seerr is active, since the connection is established - * asynchronously after login. + * Ratings for a Jellyfin movie or series using its TMDB id. Follows whether Seerr is active, since the + * connection is established asynchronously after login. * - * Already cached ratings are emitted at once. Otherwise nothing is requested until [debounceMillis] has passed, so - * collecting for each focused item while scrolling quickly only asks Seerr about the item the user stops on: - * collecting is cancelled for the previous item, which cancels the wait or the request in progress. - * - * Emits null while Seerr is not active, the item has no ratings (see above) or no TMDB id, or the lookup failed. + * Emits null while Seerr is not active, the item isn't a movie/series with a TMDB id, or the lookup failed. + * Callers should keep showing the Jellyfin values for null. */ - fun forItem( - item: BaseItem?, - debounceMillis: Long = DEBOUNCE_MILLIS, - ): Flow { - if (item == null || type(item) == null) return flowOf(null) - cachedFor(item)?.let { return flowOf(it) } - val owner = ownerId(item) ?: return flowOf(null) - if (knownTmdbId(item, owner)?.isPresent == false) return flowOf(null) - return flow { - delay(debounceMillis) - emitAll( - seerrService.active - .distinctUntilChanged() - .mapLatest { active -> - if (active) resolveTarget(item)?.let { getFor(item, it) } else null - }, - ) - } - } - - /** - * The ratings for the target of an item: for an episode, those of its series with its own TMDB score - */ - private suspend fun getFor( - item: BaseItem, - target: RatingsTarget, - ): SourceRatings? { - val series = get(target.type, target.tmdbId) - val season = item.data.parentIndexNumber - val episode = item.data.indexNumber - if (item.type != BaseItemKind.EPISODE || season == null || episode == null) return series - val votes = getSeason(target.tmdbId, season) - if (series == null && votes == null) return null - return (series ?: SourceRatings()).withEpisode(episodeScore(votes?.get(episode))) - } - - /** - * The TMDB votes of the episodes of a season, fetched once for all of them. Null if the request failed, which is not cached. - */ - private suspend fun getSeason( - tmdbId: Int, - season: Int, - ): Map? { - val key = SeasonKey(tmdbId, season) - seasonCache.getIfPresent(key)?.let { return it } - val votes = - try { - seerrService.api.tvApi - .tvTvIdSeasonSeasonNumberGet(tvId = tmdbId, seasonNumber = season) - .episodes - .orEmpty() - .mapNotNull { episode -> - episode.episodeNumber?.let { it to TmdbVote(episode.voteAverage?.toDouble(), episode.voteCount) } - }.toMap() - } catch (ex: CancellationException) { - throw ex - } catch (ex: ClientException) { - if (ex.statusCode == 404) { - // TMDB has no such season, that is an answer rather than a failure - emptyMap() - } else { - Timber.w(ex, "Error fetching season %s of %s", season, tmdbId) - return null - } - } catch (ex: Exception) { - Timber.w(ex, "Error fetching season %s of %s", season, tmdbId) - return null - } - seasonCache.put(key, votes) - return votes + fun forItem(item: BaseItem): Flow { + val (type, tmdbId) = target(item) ?: return flowOf(null) + return seerrService.active + .distinctUntilChanged() + .mapLatest { active -> if (active) get(type, tmdbId) else null } } /** @@ -406,6 +228,5 @@ class SeerrRatingsService companion object { private const val CACHE_SIZE = 300L private const val CACHE_HOURS = 6L - const val DEBOUNCE_MILLIS = 350L } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index 0d0eb0b6..90d9526b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -9,24 +9,20 @@ import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.SeerrAuthMethod import com.github.damontecres.wholphin.data.model.SeerrPermission -import com.github.damontecres.wholphin.data.model.SeerrPluginLogin -import com.github.damontecres.wholphin.data.model.SeerrPluginSettings import com.github.damontecres.wholphin.data.model.SeerrServer import com.github.damontecres.wholphin.data.model.SeerrUser import com.github.damontecres.wholphin.data.model.hasPermission import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient -import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import com.github.damontecres.wholphin.util.LoadingState -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import okhttp3.OkHttpClient import org.jellyfin.sdk.model.api.ImageType -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds @@ -43,25 +39,26 @@ class SeerrServerRepository private val serverRepository: ServerRepository, @param:StandardOkHttpClient private val okHttpClient: OkHttpClient, ) { - private val _connection = MutableStateFlow(SeerrState()) - val connection: StateFlow = _connection + private val _connection = + MutableStateFlow(SeerrConnectionStatus.NotConfigured) + val connection: StateFlow = _connection val current: Flow = - _connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current } + _connection.map { (it as? SeerrConnectionStatus.Success)?.current } val currentServer: Flow = - connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current?.server } + connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server } val currentUser: Flow = - connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current?.user } + connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user } val currentUserId: Flow = current.map { it?.config?.id } /** * Whether Seerr integration is currently active of not */ val active: Flow = - connection.map { it.connection is SeerrConnectionStatus.Success && seerrApi.active } + connection.map { it is SeerrConnectionStatus.Success && seerrApi.active } fun clear() { - _connection.update { SeerrState() } + _connection.update { SeerrConnectionStatus.NotConfigured } seerrApi.update("", null) } @@ -70,7 +67,7 @@ class SeerrServerRepository user: SeerrUser, exception: Exception, ) { - _connection.update { it.copy(connection = SeerrConnectionStatus.Error(server, user, exception)) } + _connection.update { SeerrConnectionStatus.Error(server, user, exception) } seerrApi.update("", null) } @@ -81,9 +78,8 @@ class SeerrServerRepository ) { val publicSettings = seerrApi.api.settingsApi.settingsPublicGet() _connection.update { - it.copy( - connection = - SeerrConnectionStatus.Success(CurrentSeerr(server, user, userConfig, publicSettings)), + SeerrConnectionStatus.Success( + CurrentSeerr(server, user, userConfig, publicSettings), ) } } @@ -173,7 +169,7 @@ class SeerrServerRepository suspend fun removeServerForCurrentUser(): Boolean { val user = - when (val conn = connection.value.connection) { + when (val conn = connection.first()) { SeerrConnectionStatus.NotConfigured -> return false is SeerrConnectionStatus.Error -> conn.user is SeerrConnectionStatus.Success -> conn.current.user @@ -182,81 +178,6 @@ class SeerrServerRepository clear() return rows > 0 } - - /** - * Given the [SeerrPluginSettings], try to connect to the Seerr server - * - * @return true if successfully updated the SeerrApi and logged in - */ - suspend fun usePluginProvidedServer(settings: SeerrPluginSettings): Boolean { - val currentUser = serverRepository.currentUser ?: return false - if (settings.serverUrl.isNotNullOrBlank() && settings.login != null && settings.login != SeerrPluginLogin.None) { - val server = - SeerrServer( - id = -1, - url = settings.serverUrl, - ) - val user = - SeerrUser( - jellyfinUserRowId = currentUser.rowId, - serverId = server.id, - authMethod = - when (settings.login) { - is SeerrPluginLogin.ApiKey -> SeerrAuthMethod.API_KEY - is SeerrPluginLogin.Local -> SeerrAuthMethod.LOCAL - }, - username = - when (settings.login) { - is SeerrPluginLogin.ApiKey -> null - is SeerrPluginLogin.Local -> settings.login.local.username - }, - password = null, - credential = null, - ) - try { - val userConfig = - when (settings.login) { - is SeerrPluginLogin.ApiKey -> { - Timber.v("maybeAddPluginServer, using API key") - seerrApi.update(settings.serverUrl, settings.login.apiKey) - seerrLogin(seerrApi.api, SeerrAuthMethod.API_KEY, null, null) - } - - is SeerrPluginLogin.Local -> { - Timber.v("maybeAddPluginServer, using local user") - seerrApi.update(settings.serverUrl, null) - seerrLogin( - seerrApi.api, - SeerrAuthMethod.LOCAL, - settings.login.local.username, - settings.login.local.password, - ) - } - - SeerrPluginLogin.None -> { - return false - } - } - // Success - _connection.update { it.copy(pluginProvided = true) } - set(server, user, userConfig) - return true - } catch (ex: CancellationException) { - throw ex - } catch (ex: Exception) { - clear() - Timber.w(ex, "Error adding plugin provided seerr") - _connection.update { - it.copy( - pluginProvided = true, - connection = SeerrConnectionStatus.Error(server, user, ex), - ) - } - return false - } - } - return false - } } /** @@ -264,11 +185,6 @@ class SeerrServerRepository */ typealias SeerrUserConfig = User -data class SeerrState( - val connection: SeerrConnectionStatus = SeerrConnectionStatus.NotConfigured, - val pluginProvided: Boolean = false, -) - sealed interface SeerrConnectionStatus { data object NotConfigured : SeerrConnectionStatus @@ -289,15 +205,11 @@ data class CurrentSeerr( val config: SeerrUserConfig, val serverConfig: PublicSettings, ) { - val requestMovieEnabled: Boolean - get() = config.hasPermission(SeerrPermission.REQUEST_MOVIE) val request4kMovieEnabled: Boolean get() = (serverConfig.movie4kEnabled ?: false) && config.hasPermission(SeerrPermission.REQUEST_4K_MOVIE) - val requestTvEnabled: Boolean - get() = config.hasPermission(SeerrPermission.REQUEST_TV) val request4kTvEnabled: Boolean get() = (serverConfig.series4kEnabled ?: false) && diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ServerPluginApi.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ServerPluginApi.kt deleted file mode 100644 index 8b51ad63..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ServerPluginApi.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.github.damontecres.wholphin.services - -import com.github.damontecres.wholphin.data.model.HomePageSettings -import com.github.damontecres.wholphin.data.model.SeerrPluginSettings -import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient -import com.github.damontecres.wholphin.util.WholphinDispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.decodeFromStream -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient -import okhttp3.Request -import okio.IOException -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.exception.ApiClientException -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Service for interacting with the companion plugin - */ -@Singleton -class ServerPluginApi - @Inject - constructor( - @param:AuthOkHttpClient private val okHttpClient: OkHttpClient, - private val api: ApiClient, - ) { - private fun createUrl(path: String): String? = - api.baseUrl?.let { if (it.endsWith("/")) "${it}wholphin/$path" else "$it/wholphin/$path" } - - private val json get() = HomeSettingsService.jsonParser - - companion object { - private const val HOME_CONFIG_PATH = "homesettings" - private const val SEERR_CONFIG_PATH = "seerrsettings" - } - - /** - * Check if the plugin is installed on the server - */ - suspend fun checkInstalled(): Boolean = - withContext(WholphinDispatchers.IO) { - val url = createUrl("public") ?: return@withContext false - val request = - Request - .Builder() - .url(url) - .get() - .build() - try { - return@withContext okHttpClient - .newCall(request) - .execute() - .use { it.isSuccessful } - } catch (ex: IOException) { - Timber.w("Could not connect to server plugin: %s", ex.message) - return@withContext false - } - } - - private suspend inline fun executeRequest(path: String): T? { - val url = createUrl(path) ?: return null - val request = Request(url.toHttpUrl()) - return executeRequest(request) - } - - @OptIn(ExperimentalSerializationApi::class) - private suspend inline fun executeRequest(request: Request): T? = - withContext(WholphinDispatchers.IO) { - okHttpClient.newCall(request).execute().use { res -> - if (res.isSuccessful) { - json.decodeFromStream(res.body.byteStream()) - } else if (res.code == 404) { - Timber.w("Plugin 404: %s", request.url.encodedPath) - null - } else { - throw ApiClientException(res.code.toString() + " " + res.body.string()) - } - } - } - - /** - * Get the default home page settings from the plugin - * - * @return the home page settings or null if no rows are configured or the server isn't installed - */ - suspend fun fetchHomePageSettings(): HomePageSettings? = - executeRequest(HOME_CONFIG_PATH)?.takeIf { it.rows.isNotEmpty() } - - /** - * Get the [SeerrPluginSettings] as configured by teh server plugin, or null if none are configured - */ - suspend fun fetchSeerrSettings(): SeerrPluginSettings? = executeRequest(SEERR_CONFIG_PATH) - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt index 0b9da9f8..09450b34 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt @@ -1,27 +1,15 @@ package com.github.damontecres.wholphin.services -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao +import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.ActivationFlag import com.github.damontecres.wholphin.data.model.ItemPlayback -import com.github.damontecres.wholphin.data.model.RelativeMediaStream -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType -import com.github.damontecres.wholphin.data.model.TrackChoiceParentType -import com.github.damontecres.wholphin.data.model.TrackFlag -import com.github.damontecres.wholphin.data.model.TrackFlag.Companion.has +import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice import com.github.damontecres.wholphin.data.model.TrackIndex -import com.github.damontecres.wholphin.data.model.asRelative import com.github.damontecres.wholphin.preferences.SubtitleModePreference import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserProfileSettings -import com.github.damontecres.wholphin.ui.gt import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty -import com.github.damontecres.wholphin.ui.util.ResProviderStringProvider -import com.github.damontecres.wholphin.ui.util.ResStringProvider -import com.github.damontecres.wholphin.ui.util.StringProvider import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaStream @@ -42,69 +30,50 @@ class StreamChoiceService @Inject constructor( private val serverRepository: ServerRepository, - private val seriesTrackChoiceDao: SeriesTrackChoiceDao, + private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao, ) { private val userConfig: UserConfiguration? get() = serverRepository.currentUserDto?.configuration - suspend fun saveSeriesTrackChoice( + suspend fun updateAudio( dto: BaseItemDto, - type: MediaStreamType, - stream: RelativeMediaStream?, - activation: ActivationFlag = ActivationFlag.ACTIVATED, - ) { - val type = - when (stream?.type ?: type) { - MediaStreamType.AUDIO -> SeriesTrackChoiceType.AUDIO - MediaStreamType.SUBTITLE -> SeriesTrackChoiceType.SUBTITLE - else -> return - } - val userId = serverRepository.currentUser!!.rowId - val newStc = - buildList { - dto.parentId - ?.let { seasonId -> - SeriesTrackChoice( - userId = userId, - itemId = dto.id, - parentId = seasonId, - type = type, - parentType = TrackChoiceParentType.SEASON, - stream = stream, - activation = activation, - ) - }?.let(::add) - dto.seriesId - ?.let { seriesId -> - SeriesTrackChoice( - userId = userId, - itemId = dto.id, - parentId = seriesId, - type = type, - parentType = TrackChoiceParentType.SERIES, - stream = stream, - activation = activation, - ) - }?.let(::add) - } - Timber.d( - "Saving series track choices for itemId=%s, seriesId=%s, parentId=%s: %s", - dto.id, - dto.seriesId, - dto.parentId, - newStc, + audioLang: String, + ) = update(dto) { + it.copy( + audioLanguage = audioLang, ) - seriesTrackChoiceDao.save(newStc) } - suspend fun saveDisabledSeriesTrackChoice( + suspend fun updateSubtitles( dto: BaseItemDto, - type: MediaStreamType, - ) = saveSeriesTrackChoice(dto, type, null, ActivationFlag.DISABLED) + subtitleLang: String?, + subtitlesDisabled: Boolean, + ) = update(dto) { + it.copy( + subtitleLanguage = if (subtitlesDisabled) null else subtitleLang, + subtitlesDisabled = subtitlesDisabled, + ) + } - suspend fun saveOnlyForcedSeriesTrackChoice( + suspend fun update( dto: BaseItemDto, - type: MediaStreamType, - ) = saveSeriesTrackChoice(dto, type, null, ActivationFlag.ONLY_FORCED) + update: (PlaybackLanguageChoice) -> PlaybackLanguageChoice, + ) { + val seriesId = dto.seriesId + if (seriesId != null) { + val userId = serverRepository.currentUser!!.rowId + val currentPlc = + playbackLanguageChoiceDao.get(userId, seriesId) + ?: PlaybackLanguageChoice(userId, seriesId, dto.id) + val newPlc = update.invoke(currentPlc) + Timber.v("Saving series PLC: %s", newPlc) + playbackLanguageChoiceDao.save(newPlc) + } + } + + suspend fun getPlaybackLanguageChoice(dto: BaseItemDto) = + dto.seriesId?.let { + playbackLanguageChoiceDao.get(serverRepository.currentUser!!.rowId, it) + } /** * Returns the [MediaSourceInfo] that matched the [ItemPlayback] or else the one with the highest resolution @@ -135,15 +104,21 @@ class StreamChoiceService */ suspend fun chooseAudioStream( source: MediaSourceInfo, - item: BaseItemDto, + seriesId: UUID?, itemPlayback: ItemPlayback?, - stc: List?, + plc: PlaybackLanguageChoice?, prefs: UserPreferences, - ): StreamChoiceResult? { - val stc = stc ?: getSeriesTrackChoices(item, SeriesTrackChoiceType.SUBTITLE) + ): MediaStream? { + val plc = + plc ?: seriesId?.let { + playbackLanguageChoiceDao.get( + serverRepository.currentUser!!.rowId, + it, + ) + } return source.mediaStreams?.letNotEmpty { streams -> val candidates = streams.filter { it.type == MediaStreamType.AUDIO } - chooseAudioStream(candidates, itemPlayback, stc, prefs) + chooseAudioStream(candidates, itemPlayback, plc, prefs) } } @@ -153,70 +128,59 @@ class StreamChoiceService fun chooseAudioStream( candidates: List, itemPlayback: ItemPlayback?, - stc: List, + playbackLanguageChoice: PlaybackLanguageChoice?, prefs: UserPreferences, - ): StreamChoiceResult = + ): MediaStream? = if (itemPlayback?.audioIndexEnabled == true) { - candidates.firstOrNull { it.index == itemPlayback.audioIndex } with - StreamChoiceReason.Item( - itemPlayback, - ) - } else if (stc.isNotEmpty()) { - val first = stc.first() - val result = scoreStreams(candidates, first) - if (result == null) { - null with StreamChoiceReason.from(first) - } else if (result.isEmpty() && stc.size > 1) { - // SeriesTrackChoice did not apply to any streams, but there are more options - chooseAudioStream(candidates, itemPlayback, stc.subList(1, stc.size), prefs) - } else if (result.isEmpty()) { - // SeriesTrackChoice did not apply to any streams, so use regular selection logic - Timber.v("No audio STC applied") - chooseAudioStream(candidates, itemPlayback, emptyList(), prefs) - } else { - // Otherwise, use the best scored stream - Timber.v("Using audio STC from %s", first.parentId) - result.first().stream with StreamChoiceReason.from(first, result) - } + candidates.firstOrNull { it.index == itemPlayback.audioIndex } } else { - val audioLanguage = getPreferredLanguage(MediaStreamType.AUDIO, prefs, userConfig) - val stream = - if (audioLanguage.isNotNullOrBlank()) { - val sorted = - candidates.sortedWith(compareBy { it.language }.thenByDescending { it.channels }) - sorted.firstOrNull { it.language == audioLanguage && it.isDefault } - ?: sorted.firstOrNull { it.language == audioLanguage } - ?: sorted.firstOrNull { it.isDefault } - ?: sorted.firstOrNull() - } else { - candidates.firstOrNull { it.isDefault } - ?: candidates.firstOrNull() - } - stream with StreamChoiceReason.UserPreferences + val seriesLang = + playbackLanguageChoice?.audioLanguage?.takeIf { it.isNotNullOrBlank() } + // If the user has chosen a different language for the series, prefer that + val audioLanguage = + seriesLang ?: getPreferredLanguage(MediaStreamType.AUDIO, prefs, userConfig) + if (audioLanguage.isNotNullOrBlank()) { + val sorted = + candidates.sortedWith(compareBy { it.language }.thenByDescending { it.channels }) + sorted.firstOrNull { it.language == audioLanguage && it.isDefault } + ?: sorted.firstOrNull { it.language == audioLanguage } + ?: sorted.firstOrNull { it.isDefault } + ?: sorted.firstOrNull() + } else { + candidates.firstOrNull { it.isDefault } + ?: candidates.firstOrNull() + } } /** * Returns the subtitle stream that should play */ suspend fun chooseSubtitleStream( - item: BaseItemDto, source: MediaSourceInfo, audioStream: MediaStream?, + seriesId: UUID?, itemPlayback: ItemPlayback?, - stc: List?, + plc: PlaybackLanguageChoice?, prefs: UserPreferences, - ): StreamChoiceResult? = - source.mediaStreams?.letNotEmpty { streams -> + ): MediaStream? { + val plc = + plc ?: seriesId?.let { + playbackLanguageChoiceDao.get( + serverRepository.currentUser!!.rowId, + it, + ) + } + return source.mediaStreams?.letNotEmpty { streams -> val candidates = streams.filter { it.type == MediaStreamType.SUBTITLE } - val stc = stc ?: getSeriesTrackChoices(item, SeriesTrackChoiceType.SUBTITLE) chooseSubtitleStream( audioStream?.language, candidates, itemPlayback, - stc, + plc, prefs, ) } + } /** * Resolves ONLY_FORCED to an actual subtitle track index. @@ -225,7 +189,7 @@ class StreamChoiceService suspend fun resolveSubtitleIndex( source: MediaSourceInfo, audioStreamIndex: Int?, - item: BaseItemDto, + seriesId: UUID?, subtitleIndex: Int, prefs: UserPreferences, ): Int? = @@ -239,17 +203,17 @@ class StreamChoiceService val itemPlayback = ItemPlayback( userId = serverRepository.currentUser!!.rowId, - itemId = item.id, + itemId = UUID.randomUUID(), // Not used for ONLY_FORCED resolution subtitleIndex = TrackIndex.ONLY_FORCED, ) chooseSubtitleStream( - item = item, source = source, audioStream = audioStream, + seriesId = seriesId, itemPlayback = itemPlayback, - stc = null, + plc = null, prefs = prefs, - )?.stream?.index + )?.index } /** @@ -259,81 +223,47 @@ class StreamChoiceService audioStreamLang: String?, candidates: List, itemPlayback: ItemPlayback?, - stc: List, + playbackLanguageChoice: PlaybackLanguageChoice?, prefs: UserPreferences, - ): StreamChoiceResult { - if (itemPlayback != null && itemPlayback.subtitleIndex != TrackIndex.UNSPECIFIED) { - if (itemPlayback.subtitleIndex == TrackIndex.DISABLED) { - return null with StreamChoiceReason.Item(itemPlayback) - } else if (itemPlayback.subtitleIndex == TrackIndex.ONLY_FORCED) { - val subtitleLanguage = - getPreferredLanguage(MediaStreamType.SUBTITLE, prefs, userConfig) - // Client-side manual override: User selected "Only Forced" in player menu - return findForcedTrack( - candidates, - subtitleLanguage, - audioStreamLang, - ) with StreamChoiceReason.Item(itemPlayback) - } else { - return candidates.firstOrNull { it.index == itemPlayback.subtitleIndex } with - StreamChoiceReason.Item(itemPlayback) - } - } else if (stc.isNotEmpty()) { - val first = stc.first() - return when (first.activation) { - ActivationFlag.DISABLED -> { - null with StreamChoiceReason.from(first) - } - - ActivationFlag.ONLY_FORCED -> { - val subtitleLanguage = - getPreferredLanguage(MediaStreamType.SUBTITLE, prefs, userConfig) - val stream = - findForcedTrack( - candidates, - subtitleLanguage, - audioStreamLang, - ) - if (stream != null) { - stream with StreamChoiceReason.from(first) - } else { - handleSubtitleStreamChoiceResult( - result = emptyList(), - audioStreamLang = audioStreamLang, - candidates = candidates, - itemPlayback = itemPlayback, - stc = stc, - prefs = prefs, - ) - } - } - - ActivationFlag.ACTIVATED -> { - val result = - scoreStreams(candidates, first) - ?: throw IllegalStateException("scoreStreams should not return null for activated") - handleSubtitleStreamChoiceResult( - result = result, - audioStreamLang = audioStreamLang, - candidates = candidates, - itemPlayback = itemPlayback, - stc = stc, - prefs = prefs, - ) - } - } + ): MediaStream? { + if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) { + return null + } + val seriesLang = + playbackLanguageChoice?.subtitleLanguage?.takeIf { it.isNotNullOrBlank() } + val subtitleLanguage = + seriesLang ?: getPreferredLanguage(MediaStreamType.SUBTITLE, prefs, userConfig) + if (itemPlayback?.subtitleIndex == TrackIndex.ONLY_FORCED) { + // Client-side manual override: User selected "Only Forced" in player menu + return findForcedTrack(candidates, subtitleLanguage, audioStreamLang) + } else if (itemPlayback?.subtitleIndexEnabled == true) { + return candidates.firstOrNull { it.index == itemPlayback.subtitleIndex } } else { - val subtitleLanguage = - getPreferredLanguage(MediaStreamType.SUBTITLE, prefs, userConfig) val subtitleMode = - when (prefs.userPreferences?.subtitleMode) { - SubtitleModePreference.USE_USER_PROFILE -> userConfig?.subtitleMode - SubtitleModePreference.DEFAULT -> SubtitlePlaybackMode.DEFAULT - SubtitleModePreference.SMART -> SubtitlePlaybackMode.SMART - SubtitleModePreference.ONLY_FORCED -> SubtitlePlaybackMode.ONLY_FORCED - SubtitleModePreference.ALWAYS -> SubtitlePlaybackMode.ALWAYS - SubtitleModePreference.NONE -> SubtitlePlaybackMode.NONE - null -> SubtitlePlaybackMode.DEFAULT + when { + playbackLanguageChoice?.subtitlesDisabled == false && seriesLang != null -> { + // User has chosen a series level subtitle language, so override their normal + // subtitle mode to display that language + SubtitlePlaybackMode.ALWAYS + } + + playbackLanguageChoice?.subtitlesDisabled == true && seriesLang == null -> { + // Series level settings disables subtitles + SubtitlePlaybackMode.NONE + } + + else -> { + // Fallback to the user's preference + when (prefs.userPreferences?.subtitleMode) { + SubtitleModePreference.USE_USER_PROFILE -> userConfig?.subtitleMode + SubtitleModePreference.DEFAULT -> SubtitlePlaybackMode.DEFAULT + SubtitleModePreference.SMART -> SubtitlePlaybackMode.SMART + SubtitleModePreference.ONLY_FORCED -> SubtitlePlaybackMode.ONLY_FORCED + SubtitleModePreference.ALWAYS -> SubtitlePlaybackMode.ALWAYS + SubtitleModePreference.NONE -> SubtitlePlaybackMode.NONE + null -> SubtitlePlaybackMode.DEFAULT + } + } } ?: SubtitlePlaybackMode.DEFAULT val candidates = candidates @@ -347,104 +277,73 @@ class StreamChoiceService }.thenByDescending { it.isForced && it.language.isUnknown } .thenByDescending { it.isForced }, ) - val stream = - when (subtitleMode) { - SubtitlePlaybackMode.ALWAYS -> { - if (subtitleLanguage.isNotNullOrBlank()) { - candidates.firstOrNull { - // Prefer non-forced first - !it.isForced && it.language.equalsLangOrUnknown(subtitleLanguage) - } ?: candidates.firstOrNull { - it.language.equalsLangOrUnknown(subtitleLanguage) - } - } else { - candidates.firstOrNull { !it.isForced } ?: candidates.firstOrNull() + return when (subtitleMode) { + SubtitlePlaybackMode.ALWAYS -> { + if (subtitleLanguage.isNotNullOrBlank()) { + candidates.firstOrNull { + // Prefer non-forced first + !it.isForced && it.language.equalsLangOrUnknown(subtitleLanguage) + } ?: candidates.firstOrNull { + it.language.equalsLangOrUnknown(subtitleLanguage) } - } - - SubtitlePlaybackMode.ONLY_FORCED -> { - if (subtitleLanguage.isNotNullOrBlank()) { - candidates.firstOrNull { it.language == subtitleLanguage && it.isForced } - ?: candidates.firstOrNull { it.language.isUnknown && it.isForced } - } else { - candidates.firstOrNull { it.isForced } - } - } - - SubtitlePlaybackMode.SMART -> { - if (subtitleLanguage.isNotNullOrBlank()) { - val audioLanguage = userConfig?.audioLanguagePreference - if ( - // Has preferred subtitle lang & preferred audio, so only show subtitles if actual audio is different - (audioLanguage.isNotNullOrBlank() && audioLanguage != audioStreamLang) || - // Has preferred subtitle lang, but no preferred audio lang, so show subtitle if subtitle lang is different from actual audio - (audioLanguage.isNullOrBlank() && subtitleLanguage != audioStreamLang) - ) { - candidates.firstOrNull { it.language == subtitleLanguage } - ?: candidates.firstOrNull { it.language.isUnknown } - } else { - // Otherwise, show forced subtitles in preferred lang - candidates.firstOrNull { it.isForced && it.language == subtitleLanguage } - ?: candidates.firstOrNull { it.isForced && it.language.isUnknown } - } - } else { - candidates.firstOrNull { it.isDefault } - } - } - - SubtitlePlaybackMode.DEFAULT -> { - if (subtitleLanguage.isNotNullOrBlank()) { - candidates.firstOrNull { it.language == subtitleLanguage && (it.isDefault || it.isForced) } - ?: candidates.firstOrNull { it.isDefault || it.isForced } - } else { - candidates.firstOrNull { it.isDefault || it.isForced } - } - } - - SubtitlePlaybackMode.NONE -> { - null + } else { + candidates.firstOrNull { !it.isForced } ?: candidates.firstOrNull() } } - return stream with StreamChoiceReason.UserPreferences + + SubtitlePlaybackMode.ONLY_FORCED -> { + if (subtitleLanguage.isNotNullOrBlank()) { + candidates.firstOrNull { it.language == subtitleLanguage && it.isForced } + ?: candidates.firstOrNull { it.language.isUnknown && it.isForced } + } else { + candidates.firstOrNull { it.isForced } + } + } + + SubtitlePlaybackMode.SMART -> { + if (subtitleLanguage.isNotNullOrBlank()) { + val audioLanguage = userConfig?.audioLanguagePreference + if ( + // Has preferred subtitle lang & preferred audio, so only show subtitles if actual audio is different + (audioLanguage.isNotNullOrBlank() && audioLanguage != audioStreamLang) || + // Has preferred subtitle lang, but no preferred audio lang, so show subtitle if subtitle lang is different from actual audio + (audioLanguage.isNullOrBlank() && subtitleLanguage != audioStreamLang) + ) { + candidates.firstOrNull { it.language == subtitleLanguage } + ?: candidates.firstOrNull { it.language.isUnknown } + } else { + // Otherwise, show forced subtitles in preferred lang + candidates.firstOrNull { it.isForced && it.language == subtitleLanguage } + ?: candidates.firstOrNull { it.isForced && it.language.isUnknown } + } + } else { + candidates.firstOrNull { it.isDefault } + } + } + + SubtitlePlaybackMode.DEFAULT -> { + if (subtitleLanguage.isNotNullOrBlank()) { + candidates.firstOrNull { it.language == subtitleLanguage && (it.isDefault || it.isForced) } + ?: candidates.firstOrNull { it.isDefault || it.isForced } + } else { + candidates.firstOrNull { it.isDefault || it.isForced } + } + } + + SubtitlePlaybackMode.NONE -> { + null + } + } } } - /** - * Recursively calls [chooseSubtitleStream] based on [result] and [stc] - */ - private fun handleSubtitleStreamChoiceResult( - result: List, - audioStreamLang: String?, - candidates: List, - itemPlayback: ItemPlayback?, - stc: List, - prefs: UserPreferences, - ): StreamChoiceResult { - val first = stc.first() - return if (result.isEmpty() && stc.size > 1) { - // SeriesTrackChoice did not apply to any streams, but there are more options - chooseSubtitleStream( - audioStreamLang, - candidates, - itemPlayback, - stc.subList(1, stc.size), - prefs, - ) - } else if (result.isEmpty()) { - // SeriesTrackChoice did not apply to any streams, so use regular selection logic - Timber.v("No subtitle STC applied") - chooseSubtitleStream( - audioStreamLang, - candidates, - itemPlayback, - emptyList(), - prefs, - ) - } else { - // Otherwise, use the best scored stream - Timber.v("Using subtitle STC from %s", first.parentId) - result.first().stream with StreamChoiceReason.from(first, result) - } + /** Returns true if the track is forced (via metadata flag or title patterns). */ + private fun isForcedOrSigns(track: MediaStream): Boolean { + if (track.isForced) return true + val title = track.title ?: track.displayTitle ?: return false + return title.contains("forced", ignoreCase = true) || + title.contains("signs", ignoreCase = true) || + title.contains("songs", ignoreCase = true) } /** Finds a forced/signs track: subtitle pref -> audio -> unknown -> null. */ @@ -468,92 +367,19 @@ class StreamChoiceService // 3. Unknown language forced track return candidates.firstOrNull { it.language.isUnknown && isForcedOrSigns(it) } } - - suspend fun getSeriesTrackChoices( - seriesId: UUID?, - seasonId: UUID?, - type: SeriesTrackChoiceType, - ): List { - val userId = serverRepository.currentUser?.rowId ?: return emptyList() - return when { - seriesId != null && seasonId != null -> { - seriesTrackChoiceDao.get( - userId = userId, - seasonId = seasonId, - seriesId = seriesId, - type = type, - ) - } - - seriesId != null -> { - seriesTrackChoiceDao.getBySeriesId( - userId = userId, - seriesId = seriesId, - type = type, - ) - } - - seasonId != null -> { - seriesTrackChoiceDao.getBySeasonId( - userId = userId, - seasonId = seasonId, - type = type, - ) - } - - else -> { - emptyList() - } - } - } - - suspend fun getSeriesTrackChoices( - item: BaseItemDto, - type: SeriesTrackChoiceType, - ): List = getSeriesTrackChoices(item.seriesId, item.parentId, type) - - suspend fun getSeriesTrackChoices( - item: BaseItemDto, - type: MediaStreamType, - ): List = - getSeriesTrackChoices( - seriesId = item.seriesId, - seasonId = item.parentId, - type = - when (type) { - MediaStreamType.AUDIO -> SeriesTrackChoiceType.AUDIO - MediaStreamType.SUBTITLE -> SeriesTrackChoiceType.SUBTITLE - else -> return emptyList() - }, - ) } private val String?.isUnknown: Boolean get() = - this.isNullOrBlank() || + this == null || this.equals("unknown", true) || this.equals("und", true) || this.equals("undetermined", true) || this.equals("mul", true) || this.equals("zxx", true) -private val String?.isNotUnknown: Boolean - get() = !this.isUnknown - private fun String?.equalsLangOrUnknown(lang: String): Boolean = equals(lang, ignoreCase = true) || this.isUnknown -private fun String?.equalsLangExact(lang: String?): Boolean = this.isNotUnknown && lang.isNotUnknown && equals(lang, ignoreCase = true) - -/** Returns true if the track is forced (via metadata flag or title patterns). */ -fun isForcedOrSigns(track: MediaStream): Boolean = track.isForced || isSigns(track) - -fun isSigns(track: MediaStream): Boolean { - val title = track.title ?: track.displayTitle ?: return false - return title.contains("forced", ignoreCase = true) || - title.contains("signs", ignoreCase = true) || - title.contains("songs", ignoreCase = true) -} - /** * Based on the user's preferences, get their preferred language for audio or subtitles * @@ -586,153 +412,3 @@ fun getPreferredLanguage( else -> pref }?.takeIf { it.isNotNullOrBlank() } } - -/** - * Scores streams based the user's explicit [SeriesTrackChoice] - * - * @return sorted list of preferred streams or null if stream should be disabled - */ -fun scoreStreams( - streams: List, - choice: SeriesTrackChoice, -): List? { - if (choice.activation == ActivationFlag.DISABLED) { - return null - } - val streams = - streams.filter { - if ( - (choice.activation == ActivationFlag.ONLY_FORCED || choice.has(TrackFlag.FORCED)) && - !isForcedOrSigns(it) - ) { - // Filter out non-forced tracks if the user only wants forced - return@filter false - } - if (choice.language.isNotUnknown && - !choice.language.equalsLangExact(it.language) - ) { - // Filter out languages that do not match - return@filter false - } - // Filter by type - choice.type.mediaStreamType == it.type - } - val scored = - streams - .asRelative() - .map { s -> - var score = 0 - - // Prefer exact language match over unknown - if (choice.language.equalsLangExact(s.language)) score += 10_000 - - if (s.isForced && choice.has(TrackFlag.FORCED)) { - score += 1_000 - } - if (choice.has(TrackFlag.SIGNS) && isSigns(s.stream)) { - score += 500 - } - if (s.isDefault && choice.has(TrackFlag.DEFAULT)) { - score += 500 - } - - if (s.isHearingImpaired && choice.has(TrackFlag.SDH)) { - score += 500 - } else if (choice.has(TrackFlag.SDH) && s.isForced) { - // If user wants SDH, prefer non-forced "regular" over forced - score -= 10 - } - - if (s.isExternal && choice.has(TrackFlag.EXTERNAL)) { - score += 100 - } - - if (s.relativeIndex == choice.relativeTrackIndex) { - score += 100 - } - if (s.stream.title == choice.title) { - score += 10 - } - - if (choice.audioChannels != null && s.stream.channels == choice.audioChannels) { - score += 100 - } else if (choice.audioChannels != null && s.stream.channels.gt(choice.audioChannels)) { - score += 50 - } - - if (s.isDefault) { - // All else being equal, prefer the default track - score += 1 - } - ScoredMediaStream(score, s.stream) - }.sortedByDescending { it.score } - return scored -} - -data class StreamChoiceResult( - val stream: MediaStream?, - val reason: StreamChoiceReason, -) - -data class ScoredMediaStream( - val score: Int, - val stream: MediaStream, -) - -private infix fun MediaStream?.with(reason: StreamChoiceReason) = StreamChoiceResult(this, reason) - -/** - * Why a particular [MediaStream] was chosen - */ -sealed interface StreamChoiceReason { - val description: StringProvider - val scores: List get() = emptyList() - - data object Unknown : StreamChoiceReason { - override val description: StringProvider = ResStringProvider(R.string.unknown) - } - - data object UserPreferences : StreamChoiceReason { - override val description: StringProvider = - ResStringProvider(R.string.profile_specific_settings) - } - - data class Series( - val stc: SeriesTrackChoice, - override val scores: List, - ) : StreamChoiceReason { - override val description: StringProvider = - ResProviderStringProvider(R.string.chosen_by, SERIES_PROVIDER) - } - - data class Season( - val stc: SeriesTrackChoice, - override val scores: List, - ) : StreamChoiceReason { - override val description: StringProvider = - ResProviderStringProvider(R.string.chosen_by, SEASON_PROVIDER) - } - - data class Item( - val itemPlayback: ItemPlayback, - ) : StreamChoiceReason { - override val description: StringProvider = - ResProviderStringProvider(R.string.chosen_by, USER_PROVIDER) - } - - companion object { - fun from( - stc: SeriesTrackChoice, - scores: List = emptyList(), - ) = when (stc.parentType) { - TrackChoiceParentType.SERIES -> Series(stc, scores) - TrackChoiceParentType.SEASON -> Season(stc, scores) - TrackChoiceParentType.DEPRECATED -> Unknown - TrackChoiceParentType.UNKNOWN -> Unknown - } - - private val USER_PROVIDER: StringProvider by lazy { ResStringProvider(R.string.user) } - private val SERIES_PROVIDER: StringProvider by lazy { ResStringProvider(R.string.series) } - private val SEASON_PROVIDER: StringProvider by lazy { ResStringProvider(R.string.tv_season) } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SuggestionsWorker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SuggestionsWorker.kt index e0dd472f..aa64058d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SuggestionsWorker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SuggestionsWorker.kt @@ -5,7 +5,6 @@ import androidx.datastore.core.DataStore import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences @@ -64,8 +63,12 @@ class SuggestionsWorker } if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) { - val result = serverRepository.restoreLastSession() - if (result !is RestoredSession.Success) { + var currentUser = serverRepository.current.value + if (currentUser == null) { + serverRepository.restoreSession(serverId, userId) + currentUser = serverRepository.current.value + } + if (currentUser == null) { Timber.w("No user found during run") return Result.failure() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt index b208d663..06115982 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.cancellation.CancellationException /** * Listens for JF user switching in the app to also switch other settings like Seerr user/server @@ -37,7 +36,6 @@ class UserSwitchListener private val seerrServerDao: SeerrServerDao, private val seerrApi: SeerrApi, private val homeSettingsService: HomeSettingsService, - private val serverPluginApi: ServerPluginApi, ) { init { context as AppCompatActivity @@ -67,78 +65,49 @@ class UserSwitchListener AppCompatDelegate.setApplicationLocales(localeList) } - // Check if plugin is installed, then for home settings & seerr settings + // Check for home settings launchIO { - val serverPluginInstalled = - try { - serverPluginApi.checkInstalled() - } catch (ex: CancellationException) { - throw ex - } catch (ex: Exception) { - Timber.e(ex, "Error checking for server plugin") - false - } - Timber.i("Server plugin installed: %s", serverPluginInstalled) - serverRepository.serverPluginInstalled.value = serverPluginInstalled - + homeSettingsService.loadCurrentSettings(user.id) + } + if (BuildConfig.DISCOVER_ENABLED) { + // Check for seerr server launchIO { - // Check for home settings - homeSettingsService.loadCurrentSettings(user) - } - - if (BuildConfig.DISCOVER_ENABLED) { - // Check for seerr server - launchIO { - if (serverPluginInstalled) { - serverPluginApi.fetchSeerrSettings()?.let { - Timber.d("Trying seerr auto login") - val result = seerrServerRepository.usePluginProvidedServer(it) - if (result) { - // Successfully used server plugin provided seerr - Timber.i("Seerr auto login succeeded") - return@launchIO + seerrServerDao + .getUsersByJellyfinUser(user.rowId) + .lastOrNull() + ?.let { seerrUser -> + val server = + seerrServerDao.getServer(seerrUser.serverId)?.server + if (server != null) { + Timber.i("Found a seerr user & server") + try { + seerrApi.update(server.url, seerrUser.credential) + val userConfig = + if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { + seerrLogin( + seerrApi.api, + seerrUser.authMethod, + seerrUser.username, + seerrUser.password, + ) + } else { + seerrApi.api.usersApi.authMeGet() + } + seerrServerRepository.set( + server, + seerrUser, + userConfig, + ) + } catch (ex: Exception) { + Timber.w( + ex, + "Error logging into %s", + server.url, + ) + seerrServerRepository.error(server, seerrUser, ex) } } } - Timber.v("Checking for existing configured seerr server") - // Either no server plugin seerr or there was an issue with it - seerrServerDao - .getUsersByJellyfinUser(user.rowId) - .lastOrNull() - ?.let { seerrUser -> - val server = - seerrServerDao.getServer(seerrUser.serverId)?.server - if (server != null) { - Timber.i("Found a seerr user & server") - try { - seerrApi.update(server.url, seerrUser.credential) - val userConfig = - if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { - seerrLogin( - seerrApi.api, - seerrUser.authMethod, - seerrUser.username, - seerrUser.password, - ) - } else { - seerrApi.api.usersApi.authMeGet() - } - seerrServerRepository.set( - server, - seerrUser, - userConfig, - ) - } catch (ex: Exception) { - Timber.w( - ex, - "Error logging into %s", - server.url, - ) - seerrServerRepository.error(server, seerrUser, ex) - } - } - } - } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt index 3a114017..06607cf9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt @@ -4,8 +4,6 @@ import android.content.Context import androidx.work.WorkManager import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.MostRecentServerProvider -import com.github.damontecres.wholphin.data.MostRecentServerSharedPreferences import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.services.SeerrApi import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory @@ -190,12 +188,6 @@ object AppModule { fun seerrApi( @StandardOkHttpClient okHttpClient: OkHttpClient, ) = SeerrApi(okHttpClient) - - @Provides - @Singleton - fun mostRecentServerProvider( - @ApplicationContext context: Context, - ): MostRecentServerProvider = MostRecentServerSharedPreferences(context) } @Module diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt index 78270a9b..29688664 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt @@ -18,7 +18,6 @@ import com.github.damontecres.wholphin.data.PlaybackEffectDao import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao import com.github.damontecres.wholphin.data.RememberedTabDao import com.github.damontecres.wholphin.data.SeerrServerDao -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao import com.github.damontecres.wholphin.data.ServerPreferencesDao import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer @@ -80,10 +79,6 @@ object DatabaseModule { @Singleton fun rememberedTabDao(db: AppDatabase): RememberedTabDao = db.rememberedTabDao() - @Provides - @Singleton - fun seriesTrackChoiceTabDao(db: AppDatabase): SeriesTrackChoiceDao = db.seriesTrackChoiceDao() - @Provides @Singleton fun userPreferencesDataStore( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt index a4d73caa..d3dbf4c7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt @@ -19,7 +19,6 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.github.damontecres.wholphin.MainActivity import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.AppPreference @@ -71,8 +70,12 @@ class TvProviderWorker if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) { // Not active - val result = serverRepository.restoreLastSession() - if (result !is RestoredSession.Success) { + var currentUser = serverRepository.current.value + if (currentUser == null) { + serverRepository.restoreSession(serverId, userId) + currentUser = serverRepository.current.value + } + if (currentUser == null) { Timber.w("No user found during run") return Result.failure() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt index a284ef0b..30521965 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt @@ -8,9 +8,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.WholphinApplication -import com.github.damontecres.wholphin.preferences.AppPreference.Companion.MEGA_BIT -import com.github.damontecres.wholphin.ui.setup.ServerVersionSupported -import org.jellyfin.sdk.model.ServerVersion +import com.github.damontecres.wholphin.data.model.SourceRatings +import com.github.damontecres.wholphin.data.model.appendRatings import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaSegmentType @@ -208,13 +207,6 @@ fun formatBytes( fun formatBitrate(bitrate: Int) = formatBytes(bitrate.toLong(), byteRateSuffixes, 1000) -fun formatMaxBitrate(bitrate: Long) = - if (bitrate < MEGA_BIT) { - "${bitrate / 1024} kbps" - } else { - "${bitrate / MEGA_BIT} Mbps" - } - @get:StringRes val MediaSegmentType.stringRes: Int get() = @@ -241,12 +233,19 @@ val MediaSegmentType.skipStringRes: Int fun AnnotatedString.Builder.dot() = append(" \u2022 ") -fun listToDotString(strings: List): AnnotatedString = +fun listToDotString( + strings: List, + ratings: SourceRatings?, +): AnnotatedString = buildAnnotatedString { strings.forEachIndexed { index, string -> append(string) if (index != strings.lastIndex) dot() } + ratings?.let { + appendRatings(it.criticEntries) + appendRatings(it.communityEntries) + } } @get:StringRes @@ -310,55 +309,3 @@ fun Resources.formatDuration(duration: Duration): String = else -> getString(R.string.duration_seconds, seconds) } } - -fun Resources.serverSupportedMessage( - serverVersionSupported: ServerVersionSupported, - serverVersion: ServerVersion?, -): String? { - val errorMsg = - when (serverVersionSupported) { - ServerVersionSupported.SUPPORTED -> { - null - } - - ServerVersionSupported.NOT_SUPPORTED, - ServerVersionSupported.UNKNOWN, - -> { - getString(R.string.server_version_not_supported) - } - - ServerVersionSupported.NOT_SUPPORTED_IN_FUTURE -> { - getString(R.string.server_version_not_supported_future) - } - } - return errorMsg?.let { - val msg = - if (serverVersion != null) { - "$it: $serverVersion" - } else { - it + ": " + getString(R.string.unknown) - } - msg + "\n" + - when (serverVersionSupported) { - ServerVersionSupported.SUPPORTED -> { - "" - } - - ServerVersionSupported.NOT_SUPPORTED, - ServerVersionSupported.UNKNOWN, - -> { - getString( - R.string.server_version_required, - WholphinApplication.minimumServerVersion.toString(), - ) - } - - ServerVersionSupported.NOT_SUPPORTED_IN_FUTURE -> { - getString( - R.string.server_version_required, - WholphinApplication.newMinimumServerVersion.toString(), - ) - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt index 786db3d5..2e57b407 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt @@ -75,22 +75,12 @@ val SlimItemFields = */ val ItemRowFields = SlimItemFields + ItemFields.PRIMARY_IMAGE_ASPECT_RATIO -/** - * [ItemFields] for the home page's rows. [ItemFields.PROVIDER_IDS] is needed so a focused movie or series has its TMDB id - * for [com.github.damontecres.wholphin.services.SeerrRatingsService] without another request. - */ val HomeItemFields = listOf( ItemFields.OVERVIEW, ItemFields.CAN_DELETE, - ItemFields.PROVIDER_IDS, ) -/** - * [SlimItemFields] for the home page's continue watching & next up rows, see [HomeItemFields] - */ -val HomeSlimItemFields = SlimItemFields + ItemFields.PROVIDER_IDS - val PhotoItemFields = DetailItemFields + listOf( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/WatchedIcon.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/WatchedIcon.kt index ce8952a7..17649be9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/WatchedIcon.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/WatchedIcon.kt @@ -41,6 +41,7 @@ fun WatchedIconBackground(): Color = when (LocalTheme.current) { AppThemeColors.UNRECOGNIZED, AppThemeColors.PURPLE, + AppThemeColors.WOLFSKIN, AppThemeColors.BLUE, AppThemeColors.GREEN, AppThemeColors.ORANGE, @@ -55,6 +56,8 @@ fun WatchedIconBackground(): Color = @Composable fun WatchedIconColor(): Color = when (LocalTheme.current) { + AppThemeColors.WOLFSKIN -> MaterialTheme.colorScheme.onPrimary + AppThemeColors.UNRECOGNIZED, AppThemeColors.PURPLE, AppThemeColors.BLUE, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index af67f12c..1d70553e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -44,6 +44,7 @@ import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.main.HomePageHeader +import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType @@ -52,18 +53,19 @@ import kotlin.time.Duration @OptIn(ExperimentalFoundationApi::class) @Composable -fun CollectionFolderViewScope.CollectionFolderGrid( +fun CollectionFolderGrid( preferences: UserPreferences, + collectionType: CollectionType?, focusedItem: BaseItem?, items: List, sortAndDirection: SortAndDirection, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, letterPosition: suspend (Char) -> Int, + viewOptions: ViewOptions, onClickPlay: (Int, BaseItem) -> Unit, initialPosition: Int, gridFocusRequester: FocusRequester, - cardContent: @Composable CollectionFolderViewScope.(GridItemDetails) -> Unit, modifier: Modifier = Modifier, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { @@ -97,8 +99,18 @@ fun CollectionFolderViewScope.CollectionFolderGrid( modifier = Modifier.fillMaxSize(), initialPosition = initialPosition, positionCallback = positionCallback, - cardContent = { details -> - cardContent.invoke(this@CollectionFolderGrid, details) + cardContent = { (item, index, onClick, onLongClick, widthPx, mod) -> + GridCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + imageContentScale = viewOptions.contentScale.scale, + imageAspectRatio = viewOptions.aspectRatio.ratio, + imageType = viewOptions.imageType, + showTitle = viewOptions.showTitles, + fillWidth = widthPx, + modifier = mod, + ) }, columns = viewOptions.columns, spacing = viewOptions.spacing.dp, @@ -198,10 +210,10 @@ val CollectionType.baseItemKinds: List listOf(BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE) } - CollectionType.PHOTOS, - CollectionType.HOMEVIDEOS, - -> { - listOf(BaseItemKind.VIDEO, BaseItemKind.PHOTO, BaseItemKind.PHOTO_ALBUM) + CollectionType.HOMEVIDEOS -> { + listOf(BaseItemKind.VIDEO) + listOf(BaseItemKind.PHOTO) + listOf(BaseItemKind.PHOTO_ALBUM) } CollectionType.MUSIC -> { @@ -224,6 +236,10 @@ val CollectionType.baseItemKinds: List listOf(BaseItemKind.PLAYLIST) } + CollectionType.PHOTOS -> { + listOf(BaseItemKind.PHOTO, BaseItemKind.PHOTO_ALBUM) + } + CollectionType.LIVETV -> { listOf( BaseItemKind.TV_CHANNEL, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt index 57ff8957..a048ed94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -60,7 +59,6 @@ import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator import com.github.damontecres.wholphin.ui.cards.WatchedIcon import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.AlphabetButtons -import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.ifElse @@ -77,7 +75,7 @@ import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.extensions.ticks @Composable -fun CollectionFolderViewScope.CollectionFolderList( +fun CollectionFolderList( preferences: UserPreferences, collectionType: CollectionType?, focusedItem: BaseItem?, @@ -86,10 +84,10 @@ fun CollectionFolderViewScope.CollectionFolderList( onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, letterPosition: suspend (Char) -> Int, + viewOptions: ViewOptions, onClickPlay: (Int, BaseItem) -> Unit, initialPosition: Int, gridFocusRequester: FocusRequester, - itemContent: @Composable CollectionFolderViewScope.(GridItemDetails) -> Unit, modifier: Modifier = Modifier, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { @@ -155,8 +153,10 @@ fun CollectionFolderViewScope.CollectionFolderList( ).padding(8.dp), ) } - this@CollectionFolderList.CollectionFolderListContent( + CollectionFolderContent( items = items, + viewOptions = viewOptions, + collectionType = collectionType, listState = listState, position = position, positionFocusRequester = positionFocusRequester, @@ -167,7 +167,6 @@ fun CollectionFolderViewScope.CollectionFolderList( }, onClickItem = onClickItem, onLongClickItem = onLongClickItem, - itemContent = itemContent, modifier = Modifier .weight(1f) @@ -251,7 +250,7 @@ fun CollectionFolderListDetails( ) item?.let { - QuickDetails(item, null) + QuickDetails(item.ui.quickDetails, null) } OverviewText( @@ -267,43 +266,44 @@ fun CollectionFolderListDetails( } @Composable -fun CollectionFolderViewScope.CollectionFolderListContent( +fun CollectionFolderContent( items: List, + viewOptions: ViewOptions, + collectionType: CollectionType?, listState: LazyListState, position: Int, positionFocusRequester: FocusRequester, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, onPositionChange: (Int) -> Unit, - itemContent: @Composable CollectionFolderViewScope.(GridItemDetails) -> Unit, modifier: Modifier = Modifier, ) { - val currentPosition by rememberUpdatedState(position) LazyColumn( state = listState, verticalArrangement = Arrangement.spacedBy(viewOptions.spacing.dp), modifier = modifier.focusRestorer(positionFocusRequester), ) { itemsIndexed(items) { index, item -> - val details = - remember(item, index, onClickItem, onLongClickItem, onPositionChange) { - GridItemDetails( - item = item, - index = index, - onClick = { item?.let { onClickItem.invoke(index, item) } }, - onLongClick = { item?.let { onLongClickItem.invoke(index, item) } }, - widthPx = -1, - mod = - Modifier - .onFocusChanged { - if (it.isFocused) onPositionChange.invoke(index) - }.ifElse( - index == currentPosition, - Modifier.focusRequester(positionFocusRequester), - ), - ) + val onClick: () -> Unit = + remember(index, item) { + { item?.let { onClickItem.invoke(index, item) } } } - itemContent.invoke(this@CollectionFolderListContent, details) + val onLongClick: () -> Unit = + remember(index, item) { + { item?.let { onLongClickItem.invoke(index, item) } } + } + CollectionFolderListItem( + item = item, + dense = viewOptions.type == ViewOptionsType.DENSE_LIST, + collectionType = collectionType, + onClick = onClick, + onLongClick = onLongClick, + modifier = + Modifier + .onFocusChanged { + if (it.isFocused) onPositionChange.invoke(index) + }.ifElse(index == position, Modifier.focusRequester(positionFocusRequester)), + ) } } } @@ -522,12 +522,9 @@ fun ListItemAlbum( } val supportingContent = remember(item, dense) { - // Album artist is already shown as the overline - val subtitle = - if (item?.type == BaseItemKind.MUSIC_ALBUM) item.data.productionYear?.toString() else item?.subtitle - if (!dense && subtitle != null) { + if (!dense && item?.subtitle != null) { @Composable { - Text(subtitle) + Text(item.subtitle ?: "") } } else { null diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt index 8445dfa9..52918e57 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt @@ -54,25 +54,20 @@ import com.github.damontecres.wholphin.services.ThemeSongPlayer import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.deleteItem import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.data.SortAndDirection -import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.detail.music.addToQueue import com.github.damontecres.wholphin.ui.equalsNotNull import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.ui.util.goToRandom import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.GetAlbumArtistsHandler import com.github.damontecres.wholphin.util.GetArtistsHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetPersonsHandler @@ -94,14 +89,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.libraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder -import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest import org.jellyfin.sdk.model.api.request.GetArtistsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetPersonsRequest @@ -153,9 +146,6 @@ class CollectionFolderViewModel private val _state = MutableStateFlow(CollectionFolderState(viewOptions = defaultViewOptions)) val state: StateFlow = _state - private val artistSource: ArtistSource - get() = state.value.viewOptions.artistSource - var position: Int get() = savedStateHandle.get("position") ?: 0 set(value) { @@ -178,25 +168,15 @@ class CollectionFolderViewModel val id = collectionFilter.libraryDisplayInfoIdOverride ?: itemId libraryDisplayInfoDao.getItem(user, id) } - val ancestorDisplayInfo = - if (libraryDisplayInfo?.viewOptions == null && item != null) { - getAncestorDisplayInfo(item) - } else { - null - } _state.update { it.copy( - viewOptions = - libraryDisplayInfo?.viewOptions - ?: ancestorDisplayInfo?.viewOptions - ?: defaultViewOptions, + viewOptions = libraryDisplayInfo?.viewOptions ?: defaultViewOptions, ) } val sortAndDirection = if (collectionFilter.useSavedLibraryDisplayInfo) { libraryDisplayInfo?.sortAndDirection - ?: ancestorDisplayInfo?.sortAndDirection } else { null } ?: initialSortAndDirection ?: SortAndDirection.DEFAULT @@ -252,25 +232,6 @@ class CollectionFolderViewModel } } - /** - * For a sub-folder, returns the saved display info of the closest ancestor that has one - */ - private suspend fun getAncestorDisplayInfo(item: BaseItem): LibraryDisplayInfo? { - if (item.type != BaseItemKind.FOLDER || collectionFilter.libraryDisplayInfoIdOverride != null) { - return null - } - val user = serverRepository.currentUser ?: return null - return try { - api.libraryApi - .getAncestors(item.id, user.id) - .content - .firstNotNullOfOrNull { ancestor -> libraryDisplayInfoDao.getItem(user, ancestor.id) } - } catch (ex: Exception) { - Timber.w(ex, "Error getting ancestors for %s", item.id) - null - } - } - private fun saveLibraryDisplayInfo( newFilter: GetItemsFilter = state.value.filter, newSort: SortAndDirection = state.value.sortAndDirection, @@ -296,17 +257,7 @@ class CollectionFolderViewModel override fun saveViewOptions(viewOptions: ViewOptions) { position = 0 - val artistSourceChanged = viewOptions.artistSource != artistSource _state.update { it.copy(viewOptions = viewOptions) } - if (artistSourceChanged && state.value.filter.override == GetItemsFilterOverride.ARTIST) { - loadResults( - true, - state.value.sortAndDirection, - recursive, - state.value.filter, - useSeriesForPrimary, - ) - } viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) { saveLibraryDisplayInfo(viewOptions = viewOptions) if (!viewOptions.showBackdrop) { @@ -318,19 +269,11 @@ class CollectionFolderViewModel override fun onClickRandom() { viewModelScope.launchIO { try { - val state = state.value - val list = state.items.successValue as? BlockingList - if (list != null) { - goToRandom( - navigateTo = ::navigateTo, - parentId = itemId, - filter = state.filter, - sortAndDirection = state.sortAndDirection, - recursive = recursive, - list = list, - ) - } else { - Timber.w("state.value.items is not successful: %s", state.items) + val random = + (state.value.items.successValue as? BlockingList)?.randomBlocking() + Timber.d("Got random item: %s", random?.id) + random?.destination()?.let { + navigateTo(random.destination()) } } catch (ex: CancellationException) { throw ex @@ -456,27 +399,13 @@ class CollectionFolderViewModel } GetItemsFilterOverride.ARTIST -> { - when (artistSource) { - ArtistSource.ARTIST -> { - ApiRequestPager( - api, - createGetArtistsRequest(filter), - GetArtistsHandler, - viewModelScope, - useSeriesForPrimary = useSeriesForPrimary, - ) - } - - ArtistSource.ALBUM_ARTIST -> { - ApiRequestPager( - api, - createGetAlbumArtistsRequest(filter), - GetAlbumArtistsHandler, - viewModelScope, - useSeriesForPrimary = useSeriesForPrimary, - ) - } - } + ApiRequestPager( + api, + createGetArtistsRequest(filter), + GetArtistsHandler, + viewModelScope, + useSeriesForPrimary = useSeriesForPrimary, + ) } } @@ -495,17 +424,6 @@ class CollectionFolderViewModel ) } - private fun createGetAlbumArtistsRequest(filter: GetItemsFilter): GetAlbumArtistsRequest { - val item = state.value.item.successValue - return filter.applyTo( - GetAlbumArtistsRequest( - parentId = item?.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), - fields = SlimItemFields, - ), - ) - } - private fun createGetItemsRequest( sortAndDirection: SortAndDirection, recursive: Boolean, @@ -577,37 +495,18 @@ class CollectionFolderViewModel val filter = state.value.filter when (filter.override) { GetItemsFilterOverride.ARTIST -> { - when (artistSource) { - ArtistSource.ARTIST -> { - GetArtistsHandler.countMatching( - api = api, - request = - createGetArtistsRequest(filter).copy( - enableImageTypes = null, - fields = null, - nameLessThan = letter.toString(), - limit = 0, - enableTotalRecordCount = true, - enableUserData = false, - ), - ) - } - - ArtistSource.ALBUM_ARTIST -> { - GetAlbumArtistsHandler.countMatching( - api = api, - request = - createGetAlbumArtistsRequest(filter).copy( - enableImageTypes = null, - fields = null, - nameLessThan = letter.toString(), - limit = 0, - enableTotalRecordCount = true, - enableUserData = false, - ), - ) - } - } + GetArtistsHandler.countMatching( + api = api, + request = + createGetArtistsRequest(filter).copy( + enableImageTypes = null, + fields = null, + nameLessThan = letter.toString(), + limit = 0, + enableTotalRecordCount = true, + enableUserData = false, + ), + ) } // GetPersonsRequest has no nameLessThan or startIndex, so /Persons cannot be @@ -914,33 +813,6 @@ fun CollectionFolderViewContent( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, filterOptions: List> = DefaultFilterOptions, focusRequesterOnEmpty: FocusRequester? = null, - gridCardContent: @Composable CollectionFolderViewScope.( - GridItemDetails, - ) -> Unit = { (item, index, onClick, onLongClick, widthPx, mod) -> - GridCard( - item = item, - onClick = onClick, - onLongClick = onLongClick, - imageContentScale = viewOptions.contentScale.scale, - imageAspectRatio = viewOptions.aspectRatio.ratio, - imageType = viewOptions.imageType, - showTitle = viewOptions.showTitles, - fillWidth = widthPx, - modifier = mod, - ) - }, - listItemContent: @Composable CollectionFolderViewScope.( - GridItemDetails, - ) -> Unit = { (item, index, onClick, onLongClick, _, mod) -> - CollectionFolderListItem( - item = item, - dense = viewOptions.type == ViewOptionsType.DENSE_LIST, - collectionType = collectionType, - onClick = onClick, - onLongClick = onLongClick, - modifier = mod, - ) - }, ) { var position by rememberInt(savedPosition) @@ -1103,41 +975,9 @@ fun CollectionFolderViewContent( focusRequester?.tryRequestFocus() clickedRandom = false } - val scope = - object : CollectionFolderViewScope { - override val viewOptions: ViewOptions - get() = state.viewOptions - override val collectionType: CollectionType? - get() = item?.data?.collectionType - override val sortAndDirection: SortAndDirection - get() = state.sortAndDirection - override val collectionSize: Int - get() = pager.data.size - } - Box(Modifier.fillMaxSize()) { if (state.viewOptions.type == ViewOptionsType.GRID) { - scope.CollectionFolderGrid( - preferences = preferences, - initialPosition = savedPosition, - items = pager.data, - sortAndDirection = state.sortAndDirection, - modifier = Modifier.fillMaxSize(), - gridFocusRequester = gridFocusRequester, - onClickItem = gridActions.onClickItem, - onLongClickItem = gridActions.onLongClickItem!!, - positionCallback = { columns, pos -> - showHeader = pos < columns - position = pos - positionCallback?.invoke(columns, pos) - }, - letterPosition = { viewActions.positionOfLetter(it) ?: -1 }, - onClickPlay = gridActions.onClickPlayRemoteButton!!, - focusedItem = focusedItem, - cardContent = gridCardContent, - ) - } else { - scope.CollectionFolderList( + CollectionFolderGrid( preferences = preferences, collectionType = item?.data?.collectionType, initialPosition = savedPosition, @@ -1153,9 +993,30 @@ fun CollectionFolderViewContent( positionCallback?.invoke(columns, pos) }, letterPosition = { viewActions.positionOfLetter(it) ?: -1 }, + viewOptions = state.viewOptions, + onClickPlay = gridActions.onClickPlayRemoteButton!!, + focusedItem = focusedItem, + ) + } else { + CollectionFolderList( + preferences = preferences, + collectionType = item?.data?.collectionType, + initialPosition = savedPosition, + items = pager.data, + sortAndDirection = state.sortAndDirection, + modifier = Modifier.fillMaxSize(), + gridFocusRequester = gridFocusRequester, + onClickItem = gridActions.onClickItem, + onLongClickItem = gridActions.onLongClickItem!!, + positionCallback = { columns, pos -> + showHeader = pos < columns + position = pos + positionCallback?.invoke(columns, pos) + }, + letterPosition = { viewActions.positionOfLetter(it) ?: -1 }, + viewOptions = state.viewOptions, onClickPlay = gridActions.onClickPlayRemoteButton!!, focusedItem = focusedItem, - itemContent = listItemContent, ) } androidx.compose.animation.AnimatedVisibility( @@ -1186,7 +1047,6 @@ fun CollectionFolderViewContent( ViewOptionsDialog( viewOptions = state.viewOptions, defaultViewOptions = defaultViewOptions, - showArtistSource = state.filter.override == GetItemsFilterOverride.ARTIST, onDismissRequest = { showViewOptions = false viewActions.saveViewOptions(state.viewOptions) @@ -1195,13 +1055,3 @@ fun CollectionFolderViewContent( ) } } - -/** - * Scope with global data for [CollectionFolderView], [CollectionFolderGrid], [CollectionFolderList], and individual items - */ -interface CollectionFolderViewScope { - val viewOptions: ViewOptions - val collectionType: CollectionType? - val sortAndDirection: SortAndDirection - val collectionSize: Int -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt index f87010d9..10a41af3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt @@ -209,12 +209,7 @@ fun ContextMenu( favorite = item.favorite, seriesId = item.data.seriesId, sourceId = chosenStreams?.source?.id?.toUUIDOrNull(), - canClearChosenStreams = - chosenStreams.let { - it?.itemPlayback != null || - it?.audioStc?.isNotEmpty() == true || - it?.subtitleStc?.isNotEmpty() == true - }, + canClearChosenStreams = chosenStreams.let { it?.itemPlayback != null || it?.plc != null }, contextMenu = contextMenu, actions = actions, onChooseVersion = { @@ -240,11 +235,11 @@ fun ContextMenu( resources = resources, streams = source.mediaStreams.orEmpty(), type = type, - streamChoice = + currentIndex = if (type == MediaStreamType.AUDIO) { - chosenStreams?.audioStream + chosenStreams?.audioStream?.index } else { - chosenStreams?.subtitleStream + chosenStreams?.subtitleStream?.index }, onClick = { trackIndex -> actions.onChooseTracks.invoke( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 76b7dc77..d7bd9951 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -64,7 +64,6 @@ import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling -import com.github.damontecres.wholphin.services.StreamChoiceResult import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.formatBitrate import com.github.damontecres.wholphin.ui.isNotNullOrBlank @@ -231,35 +230,28 @@ fun DialogPopup( onDismissRequest = onDismissRequest, properties = properties, ) { - val elevatedContainerColor = - MaterialTheme.colorScheme.surfaceColorAtElevation(elevation) DialogPopupContent( title = title, dialogItems = dialogItems, waiting = waiting, onDismissRequest = onDismissRequest, dismissOnClick = dismissOnClick, + elevation = elevation, modifier = - Modifier - .onKeyEvent { event -> - val code = event.nativeKeyEvent.keyCode - if (event.nativeKeyEvent.action == KeyEvent.ACTION_UP && - code in - setOf( - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_NUMPAD_ENTER, - ) - ) { - waiting = false - } - false - }.shadow(elevation = elevation, shape = RoundedCornerShape(28.0.dp)) - .graphicsLayer { - this.clip = true - this.shape = RoundedCornerShape(28.0.dp) - }.drawBehind { drawRect(color = elevatedContainerColor) } - .padding(PaddingValues(24.dp)), + Modifier.onKeyEvent { event -> + val code = event.nativeKeyEvent.keyCode + if (event.nativeKeyEvent.action == KeyEvent.ACTION_UP && + code in + setOf( + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER, + ) + ) { + waiting = false + } + false + }, ) } } @@ -273,10 +265,20 @@ fun DialogPopupContent( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, dismissOnClick: Boolean = true, + elevation: Dp = 8.dp, ) { + val elevatedContainerColor = + MaterialTheme.colorScheme.surfaceColorAtElevation(elevation) Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier, + modifier = + modifier + .shadow(elevation = elevation, shape = RoundedCornerShape(28.0.dp)) + .graphicsLayer { + this.clip = true + this.shape = RoundedCornerShape(28.0.dp) + }.drawBehind { drawRect(color = elevatedContainerColor) } + .padding(PaddingValues(24.dp)), ) { Text( text = title, @@ -450,14 +452,12 @@ fun ConfirmDialog( properties: DialogProperties = DialogProperties(), elevation: Dp = 1.dp, bodyColor: Color = MaterialTheme.colorScheme.onSurface, - @StringRes cancelStringRes: Int = R.string.cancel, - @StringRes confirmStringRes: Int = R.string.confirm, ) = BasicDialog( onDismissRequest = onCancel, properties = properties, elevation = elevation, content = { - ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor, cancelStringRes, confirmStringRes) + ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor) }, ) @@ -472,8 +472,6 @@ fun ConfirmDialogContent( onConfirm: () -> Unit, modifier: Modifier = Modifier, bodyColor: Color = MaterialTheme.colorScheme.onSurface, - @StringRes cancelStringRes: Int = R.string.cancel, - @StringRes confirmStringRes: Int = R.string.confirm, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -504,11 +502,11 @@ fun ConfirmDialogContent( modifier = Modifier.fillMaxWidth(), ) { TextButton( - stringRes = cancelStringRes, + stringRes = R.string.cancel, onClick = onCancel, ) TextButton( - stringRes = confirmStringRes, + stringRes = R.string.confirm, onClick = onConfirm, ) } @@ -653,13 +651,12 @@ fun resourceFor(type: MediaStreamType): Int = fun chooseStream( resources: Resources, streams: List, - streamChoice: StreamChoiceResult?, + currentIndex: Int?, type: MediaStreamType, preferredSubtitleLanguage: String?, onClick: (Int) -> Unit, ): DialogParams { val filteredStreams = streams.filter { it.type == type } - val currentIndex = streamChoice?.stream?.index return DialogParams( fromLongClick = false, title = resources.getString(R.string.choose_stream, resources.getString(resourceFor(type))), @@ -677,16 +674,6 @@ fun chooseStream( }, supportingContent = { }, - trailingContent = - if (currentIndex == null && streamChoice != null) { - { - Text( - text = streamChoice.reason.description.getString(), - ) - } - } else { - null - }, onClick = { onClick.invoke(TrackIndex.DISABLED) }, ), ) @@ -732,16 +719,6 @@ fun chooseStream( supportingContent = { if (simpleStream.streamTitle != null) Text(text = simpleStream.displayTitle) }, - trailingContent = - if (currentIndex == stream.index) { - { - Text( - text = streamChoice.reason.description.getString(), - ) - } - } else { - null - }, onClick = { onClick.invoke(stream.index) }, ) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt index 902ce814..d4228be0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt @@ -1,24 +1,41 @@ package com.github.damontecres.wholphin.ui.components +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star import androidx.compose.runtime.Composable import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.INLINE_IMDB +import com.github.damontecres.wholphin.data.model.INLINE_RT_FRESH +import com.github.damontecres.wholphin.data.model.INLINE_RT_ROTTEN +import com.github.damontecres.wholphin.data.model.INLINE_RT_SPILLED +import com.github.damontecres.wholphin.data.model.INLINE_RT_UNKNOWN +import com.github.damontecres.wholphin.data.model.INLINE_RT_UPRIGHT +import com.github.damontecres.wholphin.data.model.INLINE_TMDB import com.github.damontecres.wholphin.data.model.QuickDetailsData -import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.preferences.DisplayToggle -import com.github.damontecres.wholphin.services.SeerrRatingsService import com.github.damontecres.wholphin.ui.dot import com.github.damontecres.wholphin.ui.formatTime import com.github.damontecres.wholphin.ui.util.LocalClock @@ -26,12 +43,6 @@ import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization import org.jellyfin.sdk.model.DateTime import kotlin.time.Duration -/** - * The line of details under a title: year, runtime, etc, then the ratings, then when it ends. - * - * Ratings are only drawn for [ratings], never from the Jellyfin item. Pass the result of [rememberSourceRatings] or null - * for surfaces without ratings. - */ @Composable fun QuickDetails( details: QuickDetailsData?, @@ -39,16 +50,24 @@ fun QuickDetails( modifier: Modifier = Modifier, textStyle: TextStyle = MaterialTheme.typography.titleSmall, endsAt: DateTime? = null, - ratings: SourceRatings? = null, ) { val enabled = LocalInterfaceCustomization.current.enabledDisplayToggles + val inlineContentMap = rememberQuickDetailsContentMap(textStyle) Row(modifier = modifier) { if (details != null) { - QuickDetailsText(details.basic, Modifier, textStyle) + QuickDetailsText(details.basic, Modifier, textStyle, inlineContentMap) if (DisplayToggle.OFFICIAL_RATING in enabled) { - QuickDetailsText(details.officialRating, Modifier, textStyle) + QuickDetailsText(details.officialRating, Modifier, textStyle, inlineContentMap) + } + val community = DisplayToggle.COMMUNITY_RATING in enabled + val critic = DisplayToggle.CRITIC_RATING in enabled + if (details.criticRatingFirst) { + if (critic) QuickDetailsText(details.criticRating, Modifier, textStyle, inlineContentMap) + if (community) QuickDetailsText(details.communityRating, Modifier, textStyle, inlineContentMap) + } else { + if (community) QuickDetailsText(details.communityRating, Modifier, textStyle, inlineContentMap) + if (critic) QuickDetailsText(details.criticRating, Modifier, textStyle, inlineContentMap) } - SourceRatingsRow(ratings, Modifier, textStyle) } if (timeRemaining != null) { TimeRemaining(timeRemaining, textStyle = textStyle) @@ -58,41 +77,64 @@ fun QuickDetails( } } -/** - * [QuickDetails] for a Jellyfin item with the ratings the PWA shows for it, if any (see [SeerrRatingsService]). - */ -@Composable -fun QuickDetails( - item: BaseItem, - timeRemaining: Duration?, - modifier: Modifier = Modifier, - textStyle: TextStyle = MaterialTheme.typography.titleSmall, - endsAt: DateTime? = null, -) = QuickDetails( - details = item.ui.quickDetails, - timeRemaining = timeRemaining, - modifier = modifier, - textStyle = textStyle, - endsAt = endsAt, - ratings = rememberSourceRatings(item), -) - @NonRestartableComposable @Composable fun QuickDetailsText( str: AnnotatedString?, modifier: Modifier = Modifier, textStyle: TextStyle = MaterialTheme.typography.titleSmall, + inlineContentMap: Map = rememberQuickDetailsContentMap(textStyle), ) = str?.let { Text( text = str, color = MaterialTheme.colorScheme.onSurface, style = textStyle, + inlineContent = inlineContentMap, maxLines = 1, modifier = modifier, ) } +@Composable +fun rememberQuickDetailsContentMap(textStyle: TextStyle = MaterialTheme.typography.titleSmall) = + remember(textStyle) { + val square = Placeholder(textStyle.fontSize, textStyle.fontSize, PlaceholderVerticalAlign.TextCenter) + // Wordmark badges are wider than tall, like in the PWA + val wide = Placeholder(textStyle.fontSize * 1.8f, textStyle.fontSize, PlaceholderVerticalAlign.TextCenter) + fun badge( + placeholder: Placeholder, + @DrawableRes resId: Int, + muted: Boolean = false, + ) = InlineTextContent(placeholder) { + Image( + painter = painterResource(resId), + contentDescription = null, + contentScale = ContentScale.Fit, + alpha = if (muted) 0.55f else 1f, + colorFilter = if (muted) ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }) else null, + modifier = Modifier.fillMaxSize(), + ) + } + mapOf( + "star" to + InlineTextContent(square) { + Icon( + imageVector = Icons.Filled.Star, + tint = FilledStarColor, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + ) + }, + INLINE_RT_ROTTEN to badge(square, R.drawable.ic_rotten_tomatoes_rotten), + INLINE_RT_FRESH to badge(square, R.drawable.ic_rotten_tomatoes_fresh), + INLINE_RT_UPRIGHT to badge(square, R.drawable.ic_rt_upright), + INLINE_RT_SPILLED to badge(square, R.drawable.ic_rt_spilled), + INLINE_RT_UNKNOWN to badge(square, R.drawable.ic_rotten_tomatoes_fresh, muted = true), + INLINE_IMDB to badge(wide, R.drawable.ic_rating_imdb), + INLINE_TMDB to badge(wide, R.drawable.ic_rating_tmdb), + ) + } + @Composable fun TimeRemaining( remaining: Duration, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt index 460eda05..c52fb52f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt @@ -1,29 +1,93 @@ package com.github.damontecres.wholphin.ui.components +import androidx.compose.foundation.background +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Star import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.tv.material3.Icon +import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalTextStyle +import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.playOnClickSound +import com.github.damontecres.wholphin.ui.playSoundOnFocus +import com.github.damontecres.wholphin.ui.theme.WholphinTheme +import com.github.damontecres.wholphin.ui.tryRequestFocus +import java.util.Locale + +enum class StarRatingPrecision { + FULL, + HALF, + QUARTER, + ; + + companion object { + fun fromFloat(value: Float): StarRatingPrecision = + if (value <= .25f) { + QUARTER + } else if (value <= .5f) { + HALF + } else { + FULL + } + } +} val FilledStarColor = Color(0xFFFFC700) val EmptyStarColor = Color(0xFF5D4A0A) -/** - * A minimum rating such as "7+" with a star, for the rating filter menus. - * - * This is not for showing a title's rating, see [SourceRatingsRow] for that. - */ +val ratingBarHeight: Dp = 32.dp + +@Composable +fun SimpleStarRating( + communityRating: Float?, + modifier: Modifier = Modifier, +) = CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + SimpleStarRating( + text = communityRating?.let { String.format(Locale.getDefault(), "%.1f", it) }, + modifier = modifier, + ) +} + @Composable fun SimpleStarRating( text: String?, @@ -51,3 +115,206 @@ fun SimpleStarRating( } } } + +@Composable +fun TomatoRating( + rating: Float?, + modifier: Modifier = Modifier, + threshold: Float = 60f, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + if (rating != null) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + Text( + text = rating.toInt().toString() + "%", + maxLines = 1, + modifier = Modifier, + ) + } + val height = with(LocalDensity.current) { LocalTextStyle.current.fontSize.toDp() * .8f } + Icon( + painter = + painterResource( + if (rating >= threshold) { + R.drawable.ic_rotten_tomatoes_fresh + } else { + R.drawable.ic_rotten_tomatoes_rotten + }, + ), + contentDescription = null, + modifier = Modifier.height(height), + tint = Color.Unspecified, + ) + } + } +} + +/** + * Shows a rating out of 5 stars + */ +@Composable +fun StarRating( + rating100: Int, + onRatingChange: (Int) -> Unit, + precision: StarRatingPrecision, + enabled: Boolean, + playSoundOnFocus: Boolean, + modifier: Modifier = Modifier, + bgColor: Color = AppColors.TransparentBlack75, // MaterialTheme.colorScheme.background, +) { + val context = LocalContext.current + var tempRating by remember(rating100) { mutableIntStateOf(rating100) } + val percentage = (if (enabled) tempRating else rating100) / 100f + val focusRequesters = remember { List(5) { FocusRequester() } } + Box( + modifier = + modifier + .clip(RoundedCornerShape(16.dp)) + .background(bgColor), + ) { + LazyRow( + modifier = + Modifier + .selectableGroup() + .padding(4.dp) + .drawWithCache { + onDrawWithContent { + drawContent() + if (percentage in 0f..<1f) { + drawRect( + color = bgColor, + topLeft = Offset(size.width * percentage, 0f), + blendMode = BlendMode.SrcAtop, + ) + } + } + }.focusGroup() + .focusProperties { + onEnter = { + val index = + if (rating100 <= 20) { + 0 + } else if (rating100 <= 40) { + 1 + } else if (rating100 <= 60) { + 2 + } else if (rating100 <= 80) { + 3 + } else { + 4 + } + focusRequesters[index].tryRequestFocus() + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + for (i in 1..5) { + item { + val isRated = (if (enabled) tempRating else rating100) >= (i * 20) + val icon = Icons.Filled.Star + var focused by remember { mutableStateOf(false) } + + val focusedColor = + if (focused) { + MaterialTheme.colorScheme.border + } else { + Color.Unspecified + } + Box( + modifier = + Modifier + .clip(RoundedCornerShape(16.dp)) + .background(focusedColor), + ) { + Icon( + imageVector = icon, + tint = FilledStarColor, + contentDescription = null, + modifier = + if (enabled) { + Modifier + .onFocusChanged { + focused = it.isFocused + if (it.isFocused) { + tempRating = i * 20 + } else { + tempRating = rating100 + } + }.playSoundOnFocus(playSoundOnFocus) + .focusRequester(focusRequesters[i - 1]) + .focusProperties { + left = + if (i == 1) focusRequesters.last() else FocusRequester.Default + right = + if (i == 5) focusRequesters.first() else FocusRequester.Default + }.selectable( + selected = isRated, + onClick = { + if (playSoundOnFocus) playOnClickSound(context) + val newRating100 = + when (precision) { + StarRatingPrecision.FULL -> { + if (i == 1 && rating100 > 0 && rating100 <= 20) 0 else i * 20 + } + + StarRatingPrecision.HALF -> { + if (rating100 > i * 20) { + i * 20 + } else if (rating100 == i * 20) { + i * 20 - 10 + } else if (rating100 == i * 20 - 10) { + (i - 1) * 20 + } else if (i == 1 && rating100 == 0) { + 20 + } else if (i == 1 && rating100 > 10) { + 10 + } else if (i == 1 && rating100 <= 10) { + 0 + } else { + (i) * 20 + } + } + + StarRatingPrecision.QUARTER -> { + // TODO + null + } + } + if (newRating100 != null) { + tempRating = newRating100 + onRatingChange(newRating100) + } + }, + ) + } else { + Modifier + }.fillMaxHeight() + .aspectRatio(1f), + ) + } + } + } + } + } +} + +@PreviewTvSpec +@Composable +private fun SimpleStarRatingPreview() { + WholphinTheme { + Column { + SimpleStarRating(7.5f, Modifier.height(32.dp)) + TomatoRating(75f) + SimpleStarRating(7.5f, Modifier.height(20.dp)) + SimpleStarRating("", Modifier.height(32.dp)) + TomatoRating(75f) + TomatoRating(40f) + TomatoRating(3f) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt index 73e9af44..9ac5adc0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt @@ -60,7 +60,7 @@ private fun getRecommendedRows(parentId: UUID) = ), ), RecommendedRow( - title = R.string.top_rated, + title = R.string.top_unwatched, handler = GetItemsRequestHandler, request = GetItemsRequest( @@ -69,6 +69,7 @@ private fun getRecommendedRows(parentId: UUID) = includeItemTypes = listOf(BaseItemKind.MUSIC_ALBUM), recursive = true, enableUserData = true, + isPlayed = false, sortBy = listOf(ItemSortBy.COMMUNITY_RATING), sortOrder = listOf(SortOrder.DESCENDING), enableTotalRecordCount = false, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt index 9021e213..f2addcd8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt @@ -143,6 +143,7 @@ fun sliderActiveColor(focused: Boolean): Color { return when (theme) { AppThemeColors.UNRECOGNIZED, AppThemeColors.PURPLE, + AppThemeColors.WOLFSKIN, AppThemeColors.BLUE, AppThemeColors.GREEN, AppThemeColors.ORANGE, @@ -179,6 +180,7 @@ fun sliderInactiveColor(focused: Boolean): Color { return when (theme) { AppThemeColors.UNRECOGNIZED, AppThemeColors.PURPLE, + AppThemeColors.WOLFSKIN, -> { MaterialTheme.colorScheme.border .copy(alpha = .25f) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt deleted file mode 100644 index 19567883..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.github.damontecres.wholphin.ui.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.RatingBadge -import com.github.damontecres.wholphin.data.model.RatingEntry -import com.github.damontecres.wholphin.data.model.SourceRatings -import com.github.damontecres.wholphin.preferences.DisplayToggle -import com.github.damontecres.wholphin.services.SeerrRatingsService -import com.github.damontecres.wholphin.ui.dot -import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization - -/** - * Provides the [SeerrRatingsService] to [rememberSourceRatings] - */ -val LocalSeerrRatingsService = - staticCompositionLocalOf { throw IllegalStateException("LocalSeerrRatingsService not set") } - -/** - * The ratings for an item from the same sources as the PWA. This is the only data path for ratings in the UI, every - * screen that shows ratings calls this and draws the result with [SourceRatingsRow] (usually via [QuickDetails]). - * - * Which items have ratings is decided by [SeerrRatingsService]. Returns null while loading, or if there are none, and - * never the previous item's ratings. - */ -@Composable -fun rememberSourceRatings(item: BaseItem?): SourceRatings? { - val service = LocalSeerrRatingsService.current - val key = item?.id - var ratings by remember(key) { mutableStateOf(item?.let { service.cachedFor(it) }) } - LaunchedEffect(key) { - // Leaving the composition or changing the item cancels this collection, and so a Seerr request that is still waiting or running - service.forItem(item).collect { ratings = it } - } - return ratings -} - -@DrawableRes -fun RatingBadge.drawable(): Int = - when (this) { - RatingBadge.RT_FRESH -> R.drawable.ic_rotten_tomatoes_fresh - RatingBadge.RT_ROTTEN -> R.drawable.ic_rotten_tomatoes_rotten - RatingBadge.RT_UPRIGHT -> R.drawable.ic_rt_upright - RatingBadge.RT_SPILLED -> R.drawable.ic_rt_spilled - RatingBadge.RT_UNKNOWN -> R.drawable.ic_rotten_tomatoes_fresh - RatingBadge.IMDB -> R.drawable.ic_rating_imdb - RatingBadge.TMDB -> R.drawable.ic_rating_tmdb - } - -/** - * The badges for the ratings as inline text content. IMDb and TMDB are wordmarks, wider than tall like in the PWA. - */ -@Composable -private fun rememberBadges(textStyle: TextStyle): Map = - remember(textStyle) { - val square = Placeholder(textStyle.fontSize, textStyle.fontSize, PlaceholderVerticalAlign.TextCenter) - val wide = Placeholder(textStyle.fontSize * 1.8f, textStyle.fontSize, PlaceholderVerticalAlign.TextCenter) - RatingBadge.entries.associate { badge -> - val muted = badge == RatingBadge.RT_UNKNOWN - val placeholder = if (badge == RatingBadge.IMDB || badge == RatingBadge.TMDB) wide else square - badge.name to - InlineTextContent(placeholder) { - Image( - painter = painterResource(badge.drawable()), - contentDescription = null, - contentScale = ContentScale.Fit, - alpha = if (muted) 0.55f else 1f, - colorFilter = if (muted) ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }) else null, - modifier = Modifier.fillMaxSize(), - ) - } - } - } - -/** - * The text for the ratings: each as a badge and its number, separated by dots - * - * @param leadingDot whether a dot is put in front of the first rating because something precedes it - * @param episodeLabel put in front of an episode's own score, "Ep" so it isn't mistaken for the series' score - */ -fun ratingsText( - entries: List, - leadingDot: Boolean, - episodeLabel: String, -): AnnotatedString = - buildAnnotatedString { - entries.forEachIndexed { index, entry -> - if (index > 0 || leadingDot) dot() - appendInlineContent(entry.badge.name) - append(" ") - if (entry.isEpisode) append("$episodeLabel ") - append(entry.text) - } - } - -/** - * The one way ratings are drawn: badge and number pairs from [SourceRatings] in the PWA's order, Rotten Tomatoes then IMDb or TMDB. - * - * Respects the community rating (IMDb/TMDB) and critic rating (Rotten Tomatoes) display settings. Draws nothing if - * there is nothing to show. - */ -@Composable -fun SourceRatingsRow( - ratings: SourceRatings?, - modifier: Modifier = Modifier, - textStyle: TextStyle = MaterialTheme.typography.titleSmall, - leadingDot: Boolean = true, -) { - if (ratings == null) return - val enabled = LocalInterfaceCustomization.current.enabledDisplayToggles - val community = DisplayToggle.COMMUNITY_RATING in enabled - val critic = DisplayToggle.CRITIC_RATING in enabled - val episodeLabel = stringResource(R.string.rating_episode_label) - val text = - remember(ratings, community, critic, leadingDot, episodeLabel) { - ratings.visibleEntries(community, critic).takeIf { it.isNotEmpty() }?.let { ratingsText(it, leadingDot, episodeLabel) } - } ?: return - val badges = rememberBadges(textStyle) - Text( - text = text, - color = MaterialTheme.colorScheme.onSurface, - style = textStyle, - inlineContent = badges, - maxLines = 1, - modifier = modifier, - ) -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/StudioCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/StudioCardGrid.kt index b69e1126..9ff0e2c0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/StudioCardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/StudioCardGrid.kt @@ -97,7 +97,6 @@ class StudioViewModel itemId = it.id, imageType = ImageType.THUMB, fillWidth = cardWidthPx, - tag = it.imageTags?.get(ImageType.THUMB), ) Studio( id = it.id, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt index 4268f2d9..c5da7565 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt @@ -95,11 +95,7 @@ fun rememberLogoUrl(item: BaseItem?): String? { item.data.seriesId != null && item.data.parentLogoImageTag != null ) { - imageUrlService.getItemImageUrl( - itemId = item.data.seriesId!!, - imageType = ImageType.LOGO, - tag = item.data.parentLogoImageTag.takeIf { item.data.parentLogoItemId == item.data.seriesId }, - ) + imageUrlService.getItemImageUrl(item.data.seriesId!!, ImageType.LOGO) } else if (ImageType.LOGO in item?.data?.imageTags.orEmpty()) { imageUrlService.getItemImageUrl(item, ImageType.LOGO) } else { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt index 94842ef5..083dded6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt @@ -54,8 +54,8 @@ fun VideoStreamDetails( ) = VideoStreamDetails( chosenStreams?.source, chosenStreams?.videoStream, - chosenStreams?.audioStream?.stream, - chosenStreams?.subtitleStream?.stream, + chosenStreams?.audioStream, + chosenStreams?.subtitleStream, numberOfVersions, modifier, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt index df923a55..a1237acd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt @@ -55,7 +55,6 @@ fun ViewOptionsDialog( onDismissRequest: () -> Unit, onViewOptionsChange: (ViewOptions) -> Unit, defaultViewOptions: ViewOptions = ViewOptions(), - showArtistSource: Boolean = false, ) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } @@ -73,11 +72,11 @@ fun ViewOptionsDialog( window.setDimAmount(0f) } val options = - remember(viewOptions.type, showArtistSource) { + remember(viewOptions.type) { when (viewOptions.type) { - ViewOptionsType.GRID -> ViewOptions.buildGridOptions(showArtistSource) - ViewOptionsType.LIST -> ViewOptions.buildListOptions(showArtistSource) - ViewOptionsType.DENSE_LIST -> ViewOptions.buildListOptions(showArtistSource) + ViewOptionsType.GRID -> ViewOptions.GRID_OPTIONS + ViewOptionsType.LIST -> ViewOptions.LIST_OPTIONS + ViewOptionsType.DENSE_LIST -> ViewOptions.LIST_OPTIONS } } Column( @@ -131,12 +130,6 @@ fun ViewOptionsDialog( } } -/** Which Jellyfin artists list backs the Music > Artists tab */ -enum class ArtistSource { - ARTIST, - ALBUM_ARTIST, -} - /** * Stores the customizable view changes from the user * @@ -153,7 +146,6 @@ data class ViewOptions( val imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY, val showTitles: Boolean = true, val type: ViewOptionsType = ViewOptionsType.GRID, - val extras: ViewOptionsExtras = ViewOptionsExtras.None, ) { companion object { val ViewOptionsColumns = @@ -258,34 +250,12 @@ data class ViewOptions( valueToIndex = { it.ordinal }, ) - val ViewOptionsArtistSource = - AppChoicePreference( - title = R.string.artist_source, - defaultValue = ArtistSource.ARTIST, - displayValues = R.array.artist_sources, - getter = { it.artistSource }, - setter = { viewOptions, value -> - viewOptions.copy(extras = ViewOptionsExtras.MusicLibrary(value)) - }, - indexToValue = { ArtistSource.entries[it] }, - valueToIndex = { it.ordinal }, - ) - val ViewOptionsReset = AppClickablePreference( title = R.string.reset, ) - /** - * Get the options for a grid [ViewOptions] - */ - fun buildGridOptions(showArtistSource: Boolean) = - buildList { - if (showArtistSource) add(ViewOptionsArtistSource) - addAll(GRID_OPTIONS) - } - - private val GRID_OPTIONS = + val GRID_OPTIONS = listOf( ViewOptionsTypePref, ViewOptionsImageType, @@ -299,16 +269,7 @@ data class ViewOptions( ViewOptionsReset, ) - /** - * Get the options for a list [ViewOptions] - */ - fun buildListOptions(showArtistSource: Boolean) = - buildList { - if (showArtistSource) add(ViewOptionsArtistSource) - addAll(LIST_OPTIONS) - } - - private val LIST_OPTIONS = + val LIST_OPTIONS = listOf( ViewOptionsTypePref, ViewOptionsDetailHeader, @@ -377,22 +338,3 @@ val BaseItemKind.defaultViewOptions: ViewOptions else -> ViewOptionsPoster } - -/** - * Extras [ViewOptions] info that is context or library specific - */ -@Serializable -sealed interface ViewOptionsExtras { - @Serializable - data object None : ViewOptionsExtras - - @Serializable - data class MusicLibrary( - val artistSource: ArtistSource = ArtistSource.ARTIST, - ) : ViewOptionsExtras -} - -/** - * The [ArtistSource] if the [ViewOptions.extras] is a [ViewOptionsExtras.MusicLibrary] - */ -val ViewOptions.artistSource: ArtistSource get() = (extras as? ViewOptionsExtras.MusicLibrary)?.artistSource ?: ArtistSource.ARTIST diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt index fb4cdbdd..7c8268b0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt @@ -9,16 +9,11 @@ import android.media.AudioManager import android.os.Build import android.util.Log import android.view.Display -import android.view.Display.HdrCapabilities import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalGridApi -import androidx.compose.foundation.layout.Grid -import androidx.compose.foundation.layout.GridFlow -import androidx.compose.foundation.layout.GridTrackSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -37,7 +32,6 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel @@ -46,11 +40,9 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.data.ItemPlaybackDao -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.DeviceProfileService import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.util.ExceptionHandler @@ -64,11 +56,9 @@ import kotlinx.coroutines.withContext import org.acra.util.versionCodeLong import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.DeviceInfo -import timber.log.Timber import java.io.BufferedReader import java.io.InputStreamReader import java.util.Date -import java.util.Locale import javax.inject.Inject @HiltViewModel @@ -78,18 +68,17 @@ class DebugViewModel @param:ApplicationContext private val context: Context, val serverRepository: ServerRepository, val itemPlaybackDao: ItemPlaybackDao, - val seriesTrackChoiceDao: SeriesTrackChoiceDao, val clientInfo: ClientInfo, val deviceInfo: DeviceInfo, - private val deviceProfileService: DeviceProfileService, ) : ViewModel() { val state = MutableStateFlow(DebugState()) val itemPlaybacks = MutableStateFlow>(emptyList()) val logcat = MutableStateFlow>(emptyList()) - val display: Display by lazy { + val supportedModes by lazy { val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - displayManager.getDisplay(Display.DEFAULT_DISPLAY) + val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) + display.supportedModes.orEmpty() } val av1Included by lazy { @@ -170,7 +159,7 @@ class DebugViewModel state.update { it.copy( deviceInfo = deviceInfoList, - displayModes = display.supportedModes.map { it.toDisplayMode(display) }, + displayModes = supportedModes.map { it.toString() }, ) } } @@ -180,13 +169,6 @@ class DebugViewModel itemPlaybacks.value = results } } - viewModelScope.launchIO { - serverRepository.currentUser?.rowId?.let { - val results = seriesTrackChoiceDao.getAll(it) - Timber.v("Got %s SeriesTrackChoice", results.size) - results.forEach { Timber.v("%s", it) } - } - } viewModelScope.launchIO { val logcat = getLogCatLines() this@DebugViewModel.logcat.value = logcat @@ -224,20 +206,6 @@ class DebugViewModel audioManager.registerAudioDeviceCallback(callback, null) addCloseable { audioManager.unregisterAudioDeviceCallback(callback) } } - viewModelScope.launchIO { - val hdrSupport = - buildMap { - put("HEVC HDR10", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcHDR10()) - put("HEVC HDR10+", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcHDR10Plus()) - put("DoVi single layer", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcDolbyVision()) - put("DoVi Profile 7 EL", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcDolbyVisionEL()) - put("DoVi Profile 8", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcDolbyVisionProfile8()) - put("AV1 HDR10", deviceProfileService.mediaCodecCapabilitiesTest.supportsAV1HDR10()) - put("AV1 HDR10+", deviceProfileService.mediaCodecCapabilitiesTest.supportsAV1HDR10Plus()) - put("AV1 DoVi", deviceProfileService.mediaCodecCapabilitiesTest.supportsAV1DolbyVision()) - } - state.update { it.copy(hdrSupport = hdrSupport) } - } } companion object { @@ -294,9 +262,8 @@ data class LogcatLine( data class DebugState( val appInfo: List = emptyList(), val deviceInfo: List = emptyList(), - val displayModes: List = emptyList(), + val displayModes: List = emptyList(), val audioInfo: List = emptyList(), - val hdrSupport: Map = emptyMap(), ) val AudioDeviceInfo.details: String @@ -416,7 +383,6 @@ val AudioDeviceInfo.details: String "productName=$productName, address=$addressStr)" } -@OptIn(ExperimentalGridApi::class) @Composable fun DebugPage( preferences: UserPreferences, @@ -500,53 +466,14 @@ fun DebugPage( BodyText(it) } - SubSectionTitle("Audio Devices") - state.audioInfo.forEach { + SubSectionTitle("Display Modes") + state.displayModes.forEach { BodyText(it) } - SubSectionTitle("Display Modes") - Grid( - config = { - repeat(6) { column(GridTrackSize.Auto) } - flow = GridFlow.Row - columnGap(8.dp) - rowGap(8.dp) - }, - ) { - EmphasizedBodyText("ID") - EmphasizedBodyText("Width") - EmphasizedBodyText("Height") - EmphasizedBodyText("FPS") - EmphasizedBodyText("HDR") - EmphasizedBodyText("Alt FPS") - - state.displayModes.forEachIndexed { index, mode -> - BodyText(mode.id.toString()) - BodyText(mode.width.toString()) - BodyText(mode.height.toString()) - BodyText(mode.fps) - BodyText(mode.hdr) - BodyText(mode.altFps) - } - } - - SubSectionTitle("Detected HDR Support") - Grid( - config = { - column(GridTrackSize.Auto) - column(GridTrackSize.Auto) - flow = GridFlow.Row - columnGap(8.dp) - rowGap(4.dp) - }, - ) { - EmphasizedBodyText("HDR Format") - EmphasizedBodyText("Supported") - state.hdrSupport.forEach { (name, supported) -> - BodyText(name) - BodyText(supported.toString()) - } + SubSectionTitle("Audio Devices") + state.audioInfo.forEach { + BodyText(it) } } } @@ -638,29 +565,11 @@ private fun SubSectionTitle(text: String) { } @Composable -private fun BodyText( - text: String, - modifier: Modifier = Modifier, -) { +private fun BodyText(text: String) { Text( text = text, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, - modifier = modifier, - ) -} - -@Composable -private fun EmphasizedBodyText( - text: String, - modifier: Modifier = Modifier, -) { - Text( - text = text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - modifier = modifier, ) } @@ -692,45 +601,3 @@ private val THIRD_PARTY_TAGS = "HardwareConfig:W", "DfltImageHeaderParser:W", ) - -data class DisplayMode( - val id: Int, - val width: Int, - val height: Int, - val fps: String, - val hdr: String, - val altFps: String, -) - -fun Display.Mode.toDisplayMode(display: Display): DisplayMode { - val fps = String.format(Locale.getDefault(), "%.3f", refreshRate) - val altFps = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - alternativeRefreshRates.joinToString(", ") { - String.format(Locale.getDefault(), "%.3f", it) - } - } else { - "N/A" - } - val hdr = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - supportedHdrTypes.joinToString(", ") { hdrTypeToString(it) } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - display.hdrCapabilities?.supportedHdrTypes?.joinToString(", ") { hdrTypeToString(it) } - ?: "None found" - } else { - "N/A" - } - return DisplayMode(modeId, physicalWidth, physicalHeight, fps, hdr, altFps) -} - -fun hdrTypeToString(type: Int): String = - when (type) { - HdrCapabilities.HDR_TYPE_HDR10 -> "HDR10" - HdrCapabilities.HDR_TYPE_HDR10_PLUS -> "HDR10+" - HdrCapabilities.HDR_TYPE_HLG -> "HLG" - HdrCapabilities.HDR_TYPE_HLG_PLUS -> "HLG+" - HdrCapabilities.HDR_TYPE_DOLBY_VISION -> "DoVi" - HdrCapabilities.HDR_TYPE_INVALID -> "Invalid" - else -> "Unknown ($type)" - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt index 5690fadc..adbd9a44 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt @@ -43,7 +43,6 @@ import com.github.damontecres.wholphin.ui.main.settings.Library import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.showToast -import com.github.damontecres.wholphin.ui.util.goToRandom import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.DataLoadingState @@ -701,25 +700,11 @@ class FavoritesViewModel viewModelScope.launchIO { try { collectionStateFor(type)?.let { collectionState -> - val list = - collectionState.items.successValue as? BlockingList - if (list != null) { - goToRandom( - navigateTo = ::navigateTo, - parentId = libraryDisplayItemId(type), - filter = - collectionState.filter.copy( - includeItemTypes = listOf(type), - ), - sortAndDirection = collectionState.sortAndDirection, - recursive = true, - list = list, - ) - } else { - Timber.w( - "state.value.items is not successful: %s", - collectionState.items, - ) + val random = + (collectionState.items.successValue as? BlockingList)?.randomBlocking() + Timber.d("Got random item: %s", random?.id) + random?.destination()?.let { + navigateTo(random.destination()) } } } catch (ex: CancellationException) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt index 4e49bd39..1fd7ea9f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt @@ -24,7 +24,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.FavoriteWatchManager -import com.github.damontecres.wholphin.services.HomeDataService +import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.NavDrawerService @@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.services.ThemeSongPlayer import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.deleteItem +import com.github.damontecres.wholphin.services.tvAccess import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.components.ContextMenuProvider import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -74,7 +75,7 @@ class HomeRowGridViewModel val serverRepository: ServerRepository, private val userPreferencesService: UserPreferencesService, private val navDrawerService: NavDrawerService, - private val homeDataService: HomeDataService, + private val homeSettingsService: HomeSettingsService, private val favoriteWatchManager: FavoriteWatchManager, private val backdropService: BackdropService, private val navigationManager: NavigationManager, @@ -127,7 +128,7 @@ class HomeRowGridViewModel val libraries = navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess) val result = - homeDataService.fetchDataForRow( + homeSettingsService.fetchDataForRow( row = rowConfig, scope = viewModelScope, prefs = prefs, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index 64c48f0e..27212b5e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -257,7 +257,6 @@ fun PersonPage( imageUrlService.getItemImageUrl( itemId = person.id, imageType = ImageType.PRIMARY, - tag = person.data.imageTags?.get(ImageType.PRIMARY), ) } // A long filmography is hard to navigate as a row, so it is cut at the same diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 33960f83..b7085db6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -1,26 +1,49 @@ package com.github.damontecres.wholphin.ui.detail import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource @@ -28,49 +51,60 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import androidx.tv.material3.LocalTextStyle +import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.filter.DefaultPlaylistItemsOptions +import com.github.damontecres.wholphin.data.filter.FilterValueOption +import com.github.damontecres.wholphin.data.filter.ItemFilterBy import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.CollectionFolderFilter +import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.BackdropService +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.FilterOptionCache import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.services.MusicService +import com.github.damontecres.wholphin.services.MusicServiceState import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.PlaylistCreator +import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.ui.FontAwesome -import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator -import com.github.damontecres.wholphin.ui.cards.WatchedIcon -import com.github.damontecres.wholphin.ui.collectLatestIn +import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.cards.ItemCardImage import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.Button -import com.github.damontecres.wholphin.ui.components.CollectionFolderState -import com.github.damontecres.wholphin.ui.components.CollectionFolderViewContent -import com.github.damontecres.wholphin.ui.components.CollectionFolderViewModel -import com.github.damontecres.wholphin.ui.components.CollectionFolderViewScope import com.github.damontecres.wholphin.ui.components.ContextMenu import com.github.damontecres.wholphin.ui.components.ContextMenuActions import com.github.damontecres.wholphin.ui.components.ContextMenuDialog -import com.github.damontecres.wholphin.ui.components.GridClickActions -import com.github.damontecres.wholphin.ui.components.ListItemWrapper +import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.ExpandableFaButton +import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton +import com.github.damontecres.wholphin.ui.components.FilterByButton +import com.github.damontecres.wholphin.ui.components.GridTitle +import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.MusicContextActions import com.github.damontecres.wholphin.ui.components.Optional +import com.github.damontecres.wholphin.ui.components.OverviewText +import com.github.damontecres.wholphin.ui.components.SortByButton import com.github.damontecres.wholphin.ui.components.TextButton -import com.github.damontecres.wholphin.ui.components.ViewOptions -import com.github.damontecres.wholphin.ui.components.ViewOptionsType import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel -import com.github.damontecres.wholphin.ui.data.PlaylistSortOptions +import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.music.MusicQueueMarker import com.github.damontecres.wholphin.ui.detail.music.MusicViewModel import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.equalsNotNull +import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.formatTime -import com.github.damontecres.wholphin.ui.gt +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.main.settings.MoveDirection @@ -79,9 +113,13 @@ import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundSeconds import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.toServerString +import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.util.LocalClock import com.github.damontecres.wholphin.util.ApiRequestPager -import com.github.damontecres.wholphin.util.successValue +import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.WholphinDispatchers import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -89,62 +127,62 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.extensions.playlistsApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber import java.util.UUID +import kotlin.time.Duration @HiltViewModel(assistedFactory = PlaylistViewModel.Factory::class) class PlaylistViewModel @AssistedInject constructor( - private val savedStateHandle: SavedStateHandle, @ApplicationContext context: Context, api: ApiClient, navigationManager: NavigationManager, musicService: MusicService, mediaManagementService: MediaManagementService, + private val backdropService: BackdropService, private val serverRepository: ServerRepository, + private val libraryDisplayInfoDao: LibraryDisplayInfoDao, + private val favoriteWatchManager: FavoriteWatchManager, + private val serverReportService: ServerReportService, + private val filterOptionCache: FilterOptionCache, + private val playlistCreator: PlaylistCreator, @Assisted itemId: UUID, - @Assisted private val collectionState: StateFlow, ) : MusicViewModel(itemId, context, api, musicService, navigationManager, mediaManagementService) { @AssistedFactory interface Factory { - fun create( - itemId: UUID, - collectionState: StateFlow, - ): PlaylistViewModel + fun create(itemId: UUID): PlaylistViewModel } - private val _state = MutableStateFlow(PlaylistDetailsState()) - val state: StateFlow = _state - + val state = MutableStateFlow(PlaylistDetailsState()) val musicState = musicService.state - var position: Int - get() = savedStateHandle.get("position") ?: 0 - set(value) { - savedStateHandle["position"] = value - } - init { init() } override fun init() { + state.update { it.copy(loading = LoadingState.Loading) } viewModelScope.launchDefault { try { + val playlist = + api.userLibraryApi + .getItem(itemId) + .content + .let { BaseItem(it, false) } val user = serverRepository.currentUser val canEdit = user?.let { user -> @@ -177,12 +215,95 @@ class PlaylistViewModel false } } ?: false + state.update { + it.copy( + playlist = playlist, + canEdit = canEdit, + ) + } - _state.update { it.copy(canEdit = canEdit) } + val libraryDisplayInfo = + user?.let { user -> + libraryDisplayInfoDao.getItem(user, itemId) + } + val filter = libraryDisplayInfo?.filter ?: GetItemsFilter() + val sortAndDirection = + libraryDisplayInfo?.sortAndDirection ?: SortAndDirection( + ItemSortBy.DEFAULT, + SortOrder.ASCENDING, + ) + loadItems(filter, sortAndDirection).join() determineMediaType() } catch (ex: Exception) { - Timber.e(ex, "Error fetching playlist info for %s", itemId) -// _state.update { it.copy(item = DataLoadingState.Error(ex)) } + Timber.e(ex, "Error fetching playlist %s", itemId) + state.update { it.copy(loading = LoadingState.Error(ex)) } + } + } + } + + fun loadItems( + filter: GetItemsFilter, + sortAndDirection: SortAndDirection, + ) = viewModelScope.launchIO { + backdropService.clearBackdrop() + state.update { + it.copy( + loading = LoadingState.Loading, + filterAndSort = FilterAndSort(filter, sortAndDirection), + ) + } + + serverRepository.currentUser?.let { user -> + viewModelScope.launchIO { + val libraryDisplayInfo = + libraryDisplayInfoDao.getItem(user, itemId)?.copy( + filter = filter, + sort = sortAndDirection.sort, + direction = sortAndDirection.direction, + ) + ?: LibraryDisplayInfo( + userId = user.rowId, + itemId = itemId.toServerString(), + sort = sortAndDirection.sort, + direction = sortAndDirection.direction, + filter = filter, + viewOptions = null, + ) + libraryDisplayInfoDao.saveItem(libraryDisplayInfo) + } + + val request = + filter.applyTo( + GetItemsRequest( + parentId = itemId, + userId = user.id, + fields = SlimItemFields, + sortBy = listOf(sortAndDirection.sort), + sortOrder = listOf(sortAndDirection.direction), + ), + ) + try { + val pager = + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + viewModelScope, + ).init() + state.update { + it.copy( + items = pager, + loading = LoadingState.Success, + ) + } + } catch (ex: Exception) { + Timber.e(ex, "Error fetching playlist %s", itemId) + state.update { + it.copy( + items = emptyList(), + loading = LoadingState.Error(ex), + ) + } } } } @@ -192,49 +313,77 @@ class PlaylistViewModel * * In theory, the server will set the type, but sometimes it doesn't */ - private fun determineMediaType() { - collectionState - .map { Pair(it.item, it.items) } - .distinctUntilChanged() - .catch { ex -> Timber.e(ex, "Error in determineMediaType") } - .collectLatestIn(viewModelScope) { (item, items) -> - var mediaType = item.successValue?.data?.mediaType ?: MediaType.UNKNOWN - mediaType = - if (mediaType == MediaType.UNKNOWN) { - // Otherwise, if a most of the list is one type, we can assume that type - val pager = (items.successValue as? ApiRequestPager<*>) - if (pager != null && pager.size <= 50) { - val types = - (0..<50.coerceAtMost(pager.size)).groupBy { index -> - val pagerItem = pager.getBlocking(index) - when (pagerItem?.type) { - BaseItemKind.AUDIO -> MediaType.AUDIO + private suspend fun determineMediaType() { + // Use the type the server says + var mediaType = + state.value.playlist + ?.data + ?.mediaType ?: MediaType.UNKNOWN + mediaType = + if (mediaType == MediaType.UNKNOWN) { + // Otherwise, if a most of the list is one type, we can assume that type + val pager = (state.value.items as? ApiRequestPager<*>) + if (pager != null && pager.size <= 50) { + val types = + (0..<50.coerceAtMost(pager.size)).groupBy { index -> + val pagerItem = pager.getBlocking(index) + when (pagerItem?.type) { + BaseItemKind.AUDIO -> MediaType.AUDIO - BaseItemKind.VIDEO, - BaseItemKind.EPISODE, - BaseItemKind.MOVIE, - BaseItemKind.BOX_SET, - -> MediaType.VIDEO + BaseItemKind.VIDEO, + BaseItemKind.EPISODE, + BaseItemKind.MOVIE, + BaseItemKind.BOX_SET, + -> MediaType.VIDEO - else -> MediaType.UNKNOWN - } - } - if (types.keys.size == 1) { - types.keys.first() - } else { - MediaType.UNKNOWN + else -> MediaType.UNKNOWN } - } else { - MediaType.UNKNOWN } + if (types.keys.size == 1) { + types.keys.first() } else { - mediaType + MediaType.UNKNOWN } - Timber.d("mediaType=%s", mediaType) - _state.update { - it.copy(mediaType = mediaType) + } else { + MediaType.UNKNOWN } + } else { + mediaType } + Timber.d("mediaType=%s", mediaType) + state.update { + it.copy(mediaType = mediaType) + } + } + + suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List = + filterOptionCache.getFilterOptionValues( + itemId, + filterOption, + ) + + fun updateBackdrop(item: BaseItem) { + viewModelScope.launchIO { + backdropService.submit(item) + } + } + + fun setWatched( + itemId: UUID, + played: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) { + favoriteWatchManager.setWatched(itemId, played) + } + + fun setFavorite( + itemId: UUID, + favorite: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) { + favoriteWatchManager.setFavorite(itemId, favorite) + } + + fun sendMediaReport(itemId: UUID) { + viewModelScope.launchDefault { serverReportService.sendMediaReportFor(itemId) } } fun removeFromPlaylist( @@ -243,12 +392,11 @@ class PlaylistViewModel ) { viewModelScope.launchIO { try { - api.playlistsApi.removeItemFromPlaylist( - this@PlaylistViewModel.itemId.toServerString(), - listOf(itemId.toServerString()), + playlistCreator.removeFromServerPlaylist( + playlistId = this@PlaylistViewModel.itemId, + itemId = itemId, ) - (collectionState.value.items.successValue as? ApiRequestPager<*>) - ?.refreshPagesAfter(index) + (state.value.items as? ApiRequestPager<*>)?.refreshPagesAfter(index) } catch (ex: CancellationException) { throw ex } catch (ex: Exception) { @@ -275,15 +423,31 @@ class PlaylistViewModel itemId = itemId.toServerString(), newIndex = newIndex, ) - (collectionState.value.items.successValue as? ApiRequestPager<*>)?.refreshPagesAfter( - index - 1, - ) + (state.value.items as? ApiRequestPager<*>)?.refreshPagesAfter(index - 1) } } } +@Immutable +data class FilterAndSort( + val filter: GetItemsFilter, + val sortAndDirection: SortAndDirection, +) + data class PlaylistDetailsState( + val playlist: BaseItem? = null, val mediaType: MediaType = MediaType.UNKNOWN, + val items: List = emptyList(), + val filterAndSort: FilterAndSort = + FilterAndSort( + filter = GetItemsFilter(), + sortAndDirection = + SortAndDirection( + ItemSortBy.DEFAULT, + SortOrder.ASCENDING, + ), + ), + val loading: LoadingState = LoadingState.Pending, val canEdit: Boolean = false, ) @@ -292,33 +456,14 @@ fun PlaylistDetails( preferences: UserPreferences, destination: Destination.MediaItem, modifier: Modifier = Modifier, - collectionViewModel: CollectionFolderViewModel = - hiltViewModel( - creationCallback = { - it.create( - itemId = destination.itemId.toServerString(), - initialSortAndDirection = - SortAndDirection( - ItemSortBy.DEFAULT, - SortOrder.ASCENDING, - ), - recursive = false, - collectionFilter = CollectionFolderFilter(), - useSeriesForPrimary = false, - defaultViewOptions = playlistDefaultViewOptions, - ) - }, - ), viewModel: PlaylistViewModel = hiltViewModel( - creationCallback = { it.create(destination.itemId, collectionViewModel.state) }, + creationCallback = { it.create(destination.itemId) }, ), addToPlaylistViewModel: AddPlaylistViewModel = hiltViewModel(), ) { - val state by collectionViewModel.state.collectAsState() - val playlistState by viewModel.state.collectAsState() + val state by viewModel.state.collectAsState() val musicState by viewModel.musicState.collectAsState() - val focusManager = LocalFocusManager.current var showContextMenu by remember { mutableStateOf(null) } var showConfirmTypeDialog by remember { mutableStateOf?>(null) } @@ -331,15 +476,15 @@ fun PlaylistDetails( shuffle: Boolean, mediaTypeOverride: MediaType? = null, ) { - when (mediaTypeOverride ?: playlistState.mediaType) { + when (mediaTypeOverride ?: state.mediaType) { MediaType.VIDEO -> { - collectionViewModel.navigateTo( + viewModel.navigationManager.navigateTo( Destination.PlaybackList( itemId = destination.itemId, startIndex = index, shuffle = shuffle, - filter = state.filter, - sortAndDirection = state.sortAndDirection, + filter = state.filterAndSort.filter, + sortAndDirection = state.filterAndSort.sortAndDirection, ), ) } @@ -353,152 +498,93 @@ fun PlaylistDetails( } } } - - fun showContextMenu( - index: Int, - item: BaseItem, - fromLongClick: Boolean, - ) { - showContextMenu = - if (item.type == BaseItemKind.AUDIO) { - val musicContextActions = - MusicContextActions( - navigateTo = { collectionViewModel.navigateTo(it) }, - onClickPlay = { index, item -> - play( - index, - item, - false, - MediaType.AUDIO, - ) - }, - onClickPlayNext = { index, item -> viewModel.playNext(item) }, - onClickAddToQueue = { item -> - viewModel.addToQueue( - item, - Int.MAX_VALUE, - ) - }, - onClickFavorite = { id, favorite -> - collectionViewModel.setFavorite(index, id, favorite) - }, - onClickAddPlaylist = { itemId -> - addToPlaylistViewModel.loadPlaylists() - showPlaylistDialog.makePresent(itemId) - }, - onClickRemoveFromQueue = { _, _ -> }, - onDeleteItem = { collectionViewModel.deleteItem(index, item) }, - onRemoveFromPlaylist = viewModel::removeFromPlaylist, - ) - ContextMenu.ForMusic( - fromLongClick = fromLongClick, - item = item, - index = index, - canDelete = viewModel.canDelete(item, preferences.appPreferences), - canRemoveFromQueue = false, - actions = musicContextActions, - showRemoveFromPlaylist = playlistState.canEdit, - ) - } else { - val contextActions = - ContextMenuActions( - navigateTo = { collectionViewModel.navigateTo(it) }, - onClickWatch = { id, watched -> - collectionViewModel.setWatched( - index, - id, - watched, - ) - }, - onClickFavorite = { id, favorite -> - collectionViewModel.setFavorite(index, id, favorite) - }, - onClickAddPlaylist = { itemId -> - addToPlaylistViewModel.loadPlaylists() - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = collectionViewModel::sendReportFor, - onDeleteItem = { collectionViewModel.deleteItem(index, item) }, - onClickAddToQueue = { viewModel.addToQueue(it, 0) }, - onShowOverview = {}, - onChooseVersion = { _, _ -> }, - onChooseTracks = {}, - onClearChosenStreams = {}, - onClickRemoveFromNextUp = {}, - onRemoveFromPlaylist = viewModel::removeFromPlaylist, - ) - ContextMenu.ForBaseItem( - fromLongClick = fromLongClick, - item = item, - index = index, - chosenStreams = null, - showGoTo = true, - showStreamChoices = false, - canDelete = viewModel.canDelete(item, preferences.appPreferences), - canRemoveContinueWatching = false, - canRemoveNextUp = false, - actions = contextActions, - showRemoveFromPlaylist = playlistState.canEdit, - ) - } - } - - val actions = + val musicContextActions = remember { - GridClickActions( - onClickItem = { index, item -> collectionViewModel.navigateTo(item.destination(index)) }, - onLongClickItem = { index, item -> showContextMenu(index, item, true) }, - onClickPlayAll = { shuffle -> - state.item.successValue?.let { playlist -> play(0, playlist, shuffle) } + MusicContextActions( + navigateTo = { viewModel.navigationManager.navigateTo(it) }, + onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) }, + onClickPlayNext = { index, item -> viewModel.playNext(item) }, + onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) }, + onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, + onClickAddPlaylist = { itemId -> + addToPlaylistViewModel.loadPlaylists() + showPlaylistDialog.makePresent(itemId) }, - onClickPlayRemoteButton = { index, item -> play(index, item, false) }, + onClickRemoveFromQueue = { _, _ -> }, + onDeleteItem = viewModel::deleteItem, + onRemoveFromPlaylist = viewModel::removeFromPlaylist, ) } - CollectionFolderViewContent( - preferences = preferences, - state = state, - savedPosition = viewModel.position, - itemId = destination.itemId.toServerString(), - initialFilter = CollectionFolderFilter(filter = state.filter), - recursive = false, - actions = actions, - sortOptions = PlaylistSortOptions, - playEnabled = true, - defaultViewOptions = playlistDefaultViewOptions, - viewActions = collectionViewModel, - provider = collectionViewModel, - positionCallback = { _, position -> viewModel.position = position }, - modifier = modifier, - listItemContent = { (item, index, onClick, onLongClick, _, mod) -> - PlaylistItem( - item = item, - index = index, - onClick = onClick, - onLongClick = onLongClick, - canMove = playlistState.canEdit, - moveUpAllowed = index > 0, - moveDownAllowed = - state.items.successValue - ?.lastIndex - ?.gt(index) ?: false, - onClickMove = { direction -> - item?.let { viewModel.onMoveItem(index, item.id, direction) } - when (direction) { - MoveDirection.UP -> focusManager.moveFocus(FocusDirection.Up) - MoveDirection.DOWN -> focusManager.moveFocus(FocusDirection.Down) - } + val contextActions = + remember { + ContextMenuActions( + navigateTo = { viewModel.navigationManager.navigateTo(it) }, + onClickWatch = { id, watched -> viewModel.setWatched(id, watched) }, + onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, + onClickAddPlaylist = { itemId -> + addToPlaylistViewModel.loadPlaylists() + showPlaylistDialog.makePresent(itemId) }, - onClickMore = { item?.let { showContextMenu(index, item, false) } }, - modifier = mod, -// interactionSource = TODO(), - isPlaying = - equalsNotNull( - musicState.currentItemId, - item?.id, - ), - isQueued = remember(musicState.queueVersion) { item?.id in musicState.queuedIds }, + onSendMediaInfo = viewModel::sendMediaReport, + onDeleteItem = viewModel::deleteItem, + onClickAddToQueue = { viewModel.addToQueue(it, 0) }, + onShowOverview = {}, + onChooseVersion = { _, _ -> }, + onChooseTracks = {}, + onClearChosenStreams = {}, + onClickRemoveFromNextUp = {}, + onRemoveFromPlaylist = viewModel::removeFromPlaylist, ) + } + + PlaylistDetailsContent( + loadingState = state.loading, + playlist = state.playlist, + items = state.items, + musicState = musicState, + onChangeBackdrop = viewModel::updateBackdrop, + onClickIndex = { index, item -> + play(index, item, false) }, + onClickPlay = { shuffle -> + state.playlist?.let { + play(0, it, shuffle) + } + }, + onShowContextMenu = { index, item, fromLongClick -> + showContextMenu = + if (item.type == BaseItemKind.AUDIO) { + ContextMenu.ForMusic( + fromLongClick = fromLongClick, + item = item, + index = index, + canDelete = viewModel.canDelete(item, preferences.appPreferences), + canRemoveFromQueue = false, + actions = musicContextActions, + showRemoveFromPlaylist = state.canEdit, + ) + } else { + ContextMenu.ForBaseItem( + fromLongClick = fromLongClick, + item = item, + index = index, + chosenStreams = null, + showGoTo = true, + showStreamChoices = false, + canDelete = viewModel.canDelete(item, preferences.appPreferences), + canRemoveContinueWatching = false, + canRemoveNextUp = false, + actions = contextActions, + showRemoveFromPlaylist = state.canEdit, + ) + } + }, + filterAndSort = state.filterAndSort, + onFilterAndSortChange = viewModel::loadItems, + getPossibleFilterValues = viewModel::getFilterOptionValues, + canEdit = state.canEdit, + onMoveItem = viewModel::onMoveItem, + modifier = modifier, ) showContextMenu?.let { contextMenu -> ContextMenuDialog( @@ -535,7 +621,294 @@ fun PlaylistDetails( } @Composable -fun CollectionFolderViewScope.PlaylistItem( +fun PlaylistDetailsContent( + playlist: BaseItem?, + items: List, + musicState: MusicServiceState, + onClickIndex: (Int, BaseItem) -> Unit, + onShowContextMenu: (Int, BaseItem, Boolean) -> Unit, + onClickPlay: (shuffle: Boolean) -> Unit, + onChangeBackdrop: (BaseItem) -> Unit, + onMoveItem: (Int, UUID, MoveDirection) -> Unit, + filterAndSort: FilterAndSort, + onFilterAndSortChange: (GetItemsFilter, SortAndDirection) -> Unit, + getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, + loadingState: LoadingState, + canEdit: Boolean, + modifier: Modifier = Modifier, +) { + var savedIndex by rememberSaveable { mutableIntStateOf(0) } + var focusedIndex by remember { mutableIntStateOf(savedIndex) } + val focusedItem = items.getOrNull(focusedIndex) + LaunchedEffect(focusedItem) { + focusedItem?.let(onChangeBackdrop) + } + val focusRequester = remember { FocusRequester() } + LaunchedEffect(loadingState) { + if (loadingState is LoadingState.Success || loadingState is LoadingState.Error) { + focusRequester.tryRequestFocus() + } + } + + val playButtonFocusRequester = remember { FocusRequester() } + + Box( + modifier = modifier, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = + Modifier + .fillMaxSize(), + ) { + val title = + if (loadingState is LoadingState.Success) { + playlist?.name ?: stringResource(R.string.playlist) + } else { + "" + } + GridTitle(title) + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = + Modifier + .fillMaxWidth(), + ) { + PlaylistDetailsHeader( + focusedItem = focusedItem, + onClickPlay = onClickPlay, + playButtonFocusRequester = playButtonFocusRequester, + focusRequester = if (items.isEmpty()) focusRequester else remember { FocusRequester() }, + filterAndSort = filterAndSort, + onFilterAndSortChange = onFilterAndSortChange, + getPossibleFilterValues = getPossibleFilterValues, + filterOptions = DefaultPlaylistItemsOptions, + modifier = + Modifier + .padding(top = 80.dp) + .fillMaxWidth(.25f), + ) + val filterCount = + remember(filterAndSort) { + filterAndSort.filter.countFilters(DefaultPlaylistItemsOptions) + } + PlaylistItems( + loadingState = loadingState, + items = items, + musicState = musicState, + playButtonFocusRequester = playButtonFocusRequester, + onFocusItem = { index, item -> + focusedIndex = index + }, + onClickItem = { index, item -> + savedIndex = index + item?.let { onClickIndex.invoke(index, item) } + }, + onShowContextMenu = { index, item, fromLongClick -> + savedIndex = index + item?.let { onShowContextMenu.invoke(index, item, fromLongClick) } + }, + canMove = canEdit && filterAndSort.sortAndDirection.sort == ItemSortBy.DEFAULT && filterCount == 0, + onMoveItem = onMoveItem, + modifier = + Modifier + .padding(start = 16.dp) + .weight(1f) + .focusRequester(focusRequester) + .focusProperties { + onExit = { + playButtonFocusRequester.tryRequestFocus() + } + }, + ) + } + } + } +} + +@Composable +fun PlaylistDetailsHeader( + focusedItem: BaseItem?, + filterOptions: List>, + onClickPlay: (shuffle: Boolean) -> Unit, + playButtonFocusRequester: FocusRequester, + focusRequester: FocusRequester, + filterAndSort: FilterAndSort, + onFilterAndSortChange: (GetItemsFilter, SortAndDirection) -> Unit, + getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier, + ) { + ExpandablePlayButton( + title = R.string.play, + resume = Duration.ZERO, + icon = Icons.Default.PlayArrow, + onClick = { onClickPlay.invoke(false) }, + modifier = Modifier.focusRequester(playButtonFocusRequester), + ) + ExpandableFaButton( + title = R.string.shuffle, + iconStringRes = R.string.fa_shuffle, + onClick = { onClickPlay.invoke(true) }, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier, + ) { + FilterByButton( + filterOptions = filterOptions, + current = filterAndSort.filter, + onFilterChange = { + onFilterAndSortChange.invoke( + it, + filterAndSort.sortAndDirection, + ) + }, + getPossibleValues = getPossibleFilterValues, + modifier = Modifier.focusRequester(focusRequester), + ) + SortByButton( + sortOptions = BoxSetSortOptions, + current = filterAndSort.sortAndDirection, + onSortChange = { onFilterAndSortChange.invoke(filterAndSort.filter, it) }, + ) + } + Text( + text = focusedItem?.title ?: "", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall, + ) + Text( + text = focusedItem?.subtitle ?: "", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + ) + if (focusedItem?.type == BaseItemKind.EPISODE && focusedItem.data.premiereDate != null) { + Text( + text = formatDateTime(focusedItem.data.premiereDate!!), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleSmall, + ) + } + OverviewText( + overview = focusedItem?.data?.overview ?: "", + maxLines = 10, + onClick = {}, + enabled = false, + ) + } +} + +@Composable +fun PlaylistItems( + loadingState: LoadingState, + items: List, + musicState: MusicServiceState, + playButtonFocusRequester: FocusRequester, + canMove: Boolean, + onMoveItem: (Int, UUID, MoveDirection) -> Unit, + onFocusItem: (Int, BaseItem?) -> Unit, + onClickItem: (Int, BaseItem?) -> Unit, + onShowContextMenu: (Int, BaseItem?, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + when (loadingState) { + is LoadingState.Error -> { + ErrorMessage(loadingState, modifier) + } + + LoadingState.Pending, LoadingState.Loading -> { + LoadingPage(modifier) + } + + LoadingState.Success -> { + if (items.isNotEmpty()) { + LazyColumn( + contentPadding = PaddingValues(8.dp), + modifier = + modifier + .padding(bottom = 32.dp) + .fillMaxHeight() + .background( + MaterialTheme.colorScheme + .surfaceColorAtElevation(1.dp) + .copy(alpha = .75f), + shape = RoundedCornerShape(16.dp), + ).focusGroup() + .focusRestorer(), + ) { + itemsIndexed(items) { index, item -> + PlaylistItem( + item = item, + index = index, + onClick = { + onClickItem.invoke(index, item) + }, + onLongClick = { + onShowContextMenu.invoke(index, item, true) + }, + isPlaying = + equalsNotNull( + musicState.currentItemId, + item?.id, + ), + isQueued = item?.id in musicState.queuedIds, + canMove = canMove, + moveUpAllowed = index > 0, + moveDownAllowed = index < items.lastIndex, + onClickMove = { direction -> + item?.let { onMoveItem.invoke(index, item.id, direction) } + when (direction) { + MoveDirection.UP -> focusManager.moveFocus(FocusDirection.Up) + MoveDirection.DOWN -> focusManager.moveFocus(FocusDirection.Down) + } + }, + onClickMore = { onShowContextMenu.invoke(index, item, false) }, + modifier = + Modifier + .animateItem() + .ifElse( + item?.type != BaseItemKind.AUDIO, + Modifier.height(80.dp), + ).onFocusChanged { + if (it.hasFocus) { + onFocusItem(index, item) + } + }, + ) + } + } + } else { + LaunchedEffect(Unit) { + playButtonFocusRequester.tryRequestFocus() + } + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.no_results), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier, + ) + } + } + } + } +} + +@Composable +fun PlaylistItem( item: BaseItem?, index: Int, onClick: () -> Unit, @@ -550,18 +923,19 @@ fun CollectionFolderViewScope.PlaylistItem( isPlaying: Boolean = false, isQueued: Boolean = false, ) { - val dense = viewOptions.type == ViewOptionsType.DENSE_LIST val focused by interactionSource.collectIsFocusedAsState() + val imageWidth = 160.dp + val density = LocalDensity.current + val imageWidthPx = remember(imageWidth, density) { with(density) { imageWidth.roundToPx() } } Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier - .fillMaxWidth(), -// .heightIn(min = 40.dp, max = 88.dp), + .fillMaxWidth() + .heightIn(min = 40.dp, max = 88.dp), ) { - ListItemWrapper( - dense = dense, + ListItem( selected = false, onClick = onClick, onLongClick = onLongClick, @@ -580,21 +954,19 @@ fun CollectionFolderViewScope.PlaylistItem( }, trailingContent = { val duration = - remember(item?.data?.runTimeTicks) { - when (item?.type) { - BaseItemKind.AUDIO -> { - item.data.runTimeTicks - ?.ticks - ?.roundSeconds - } + when (item?.type) { + BaseItemKind.AUDIO -> { + item.data.runTimeTicks + ?.ticks + ?.roundSeconds + } - else -> { - item - ?.data - ?.runTimeTicks - ?.ticks - ?.roundMinutes - } + else -> { + item + ?.data + ?.runTimeTicks + ?.ticks + ?.roundMinutes } } duration?.let { duration -> @@ -608,25 +980,15 @@ fun CollectionFolderViewScope.PlaylistItem( val resources = LocalResources.current val durationText = remember(resources, duration) { resources.formatDuration(duration) } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Column { + Column { + Text( + text = durationText, + ) + if (item?.type != BaseItemKind.AUDIO) { Text( - text = durationText, + text = stringResource(R.string.ends_at, endTimeStr), + style = MaterialTheme.typography.bodySmall, ) - if (item?.type != BaseItemKind.AUDIO && !dense) { - val style = - LocalTextStyle.current.let { it.copy(fontSize = it.fontSize * .75) } - Text( - text = stringResource(R.string.ends_at, endTimeStr), - style = style, - ) - } - } - if (item?.played == true) { - WatchedIcon(padding = 0.dp) } } } @@ -634,29 +996,41 @@ fun CollectionFolderViewScope.PlaylistItem( leadingContent = { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - if (sortAndDirection.sort == ItemSortBy.DEFAULT) { -// val indexText = -// remember(index, collectionSize, sortAndDirection) { -// when (sortAndDirection.direction) { -// SortOrder.ASCENDING -> "${index + 1}." -// SortOrder.DESCENDING -> "${collectionSize - index}." -// } -// } - // TODO default descending doesn't work? - val indexText = "${index + 1}." - Text( - text = indexText, - ) - } + Text( + text = "${index + 1}.", + style = MaterialTheme.typography.labelLarge, + ) if (item?.type == BaseItemKind.AUDIO) { MusicQueueMarker( isPlaying = isPlaying, isQueued = isQueued, ) + } else { + val imageType = + remember(item) { + if (item != null && ImageType.THUMB in item.data.imageTags.orEmpty()) { + ImageType.THUMB + } else { + ImageType.PRIMARY + } + } + ItemCardImage( + item = item, + name = item?.name, + imageType = imageType, + showOverlay = true, + favorite = item?.data?.userData?.isFavorite ?: false, + watched = item?.data?.userData?.played ?: false, + unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1, + watchedPercent = 0.0, + numberOfVersions = item?.data?.mediaSourceCount ?: 0, + modifier = Modifier.width(imageWidth), + useFallbackText = false, + fillWidth = imageWidthPx, + ) } - if (item?.favorite == true) FavoriteIndicator() } }, modifier = Modifier.weight(1f), @@ -746,10 +1120,3 @@ fun ConfirmMediaTypeDialog( } } } - -private val playlistDefaultViewOptions = - ViewOptions( - spacing = 0, - showDetails = true, - type = ViewOptionsType.LIST, - ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt index d0017d84..c5686e8f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt @@ -42,8 +42,9 @@ import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.data.model.LocalTrailer import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.SeerrAvailability +import com.github.damontecres.wholphin.data.model.SeerrPermission import com.github.damontecres.wholphin.data.model.Trailer -import com.github.damontecres.wholphin.data.model.canRequestMovies +import com.github.damontecres.wholphin.data.model.hasPermission import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.SeerrUserConfig import com.github.damontecres.wholphin.services.TrailerService @@ -83,12 +84,14 @@ fun DiscoverMovieDetails( } val state by viewModel.state.collectAsState() val userConfig by viewModel.userConfig.collectAsState(null) - val requestEnabled by viewModel.requestEnabled.collectAsState(false) val request4kEnabled by viewModel.request4kEnabled.collectAsState(false) var overviewDialog by remember { mutableStateOf(null) } var showRequestDialog by remember { mutableStateOf(false) } + val requestStr = stringResource(R.string.request) + val request4kStr = stringResource(R.string.request_4k) + when (val st = state.movie) { is DataLoadingState.Error -> { ErrorMessage(st, modifier) @@ -150,7 +153,6 @@ fun DiscoverMovieDetails( RequestMovieDialog( loading = state.profileLoading, data = state.requestData, - requestEnabled = requestEnabled, request4kEnabled = request4kEnabled, movie = movie, onSubmit = { @@ -251,7 +253,7 @@ fun DiscoverMovieDetailsContent( } } }, - canRequest = userConfig.canRequestMovies, + canRequest = userConfig.hasPermission(SeerrPermission.REQUEST), canCancel = canCancel, trailers = trailers, trailerOnClick = trailerOnClick, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt index 34bf4657..51a537e3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt @@ -20,7 +20,6 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.api.seerr.model.MovieDetails -import com.github.damontecres.wholphin.data.model.QuickDetailsData import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.jellyfinId @@ -28,7 +27,7 @@ import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.components.GenreText import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.OverviewText -import com.github.damontecres.wholphin.ui.components.QuickDetails +import com.github.damontecres.wholphin.ui.components.QuickDetailsText import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.isNotNullOrBlank @@ -80,7 +79,7 @@ fun DiscoverMovieDetailsHeader( ) { val resources = LocalResources.current val details = - remember(movie, resources) { + remember(movie, rating, resources) { buildList { movie.releaseDate?.let(::add) movie.runtime @@ -106,11 +105,14 @@ fun DiscoverMovieDetailsHeader( ?.takeIf { it.isNotNullOrBlank() } ?.let(::add) }.let { - QuickDetailsData(listToDotString(it)) + listToDotString( + it, + rating, + ) } } - QuickDetails(details, null, Modifier.padding(start = HeaderUtils.startPadding), ratings = rating) + QuickDetailsText(details, Modifier.padding(start = HeaderUtils.startPadding)) movie.genres?.mapNotNull { it.name }?.letNotEmpty { GenreText(it, Modifier.padding(start = HeaderUtils.startPadding)) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt index 407c6935..658e4ffe 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt @@ -75,8 +75,6 @@ class DiscoverMovieViewModel val state: StateFlow = _state val userConfig = seerrServerRepository.current.map { it?.config } - val requestEnabled = - seerrServerRepository.current.map { it?.requestMovieEnabled ?: false } val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false } @@ -196,7 +194,7 @@ class DiscoverMovieViewModel return@launchIO } try { - val postRequest = + seerrService.api.requestApi.requestPost( RequestPostRequest( is4k = request.is4k, mediaId = request.movieId, @@ -210,9 +208,8 @@ class DiscoverMovieViewModel profileId = request.profileId, rootFolder = request.folder, tags = emptyList(), - ) - Timber.v("postRequest=%s", postRequest) - seerrService.api.requestApi.requestPost(postRequest) + ), + ) } catch (ex: CancellationException) { throw ex } catch (ex: Exception) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index 860af84e..88fa0b50 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -34,11 +34,11 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.api.seerr.model.TvDetails import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.DiscoverItem -import com.github.damontecres.wholphin.data.model.QuickDetailsData import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.data.model.SeerrAvailability +import com.github.damontecres.wholphin.data.model.SeerrPermission import com.github.damontecres.wholphin.data.model.Trailer -import com.github.damontecres.wholphin.data.model.canRequestTv +import com.github.damontecres.wholphin.data.model.hasPermission import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.SeerrUserConfig import com.github.damontecres.wholphin.services.TrailerService @@ -55,7 +55,7 @@ import com.github.damontecres.wholphin.ui.components.GenreText import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.OverviewText -import com.github.damontecres.wholphin.ui.components.QuickDetails +import com.github.damontecres.wholphin.ui.components.QuickDetailsText import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo @@ -87,7 +87,6 @@ fun DiscoverSeriesDetails( val context = LocalContext.current val resources = LocalResources.current val state by viewModel.state.collectAsState() - val requestEnabled by viewModel.requestEnabled.collectAsState() val request4kEnabled by viewModel.request4kEnabled.collectAsState() var overviewDialog by remember { mutableStateOf(null) } @@ -112,7 +111,6 @@ fun DiscoverSeriesDetails( DiscoverSeriesDetailsContent( preferences = preferences, series = item, - availability = state.availability, userConfig = userConfig, rating = state.rating, canCancel = state.canCancelRequest, @@ -189,7 +187,6 @@ fun DiscoverSeriesDetails( title = state.tvSeries.successValue?.name ?: "", seasons = state.seasons, seasons4k = state.seasons4k, - requestEnabled = requestEnabled, request4kEnabled = request4kEnabled, onSubmit = { showRequestSeasonDialog = false @@ -214,7 +211,6 @@ fun DiscoverSeriesDetailsContent( preferences: UserPreferences, userConfig: SeerrUserConfig?, series: TvDetails, - availability: SeerrAvailability, rating: SourceRatings?, canCancel: Boolean, seasons: List, @@ -278,7 +274,9 @@ fun DiscoverSeriesDetailsContent( .padding(top = HeaderUtils.topPadding, bottom = 16.dp), ) ExpandableDiscoverButtons( - availability = availability, + availability = + SeerrAvailability.from(series.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, requestOnClick = requestOnClick, pendingOnClick = requestOnClick, cancelOnClick = cancelOnClick, @@ -292,7 +290,7 @@ fun DiscoverSeriesDetailsContent( } } }, - canRequest = userConfig.canRequestTv, + canRequest = userConfig.hasPermission(SeerrPermission.REQUEST), canCancel = canCancel, trailers = trailers, trailerOnClick = trailerOnClick, @@ -455,7 +453,7 @@ fun DiscoverSeriesDetailsHeader( ) { val resources = LocalResources.current val details = - remember(series, resources) { + remember(series, rating, resources) { buildList { series.firstAirDate?.let(::add) series.episodeRunTime @@ -466,11 +464,14 @@ fun DiscoverSeriesDetailsHeader( ?.let { add(resources.formatDuration(it)) } // TODO }.let { - QuickDetailsData(listToDotString(it)) + listToDotString( + it, + rating, + ) } } - QuickDetails(details, null, Modifier.padding(start = HeaderUtils.startPadding), ratings = rating) + QuickDetailsText(details, Modifier.padding(start = HeaderUtils.startPadding)) series.genres?.mapNotNull { it.name }?.letNotEmpty { GenreText(it, Modifier.padding(start = HeaderUtils.startPadding, bottom = 4.dp)) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt index c20ab9c9..b4d6f37f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt @@ -75,10 +75,6 @@ class DiscoverSeriesViewModel val state: StateFlow = _state val userConfig = seerrServerRepository.current.map { it?.config } - val requestEnabled = - seerrServerRepository.current - .map { it?.requestTvEnabled ?: false } - .stateIn(viewModelScope, SharingStarted.Eagerly, false) val request4kEnabled = seerrServerRepository.current .map { it?.request4kTvEnabled ?: false } @@ -112,7 +108,7 @@ class DiscoverSeriesViewModel backdropService.submit(discoveredItem) updateSeasonStatus(tv) - updateAvailability() + updateCanCancel() viewModelScope.launchIO { val rating = @@ -194,7 +190,6 @@ class DiscoverSeriesViewModel tv: TvDetails, is4k: Boolean, ) { - Timber.v("updateSeasonStatus: id=%s, is4k=%s", tv.id, is4k) val currentUserId = seerrServerRepository.currentUserId.first() val seasonStatus = mutableMapOf() val seasonAvailability = mutableMapOf() @@ -235,10 +230,7 @@ class DiscoverSeriesViewModel seasonStatus[season.seasonNumber] = new } editable[season.seasonNumber] = - currentUserId == req.requestedBy?.id && ( - req.status == RequestStatus.PENDING.status || - req.status == RequestStatus.DECLINED.status - ) + currentUserId == req.requestedBy?.id && req.status == RequestStatus.PENDING.status } } } @@ -307,7 +299,7 @@ class DiscoverSeriesViewModel } } - private suspend fun updateAvailability() { + private suspend fun updateCanCancel() { val user = userConfig.firstOrNull() val canCancel = canUserCancelRequest( @@ -316,32 +308,7 @@ class DiscoverSeriesViewModel ?.mediaInfo ?.requests, ) - val status = - state.value.tvSeries.successValue?.let { tv -> - val overallStatus = SeerrAvailability.from(tv.mediaInfo?.status) - if (overallStatus == SeerrAvailability.UNKNOWN) { - val userRequests = - tv.mediaInfo - ?.requests - ?.filter { it.requestedBy?.id == user?.id } - .orEmpty() - if (userRequests.any { RequestStatus.from(it.status) == RequestStatus.PENDING }) { - SeerrAvailability.PENDING - } else if (userRequests.any { RequestStatus.from(it.status) == RequestStatus.APPROVED }) { - SeerrAvailability.PROCESSING - } else { - SeerrAvailability.UNKNOWN - } - } else { - overallStatus - } - } - _state.update { - it.copy( - canCancelRequest = canCancel, - availability = status ?: SeerrAvailability.UNKNOWN, - ) - } + _state.update { it.copy(canCancelRequest = canCancel) } } fun requestOnClick() { @@ -424,7 +391,7 @@ class DiscoverSeriesViewModel fetchAndSetItem().await()?.let { updateSeasonStatus(it) - updateAvailability() + updateCanCancel() } } } @@ -445,7 +412,7 @@ class DiscoverSeriesViewModel fetchAndSetItem().await()?.let { updateSeasonStatus(it) - updateAvailability() + updateCanCancel() } } } @@ -464,5 +431,4 @@ data class DiscoverSeriesState( val canCancelRequest: Boolean = false, val profileLoading: LoadingState = LoadingState.Pending, val requestData: SeerrRequestData = SeerrRequestData(), - val availability: SeerrAvailability = SeerrAvailability.UNKNOWN, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestMovie.kt index ef6e54b7..7a009f09 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestMovie.kt @@ -43,7 +43,6 @@ import com.github.damontecres.wholphin.util.LoadingState fun RequestMovieDialog( loading: LoadingState, data: SeerrRequestData, - requestEnabled: Boolean, request4kEnabled: Boolean, movie: MovieDetails, onSubmit: (MovieRequest) -> Unit, @@ -55,11 +54,10 @@ fun RequestMovieDialog( RequestMovie( loading = loading, data = data, - requestEnabled = requestEnabled, request4kEnabled = request4kEnabled, movie = movie, onSubmit = onSubmit, - modifier = Modifier.padding(24.dp), + modifier = Modifier.padding(16.dp), ) } } @@ -68,7 +66,6 @@ fun RequestMovieDialog( fun RequestMovie( loading: LoadingState, data: SeerrRequestData, - requestEnabled: Boolean, request4kEnabled: Boolean, movie: MovieDetails, onSubmit: (MovieRequest) -> Unit, @@ -93,8 +90,7 @@ fun RequestMovie( if (data.profiles.isEmpty() && data.rootFolders.isEmpty() && data.profiles4k.isEmpty() && data.rootFolders4k.isEmpty() ) { - // Both enabled, show choice - if (requestEnabled && request4kEnabled) { + if (request4kEnabled) { val items = remember { listOf( @@ -122,13 +118,12 @@ fun RequestMovie( ) } else { LaunchedEffect(Unit) { - onSubmit.invoke(MovieRequest(data, movie.id, request4kEnabled, null, null)) + onSubmit.invoke(MovieRequest(data, movie.id, false, null, null)) } } } else { RequestMovieWithOptions( data = data, - requestEnabled = requestEnabled, request4kEnabled = request4kEnabled, movie = movie, onSubmit = onSubmit, @@ -142,13 +137,12 @@ fun RequestMovie( @Composable private fun RequestMovieWithOptions( data: SeerrRequestData, - requestEnabled: Boolean, request4kEnabled: Boolean, movie: MovieDetails, onSubmit: (MovieRequest) -> Unit, modifier: Modifier = Modifier, ) { - var is4k by remember { mutableStateOf(!requestEnabled) } + var is4k by remember { mutableStateOf(request4kEnabled) } var profile by remember(is4k) { mutableStateOf( if (is4k) { @@ -191,7 +185,7 @@ private fun RequestMovieWithOptions( .fillMaxWidth() .padding(bottom = 8.dp), ) { - if (requestEnabled && request4kEnabled) { + if (request4kEnabled) { ClickSwitch( label = stringResource(R.string.request_4k), checked = is4k, @@ -383,7 +377,7 @@ fun MovieRequestPreview() { ), profiles4k = listOf( - SeerrProfile(1, "4K", true), + SeerrProfile(1, "HD", true), SeerrProfile(2, "Ultra HD", false), ), rootFolders4k = @@ -391,8 +385,7 @@ fun MovieRequestPreview() { SeerrRootFolder(1, "/movies", "400GB", true), ), ), - requestEnabled = true, - request4kEnabled = true, + request4kEnabled = false, movie = MovieDetails(id = 1, title = "Movie Name", releaseDate = "2026"), onSubmit = {}, modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt index 91a10279..bc11825f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt @@ -65,12 +65,11 @@ fun RequestSeasons( seasons: List, seasons4k: List, data: SeerrRequestData, - requestEnabled: Boolean, request4kEnabled: Boolean, onSubmit: (TvRequest) -> Unit, modifier: Modifier = Modifier, ) { - var is4k by remember { mutableStateOf(!requestEnabled) } + var is4k by remember { mutableStateOf(request4kEnabled) } val seasons = remember(is4k, seasons, seasons4k) { if (is4k) seasons4k else seasons } val allSeasonNumbers = @@ -162,7 +161,7 @@ fun RequestSeasons( .fillMaxWidth() .padding(vertical = 8.dp), ) { - if (requestEnabled && request4kEnabled) { + if (request4kEnabled) { ClickSwitch( label = stringResource(R.string.request_4k), checked = is4k, @@ -392,7 +391,6 @@ fun RequestSeasonsDialog( data: SeerrRequestData, seasons: List, seasons4k: List, - requestEnabled: Boolean, request4kEnabled: Boolean, onSubmit: (TvRequest) -> Unit, onDismissRequest: () -> Unit, @@ -420,7 +418,6 @@ fun RequestSeasonsDialog( data = data, seasons = seasons, seasons4k = seasons4k, - requestEnabled = requestEnabled, request4kEnabled = request4kEnabled, onSubmit = onSubmit, modifier = @@ -478,7 +475,6 @@ fun RequestSeasonsPreview() { SeerrRootFolder(1, "/tv", "400GB", true), ), ), - requestEnabled = true, request4kEnabled = true, onSubmit = { }, modifier = Modifier.width(400.dp), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt index 3c341ff2..e8872180 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt @@ -55,7 +55,7 @@ fun EpisodeDetailsHeader( modifier = Modifier.fillMaxWidth(.60f), ) { val padding = 8.dp - QuickDetails(ep, ep.timeRemainingOrRuntime) + QuickDetails(ep.ui.quickDetails, ep.timeRemainingOrRuntime) VideoStreamDetails( chosenStreams = chosenStreams, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt index 97aaaf86..a45fbda0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt @@ -145,10 +145,11 @@ class EpisodeViewModel ) { viewModelScope.launchIO { val prefs = userPreferencesService.getCurrent() + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId) val chosen = result?.let { - itemPlaybackRepository.getChosenItemFromPlayback(item, result, prefs) + itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs) } _state.update { it.copy(chosenStreams = chosen) } } @@ -162,6 +163,7 @@ class EpisodeViewModel ) { viewModelScope.launchIO { val prefs = userPreferencesService.getCurrent() + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) val result = itemPlaybackRepository.saveTrackSelection( item = item, @@ -169,7 +171,10 @@ class EpisodeViewModel trackIndex = trackIndex, type = type, ) - val chosen = itemPlaybackRepository.getChosenItemFromPlayback(item, result, prefs) + val chosen = + result?.let { + itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs) + } _state.update { it.copy(chosenStreams = chosen) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index bcba145c..470f33c2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -149,11 +149,7 @@ class LiveTvViewModel number = it.channelNumber, name = it.channelName ?: it.name, imageUrl = - imageUrlService.getItemImageUrl( - itemId = it.id, - imageType = ImageType.PRIMARY, - tag = it.imageTags?.get(ImageType.PRIMARY), - ), + imageUrlService.getItemImageUrl(it.id, ImageType.PRIMARY), favorite = it.userData?.isFavorite == true, ) } @@ -303,7 +299,6 @@ class LiveTvViewModel imageUrlService.getItemImageUrl( dto.id, ImageType.PRIMARY, - tag = dto.imageTags?.get(ImageType.PRIMARY), ), ) if (index == 0) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index ef0d186e..bf8d698f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -339,6 +339,7 @@ fun MovieDetailsContent( chosenStreams = state.chosenStreams, bringIntoViewRequester = bringIntoViewRequester, overviewOnClick = overviewOnClick, + sourceRatings = state.sourceRatings, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index 6dbbf965..13ee747c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -20,6 +20,8 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.SourceRatings +import com.github.damontecres.wholphin.data.model.withSourceRatings import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.GenreText import com.github.damontecres.wholphin.ui.components.HeaderUtils @@ -41,8 +43,10 @@ fun MovieDetailsHeader( bringIntoViewRequester: BringIntoViewRequester, overviewOnClick: () -> Unit, modifier: Modifier = Modifier, + sourceRatings: SourceRatings? = null, ) { val dto = movie.data + val quickDetails = remember(movie, sourceRatings) { movie.ui.quickDetails.withSourceRatings(sourceRatings) } val context = LocalContext.current val scope = rememberCoroutineScope() Column( @@ -64,7 +68,7 @@ fun MovieDetailsHeader( modifier = Modifier.fillMaxWidth(.60f), ) { QuickDetails( - movie, + quickDetails, movie.timeRemainingOrRuntime, Modifier.padding(start = HeaderUtils.startPadding), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index 9a8dd033..fc9e7ad6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.ExtrasService @@ -19,6 +20,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PeopleFavorites +import com.github.damontecres.wholphin.services.SeerrRatingsService import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.services.StreamChoiceService @@ -57,6 +59,7 @@ class MovieViewModel constructor( private val api: ApiClient, private val seerrService: SeerrService, + private val ratingsService: SeerrRatingsService, @param:ApplicationContext private val context: Context, private val navigationManager: NavigationManager, val serverRepository: ServerRepository, @@ -100,6 +103,8 @@ class MovieViewModel return item } + private var ratingsJob: Job? = null + fun init(): Job = viewModelScope.launchDefault { val movie = @@ -124,6 +129,7 @@ class MovieViewModel chosenStreams = chosenStreams, trailers = remoteTrailers, chapters = chapters, + sourceRatings = it.sourceRatings ?: ratingsService.cachedFor(movie), ) } backdropService.submit(movie) @@ -152,6 +158,14 @@ class MovieViewModel ) } } + // init() runs again on resume and the flow never completes, so don't stack collectors + ratingsJob?.cancel() + ratingsJob = + viewModelScope.launchIO { + ratingsService.forItem(movie).collect { ratings -> + _state.update { it.copy(sourceRatings = ratings ?: it.sourceRatings) } + } + } viewModelScope.launchIO { val results = seerrService.similar(movie).orEmpty() _state.update { @@ -223,10 +237,11 @@ class MovieViewModel ) { viewModelScope.launchIO { val prefs = userPreferencesService.getCurrent() + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId) val chosen = result?.let { - itemPlaybackRepository.getChosenItemFromPlayback(item, result, prefs) + itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs) } _state.update { it.copy(chosenStreams = chosen) } } @@ -240,6 +255,7 @@ class MovieViewModel ) { viewModelScope.launchIO { val prefs = userPreferencesService.getCurrent() + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) val result = itemPlaybackRepository.saveTrackSelection( item = item, @@ -247,7 +263,10 @@ class MovieViewModel trackIndex = trackIndex, type = type, ) - val chosen = itemPlaybackRepository.getChosenItemFromPlayback(item, result, prefs) + val chosen = + result?.let { + itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs) + } _state.update { it.copy(chosenStreams = chosen) } } } @@ -301,6 +320,7 @@ data class MovieState( val similar: List = emptyList(), val discovered: List = emptyList(), /** Seerr ratings, null until they load or if unavailable */ + val sourceRatings: SourceRatings? = null, val chosenStreams: ChosenStreams? = null, val canDelete: Boolean = false, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt index be7795f4..b63f1cbc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -35,10 +35,10 @@ fun FocusedEpisodeHeader( ) { EpisodeName(dto, modifier = Modifier.padding(start = HeaderUtils.startPadding)) - ep?.let { + ep?.ui?.quickDetails?.let { QuickDetails( it, - it.timeRemainingOrRuntime, + ep.timeRemainingOrRuntime, Modifier.padding(start = HeaderUtils.startPadding), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 0308468e..60419e83 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -44,8 +44,10 @@ import com.github.damontecres.wholphin.data.ExtrasItem import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.studioNames +import com.github.damontecres.wholphin.data.model.withSourceRatings import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.Cards @@ -253,6 +255,7 @@ fun SeriesDetails( } }, discovered = state.discovered, + sourceRatings = state.sourceRatings, onClickDiscover = { index, item -> viewModel.navigateTo(item.destination) }, @@ -330,6 +333,7 @@ fun SeriesDetailsContent( extras: List, people: List, discovered: List, + sourceRatings: SourceRatings?, played: Boolean, favorite: Boolean, canDelete: Boolean, @@ -377,6 +381,7 @@ fun SeriesDetailsContent( showLogo = preferences.appPreferences.interfacePreferences.showLogos, overviewOnClick = overviewOnClick, bringIntoViewRequester = bringIntoViewRequester, + sourceRatings = sourceRatings, modifier = Modifier .fillMaxWidth() @@ -654,9 +659,11 @@ fun SeriesDetailsHeader( overviewOnClick: () -> Unit, bringIntoViewRequester: BringIntoViewRequester, modifier: Modifier = Modifier, + sourceRatings: SourceRatings? = null, ) { val scope = rememberCoroutineScope() val dto = series.data + val quickDetails = remember(series, sourceRatings) { series.ui.quickDetails.withSourceRatings(sourceRatings) } Column( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, @@ -674,7 +681,7 @@ fun SeriesDetailsHeader( modifier = Modifier.fillMaxWidth(.60f), ) { QuickDetails( - series, + quickDetails, null, Modifier.padding(start = HeaderUtils.startPadding), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 3bbc3a6b..caaa1fb5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.services.BackdropService @@ -20,6 +21,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PeopleFavorites +import com.github.damontecres.wholphin.services.SeerrRatingsService import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.services.StreamChoiceService @@ -100,6 +102,7 @@ class SeriesViewModel private val userPreferencesService: UserPreferencesService, private val backdropService: BackdropService, private val seerrService: SeerrService, + private val ratingsService: SeerrRatingsService, private val mediaManagementService: MediaManagementService, @Assisted val seriesId: UUID, @Assisted val seasonEpisodeIds: SeasonEpisodeIds?, @@ -218,6 +221,7 @@ class SeriesViewModel seasons = seasons, episodes = episodes, trailers = remoteTrailers, + sourceRatings = ratingsService.cachedFor(series), ) } @@ -251,6 +255,11 @@ class SeriesViewModel _state.update { it.copy(similar = similar) } } } + viewModelScope.launchIO { + ratingsService.forItem(series).collect { ratings -> + _state.update { it.copy(sourceRatings = ratings ?: it.sourceRatings) } + } + } viewModelScope.launchIO { val results = seerrService.similar(series).orEmpty() _state.update { it.copy(discovered = results) } @@ -561,10 +570,11 @@ class SeriesViewModel ) { viewModelScope.launchIO { val prefs = userPreferencesService.getCurrent() + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId) val chosen = result?.let { - itemPlaybackRepository.getChosenItemFromPlayback(item, result, prefs) + itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs) } _state.update { it.copy(chosenStreams = chosen) } } @@ -578,6 +588,7 @@ class SeriesViewModel ) { viewModelScope.launchIO { val prefs = userPreferencesService.getCurrent() + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) val result = itemPlaybackRepository.saveTrackSelection( item = item, @@ -585,7 +596,10 @@ class SeriesViewModel trackIndex = trackIndex, type = type, ) - val chosen = itemPlaybackRepository.getChosenItemFromPlayback(item, result, prefs) + val chosen = + result?.let { + itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs) + } _state.update { it.copy(chosenStreams = chosen) } } } @@ -891,5 +905,6 @@ data class SeriesState( val discovered: List = emptyList(), val discoverSeries: DiscoverItem? = null, /** Seerr ratings, null until they load or if unavailable */ + val sourceRatings: SourceRatings? = null, val chosenStreams: ChosenStreams? = null, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt index 62075882..4bdc42ba 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt @@ -389,7 +389,7 @@ fun SeerrDiscoverPage( modifier = modifier, ) { val details = - remember(focusedItem) { + remember(focusedItem, ratingMap) { (focusedItem as? DiscoverFocusedItem.Item)?.let { focusedItem -> buildList { focusedItem.item @@ -398,18 +398,17 @@ fun SeerrDiscoverPage( ?.toString() ?.let(::add) }.let { - QuickDetailsData(listToDotString(it)) + val rating = ratingMap[focusedItem.item.type to focusedItem.id] + QuickDetailsData(listToDotString(it, rating)) } } } - val focusedRatings = (focusedItem as? DiscoverFocusedItem.Item)?.let { ratingMap[it.item.type to it.id] } HomePageHeader( title = focusedItem?.title?.getString(), subtitle = focusedItem?.subtitle, overview = focusedItem?.overview, overviewTwoLines = false, quickDetails = details, - ratings = focusedRatings, timeRemaining = null, endsAt = null, showLogo = preferences.appPreferences.interfacePreferences.showLogos, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 26cc96d5..bf24ae00 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -52,7 +52,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.QuickDetailsData -import com.github.damontecres.wholphin.data.model.SourceRatings import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.BannerCard @@ -73,7 +72,6 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.components.rememberLogoUrl -import com.github.damontecres.wholphin.ui.components.rememberSourceRatings import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo @@ -573,7 +571,6 @@ fun HomePageHeader( overview = item?.data?.overview, overviewTwoLines = isEpisode, quickDetails = item?.ui?.quickDetails, - ratings = rememberSourceRatings(item), timeRemaining = item?.timeRemainingOrRuntime, endsAt = item?.data?.endDate, showLogo = showLogo, @@ -589,7 +586,6 @@ fun HomePageHeader( overview: String?, overviewTwoLines: Boolean, quickDetails: QuickDetailsData?, - ratings: SourceRatings?, timeRemaining: Duration?, endsAt: DateTime?, showLogo: Boolean, @@ -615,7 +611,7 @@ fun HomePageHeader( if (subtitle != null) { EpisodeName(subtitle) } - QuickDetails(quickDetails, timeRemaining, endsAt = endsAt, ratings = ratings) + QuickDetails(quickDetails, timeRemaining, endsAt = endsAt) val overviewModifier = Modifier .padding(0.dp) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index d02ba907..847e2e69 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -11,7 +11,6 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.DatePlayedService import com.github.damontecres.wholphin.services.FavoriteWatchManager -import com.github.damontecres.wholphin.services.HomeDataService import com.github.damontecres.wholphin.services.HomePageResolvedSettings import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.services.LatestNextUpService @@ -63,7 +62,6 @@ class HomeViewModel val serverReportService: ServerReportService, private val navDrawerService: NavDrawerService, private val homeSettingsService: HomeSettingsService, - private val homeDataService: HomeDataService, private val favoriteWatchManager: FavoriteWatchManager, private val datePlayedService: DatePlayedService, private val backdropService: BackdropService, @@ -188,7 +186,7 @@ class HomeViewModel semaphore.withPermit { Timber.v("Fetching row: %s", row) try { - homeDataService.fetchDataForRow( + homeSettingsService.fetchDataForRow( row = row.config, scope = viewModelScope, prefs = prefs, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsDestination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsDestination.kt index ae958a93..ad433c23 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsDestination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsDestination.kt @@ -33,9 +33,6 @@ sealed interface HomeSettingsDestination : NavKey { @Serializable data object GlobalSettings : HomeSettingsDestination - @Serializable - data object SourceSettings : HomeSettingsDestination - @Serializable data object Presets : HomeSettingsDestination } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsGlobal.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsGlobal.kt index 74c546b5..894980c4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsGlobal.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsGlobal.kt @@ -31,6 +31,10 @@ fun HomeSettingsGlobal( preferences: AppPreferences, onPreferenceChange: (AppPreferences) -> Unit, onClickResize: (Int) -> Unit, + onClickSave: () -> Unit, + onClickLoad: () -> Unit, + onClickLoadWeb: () -> Unit, + onClickReset: () -> Unit, onClickViewNextUp: () -> Unit, modifier: Modifier = Modifier, ) { @@ -142,6 +146,62 @@ fun HomeSettingsGlobal( modifier = Modifier, ) } + item { + HomeSettingsListItem( + selected = false, + headlineText = stringResource(R.string.save_to_server), + leadingContent = { + Text( + text = stringResource(R.string.fa_cloud_arrow_up), + fontFamily = FontAwesome, + ) + }, + onClick = onClickSave, + modifier = Modifier, + ) + } + item { + HomeSettingsListItem( + selected = false, + headlineText = stringResource(R.string.load_from_server), + leadingContent = { + Text( + text = stringResource(R.string.fa_cloud_arrow_down), + fontFamily = FontAwesome, + ) + }, + onClick = onClickLoad, + modifier = Modifier, + ) + } + item { + HomeSettingsListItem( + selected = false, + headlineText = stringResource(R.string.load_from_web_client), + leadingContent = { + Text( + text = stringResource(R.string.fa_download), + fontFamily = FontAwesome, + ) + }, + onClick = onClickLoadWeb, + modifier = Modifier, + ) + } + item { + HomeSettingsListItem( + selected = false, + headlineText = stringResource(R.string.reset), + leadingContent = { + Text( + text = stringResource(R.string.fa_arrows_rotate), + fontFamily = FontAwesome, + ) + }, + onClick = onClickReset, + modifier = Modifier, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt index 0bd6b7db..5a2d6c78 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt @@ -1,6 +1,5 @@ package com.github.damontecres.wholphin.ui.main.settings -import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -65,14 +64,11 @@ fun HomeSettingsPage( var showConfirmDialog by remember { mutableStateOf(null) } var searchForDialog by remember { mutableStateOf(null) } var showRemovedNextUpDialog by remember { mutableStateOf(false) } - var showSaveDialog by remember { mutableStateOf(false) } val state by viewModel.state.collectAsState() - val serverPluginActive by viewModel.serverPluginActive.collectAsState() var position by rememberPosition(0, 0) // TODO discover rows val discoverEnabled = false // by viewModel.discoverEnabled.collectAsState(false) - val settingsHaveChanged by viewModel.settingsChanged.collectAsState() // Adds a row, waits until its done loading, then scrolls to the new row fun addRow( @@ -90,10 +86,6 @@ fun HomeSettingsPage( } } - BackHandler(settingsHaveChanged) { - showSaveDialog = true - } - Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, @@ -145,7 +137,6 @@ fun HomeSettingsPage( position = RowColumn(index, 0) } }, - onClickSourceSettings = { backStack.add(HomeSettingsDestination.SourceSettings) }, modifier = destModifier, ) } @@ -291,16 +282,6 @@ fun HomeSettingsPage( } }, onClickResize = { viewModel.resizeCards(it) }, - onClickViewNextUp = { - showRemovedNextUpDialog = true - }, - modifier = destModifier, - ) - } - - HomeSettingsDestination.SourceSettings -> { - HomeSettingsSourcePage( - source = state.source, onClickSave = { showConfirmDialog = ShowConfirm(R.string.overwrite_server_settings) { @@ -313,13 +294,6 @@ fun HomeSettingsPage( viewModel.loadFromRemote() } }, - serverPluginActive = serverPluginActive, - onClickLoadPlugin = { - showConfirmDialog = - ShowConfirm(R.string.overwrite_local_settings) { - viewModel.loadFromRemotePlugin() - } - }, onClickLoadWeb = { showConfirmDialog = ShowConfirm(R.string.overwrite_local_settings) { @@ -332,6 +306,9 @@ fun HomeSettingsPage( addRow(false) { viewModel.resetToDefault() } } }, + onClickViewNextUp = { + showRemovedNextUpDialog = true + }, modifier = destModifier, ) } @@ -398,29 +375,6 @@ fun HomeSettingsPage( ) } } - if (showSaveDialog) { - SaveHomeSettingsDialog( - onDismissRequest = { showSaveDialog = false }, - onSaveLocal = { - scope.launch { - viewModel.saveToLocal().join() - viewModel.navigationManager.goBack() - } - }, - onSaveServer = { - scope.launch { - viewModel.saveToRemote().join() - viewModel.navigationManager.goBack() - } - }, - onDiscard = { - scope.launch { - viewModel.discardChanges().join() - viewModel.navigationManager.goBack() - } - }, - ) - } } data class ShowConfirm( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt index 72289463..dc527ed4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt @@ -49,11 +49,6 @@ enum class MoveDirection { DOWN, } -private const val POS_ADD_ROW = 0 -private const val POS_SOURCE = POS_ADD_ROW + 1 -private const val POS_SETTINGS = POS_SOURCE + 1 -private const val POS_PRESETS = POS_SETTINGS + 1 - @Composable fun HomeSettingsRowList( state: HomePageSettingsState, @@ -63,14 +58,13 @@ fun HomeSettingsRowList( onClickPresets: () -> Unit, onClickMove: (MoveDirection, Int) -> Unit, onClickDelete: (Int) -> Unit, - onClickSourceSettings: () -> Unit, modifier: Modifier, ) { val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() val listState = rememberLazyListState() - val itemsBeforeRows = POS_PRESETS + 1 // +1 for divider + val itemsBeforeRows = 4 val focusRequesters = remember(state.rows.size) { List(itemsBeforeRows + state.rows.size) { FocusRequester() } } @@ -86,7 +80,7 @@ fun HomeSettingsRowList( modifier = modifier .fillMaxHeight() - .focusRestorer(focusRequesters[POS_ADD_ROW]), + .focusRestorer(focusRequesters[0]), ) { item { HomeSettingsListItem( @@ -99,30 +93,10 @@ fun HomeSettingsRowList( ) }, onClick = { - position = POS_ADD_ROW + position = 0 onClickAdd.invoke() }, - modifier = Modifier.focusRequester(focusRequesters[POS_ADD_ROW]), - ) - } - item { - HomeSettingsListItem( - selected = false, - headlineText = "Source", - leadingContent = { - Text( - text = stringResource(R.string.fa_download), - fontFamily = FontAwesome, - ) - }, - supportingContent = { - Text(text = stringResource(state.source.stringResId)) - }, - onClick = { - position = POS_SOURCE - onClickSourceSettings.invoke() - }, - modifier = Modifier.focusRequester(focusRequesters[POS_SOURCE]), + modifier = Modifier.focusRequester(focusRequesters[0]), ) } item { @@ -136,10 +110,10 @@ fun HomeSettingsRowList( ) }, onClick = { - position = POS_SETTINGS + position = 1 onClickSettings.invoke() }, - modifier = Modifier.focusRequester(focusRequesters[POS_SETTINGS]), + modifier = Modifier.focusRequester(focusRequesters[1]), ) } item { @@ -158,10 +132,10 @@ fun HomeSettingsRowList( ) }, onClick = { - position = POS_PRESETS + position = 2 onClickPresets.invoke() }, - modifier = Modifier.focusRequester(focusRequesters[POS_PRESETS]), + modifier = Modifier.focusRequester(focusRequesters[2]), ) } item { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsSourcePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsSourcePage.kt deleted file mode 100644 index 8139b651..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsSourcePage.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.github.damontecres.wholphin.ui.main.settings - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRestorer -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.services.HomePageSettingsSource -import com.github.damontecres.wholphin.ui.FontAwesome -import com.github.damontecres.wholphin.ui.tryRequestFocus - -@Composable -fun HomeSettingsSourcePage( - source: HomePageSettingsSource, - onClickSave: () -> Unit, - onClickLoad: () -> Unit, - onClickLoadWeb: () -> Unit, - onClickReset: () -> Unit, - serverPluginActive: Boolean, - onClickLoadPlugin: () -> Unit, - modifier: Modifier = Modifier, -) { - val firstFocus: FocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { firstFocus.tryRequestFocus() } - Column(modifier = modifier) { - Text( - text = stringResource(R.string.settings), - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = - modifier - .fillMaxHeight() - .focusRestorer(firstFocus), - ) { - item { - HomeSettingsListItem( - selected = false, - headlineText = stringResource(R.string.load_from_server), - leadingContent = { - Text( - text = stringResource(R.string.fa_cloud_arrow_down), - fontFamily = FontAwesome, - ) - }, - onClick = onClickLoad, - modifier = Modifier, - ) - } - if (serverPluginActive) { - item { - HomeSettingsListItem( - selected = false, - headlineText = stringResource(R.string.load_from_server_plugin), - leadingContent = { - Text( - text = stringResource(R.string.fa_download), - fontFamily = FontAwesome, - ) - }, - onClick = onClickLoadPlugin, - modifier = Modifier, - ) - } - } - item { - HomeSettingsListItem( - selected = false, - headlineText = stringResource(R.string.load_from_web_client), - leadingContent = { - Text( - text = stringResource(R.string.fa_download), - fontFamily = FontAwesome, - ) - }, - onClick = onClickLoadWeb, - modifier = Modifier, - ) - } - item { - HomeSettingsListItem( - selected = false, - headlineText = stringResource(R.string.reset), - leadingContent = { - Text( - text = stringResource(R.string.fa_arrows_rotate), - fontFamily = FontAwesome, - ) - }, - onClick = onClickReset, - modifier = Modifier, - ) - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt index 1082a23f..e3eafcbb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt @@ -24,18 +24,16 @@ import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.SUPPORTED_HOME_PAGE_SETTINGS_VERSION import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.services.BackdropService -import com.github.damontecres.wholphin.services.HomeDataService import com.github.damontecres.wholphin.services.HomePageResolvedSettings -import com.github.damontecres.wholphin.services.HomePageSettingsSource import com.github.damontecres.wholphin.services.HomeRowConfigDisplay import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.services.NavDrawerService -import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SeerrServerRepository -import com.github.damontecres.wholphin.services.ServerPluginApi import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.getRecentlyAddedTitle +import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope +import com.github.damontecres.wholphin.services.tvAccess import com.github.damontecres.wholphin.services.viewOptionsForCollectionType import com.github.damontecres.wholphin.ui.formatTypeName import com.github.damontecres.wholphin.ui.launchDefault @@ -50,16 +48,13 @@ import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.WholphinDispatchers import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -81,55 +76,37 @@ class HomeSettingsViewModel @param:ApplicationContext private val context: Context, private val api: ApiClient, private val homeSettingsService: HomeSettingsService, - private val homeDataService: HomeDataService, private val serverRepository: ServerRepository, private val userPreferencesService: UserPreferencesService, private val navDrawerService: NavDrawerService, private val backdropService: BackdropService, private val seerrServerRepository: SeerrServerRepository, - private val serverPluginApi: ServerPluginApi, val preferencesDataStore: DataStore, - val navigationManager: NavigationManager, + @param:IoCoroutineScope private val ioScope: CoroutineScope, ) : ViewModel() { private val _state = MutableStateFlow(HomePageSettingsState.EMPTY) val state: StateFlow = _state - val serverPluginActive get() = serverRepository.serverPluginInstalled - val currentUser - get() = - serverRepository.currentUserFlow.filterNotNull().stateIn( - viewModelScope, - SharingStarted.Eagerly, - serverRepository.currentUser!!, - ) - val settingsChanged = - state - .map { - HomePageSettings( - rows = it.rows.map { it.config }, - version = SUPPORTED_HOME_PAGE_SETTINGS_VERSION, - ) != originalSettings - }.stateIn(viewModelScope, SharingStarted.Eagerly, false) - private var idCounter by Delegates.notNull() val discoverEnabled = seerrServerRepository.active - private var originalSettings: HomePageSettings? = null + private var originalLocalSettings: HomePageSettings? = null + private var originalRemoteSettings: HomePageSettings? = null init { + addCloseable { saveToLocal() } viewModelScope.launchIO { - val user = serverRepository.currentUser ?: return@launchIO val userDto = serverRepository.currentUserDto ?: return@launchIO val libraries = navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess) val currentSettings = homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY } - originalSettings = currentSettings.asHomePageSettings() + originalLocalSettings = homeSettingsService.loadFromLocal(userDto.id) + originalRemoteSettings = homeSettingsService.loadFromServer(userDto.id) Timber.v("currentSettings=%s", currentSettings) idCounter = currentSettings.rows.maxOfOrNull { it.id }?.plus(1) ?: 0 _state.update { it.copy( - source = currentSettings.source, libraries = libraries, rows = currentSettings.rows, ) @@ -157,7 +134,7 @@ class HomeSettingsViewModel viewModelScope.async(WholphinDispatchers.IO) { semaphore.withPermit { try { - homeDataService.fetchDataForRow( + homeSettingsService.fetchDataForRow( row = row.config, scope = viewModelScope, prefs = prefs, @@ -519,7 +496,7 @@ class HomeSettingsViewModel } } - fun saveToRemote(): Job = + fun saveToRemote() { viewModelScope.launchIO { serverRepository.currentUser?.let { user -> Timber.d("Saving home settings to remote") @@ -529,10 +506,6 @@ class HomeSettingsViewModel try { Timber.d("saveToRemote") homeSettingsService.saveToServer(user.id, settings) - homeSettingsService.updateCurrent( - HomePageSettingsSource.SERVER_PROFILE, - settings, - ) showSaveToast() } catch (ex: Exception) { Timber.e(ex) @@ -540,6 +513,7 @@ class HomeSettingsViewModel } } } + } fun loadFromRemote() { viewModelScope.launchIO { @@ -556,10 +530,7 @@ class HomeSettingsViewModel } idCounter = newRows.maxOfOrNull { it.id }?.plus(1) ?: 0 _state.update { - it.copy( - source = HomePageSettingsSource.SERVER_PROFILE, - rows = newRows, - ) + it.copy(rows = newRows) } } else { Timber.v("No remote settings") @@ -589,10 +560,7 @@ class HomeSettingsViewModel Timber.v("Got web settings") idCounter = result.rows.maxOfOrNull { it.id }?.plus(1) ?: 0 _state.update { - it.copy( - source = HomePageSettingsSource.LOCAL, - rows = result.rows, - ) + it.copy(rows = result.rows) } } else { Timber.v("No web settings") @@ -607,19 +575,34 @@ class HomeSettingsViewModel } } - fun saveToLocal(): Job = - viewModelScope.launchIO { + fun saveToLocal() { + // This uses injected ioScope so that it will still run when the page is closing + ioScope.launchIO { serverRepository.currentUser?.let { user -> val rows = state.value.rows.map { it.config } val settings = HomePageSettings(rows = rows, SUPPORTED_HOME_PAGE_SETTINGS_VERSION) try { Timber.d("saveToLocal") + // Only save if there are changes based on original source + val shouldSave = + if (originalLocalSettings != null) { + originalLocalSettings != settings + } else if (originalRemoteSettings != null) { + originalRemoteSettings != settings + } else { + true + } + if (shouldSave) { + homeSettingsService.saveToLocal(user.id, settings) + homeSettingsService.updateCurrent(user.id, settings) + showSaveToast() + } else { + Timber.d("No changes") + } + } catch (ex: UnsupportedHomeSettingsVersionException) { + Timber.w(ex, "Overwriting local settings") homeSettingsService.saveToLocal(user.id, settings) - homeSettingsService.updateCurrent( - HomePageSettingsSource.LOCAL, - settings, - ) showSaveToast() } catch (ex: Exception) { Timber.e(ex) @@ -627,11 +610,17 @@ class HomeSettingsViewModel } } } + } private fun updateState(update: (HomePageSettingsState) -> HomePageSettingsState) { _state.update { update.invoke(it) } + serverRepository.currentUser?.id?.let { userId -> + homeSettingsService.currentSettings.update { + HomePageResolvedSettings(userId, state.value.rows) + } + } } fun resizeCards(relative: Int) { @@ -660,32 +649,12 @@ class HomeSettingsViewModel fun resetToDefault() = viewModelScope.launchIO { - val user = serverRepository.currentUser ?: return@launchIO + val userId = serverRepository.currentUser?.id ?: return@launchIO _state.update { it.copy(loading = LoadingState.Loading) } - val result = - if (serverPluginActive.value) { - try { - val settings = - homeSettingsService.fetchSettingsFor( - user.id, - HomePageSettingsSource.PLUGIN, - ) - settings - } catch (ex: Exception) { - Timber.e(ex, "Plugin is active, but error fetching home settings") - null - } - } else { - null - } ?: homeSettingsService.createDefault(user.id) - + val result = homeSettingsService.createDefault(userId) idCounter = result.rows.maxOfOrNull { it.id }?.plus(1) ?: 0 _state.update { - it.copy( - source = result.source, - rows = result.rows, - rowData = emptyList(), - ) + it.copy(rows = result.rows, rowData = emptyList()) } fetchRowData() } @@ -777,10 +746,6 @@ class HomeSettingsViewModel is HomeRowConfig.TvChannels -> { it.config.updateViewOptions(preset.liveTv) } - - is HomeRowConfig.CustomEndpoint -> { - it.config - } } it.copy(config = newConfig) } @@ -802,43 +767,6 @@ class HomeSettingsViewModel } } - fun loadFromRemotePlugin() { - viewModelScope.launchIO { - Timber.d("Loading home settings from server plugin") - try { - // TODO should this remove local settings? - // TODO how to set up priorities - _state.update { it.copy(loading = LoadingState.Loading) } - val result = serverPluginApi.fetchHomePageSettings() - if (result != null) { - Timber.v("Got remote settings") - val newRows = - result.rows.mapIndexed { index, config -> - homeSettingsService.resolve(index, config) - } - idCounter = newRows.maxOfOrNull { it.id }?.plus(1) ?: 0 - _state.update { - it.copy( - source = HomePageSettingsSource.PLUGIN, - rows = newRows, - ) - } - } else { - Timber.v("No server plugin settings") - showToast(context, "No server plugin settings found") - } - fetchRowData() - } catch (ex: UnsupportedHomeSettingsVersionException) { - // TODO - Timber.w(ex) - showToast(context, "Error: ${ex.localizedMessage}") - } catch (ex: Exception) { - Timber.e(ex) - showToast(context, "Error: ${ex.localizedMessage}") - } - } - } - fun onConfigAction( row: HomeRowConfigDisplay, action: HomeRowConfigAction, @@ -933,13 +861,10 @@ class HomeSettingsViewModel fetchRowData() } } - - fun discardChanges(): Job = Job().apply { complete() } } data class HomePageSettingsState( val loading: LoadingState, - val source: HomePageSettingsSource, val rows: List, val rowData: List, val libraries: List, @@ -948,7 +873,6 @@ data class HomePageSettingsState( val EMPTY = HomePageSettingsState( LoadingState.Pending, - HomePageSettingsSource.UNSET, emptyList(), emptyList(), emptyList(), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/SaveHomeSettingsDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/SaveHomeSettingsDialog.kt deleted file mode 100644 index c7531b6d..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/SaveHomeSettingsDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.damontecres.wholphin.ui.main.settings - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalResources -import androidx.compose.ui.unit.dp -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.ui.components.DialogItem -import com.github.damontecres.wholphin.ui.components.DialogParams -import com.github.damontecres.wholphin.ui.components.DialogPopup - -@Composable -fun SaveHomeSettingsDialog( - onDismissRequest: () -> Unit, - onSaveLocal: () -> Unit, - onSaveServer: () -> Unit, - onDiscard: () -> Unit, -) { - val resources = LocalResources.current - val params = - remember(resources) { - DialogParams( - fromLongClick = false, - title = resources.getString(R.string.save), - items = - listOf( - DialogItem( - text = R.string.save_to_local, - iconStringRes = R.string.fa_cloud_arrow_down, - onClick = onSaveLocal, - ), - DialogItem( - text = R.string.save_to_server, - iconStringRes = R.string.fa_cloud_arrow_up, - onClick = onSaveServer, - ), - DialogItem( - text = "Discard", - icon = Icons.Default.Delete, - iconColor = Color.Red, - onClick = onDiscard, - ), - ), - ) - } - DialogPopup( - params = params, - onDismissRequest = onDismissRequest, - elevation = 3.dp, - ) -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 5bef2675..a65c97e8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -35,9 +35,6 @@ import java.util.UUID sealed class Destination( val fullScreen: Boolean = false, ) : NavKey { - @Serializable - data class Wolfskin(val route: String) : Destination(false) - @Serializable data class Home( val id: Long = 0L, @@ -137,7 +134,7 @@ sealed class Destination( @Serializable data class Slideshow( - val parentId: UUID?, + val parentId: UUID, val index: Int, val filter: CollectionFolderFilter, val sortAndDirection: SortAndDirection, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 3e449875..63789d6c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -69,10 +69,6 @@ fun DestinationContent( LaunchedEffect(Unit) { onClearBackdrop.invoke() } } when (destination) { - is Destination.Wolfskin -> { - org.justinvisser.wolfskin.tv.WolfskinDestinationContent(destination.route, modifier) - } - is Destination.Home -> { HomePage( preferences = preferences, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 100b6caf..17906fe2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -80,7 +80,6 @@ import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupNavigationManager import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.components.TimeDisplay -import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption @@ -574,7 +573,6 @@ fun NavigationDrawerScope.ProfileIcon( modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { - val focused by interactionSource.collectIsFocusedAsState() NavigationDrawerItem( modifier = modifier, selected = false, @@ -597,7 +595,7 @@ fun NavigationDrawerScope.ProfileIcon( interactionSource = interactionSource, ) { Text( - modifier = Modifier.enableMarquee(focused), + modifier = Modifier, text = user.name ?: user.id.toString(), maxLines = 1, ) @@ -641,7 +639,7 @@ fun NavigationDrawerScope.IconNavItem( interactionSource = interactionSource, ) { Text( - modifier = Modifier.enableMarquee(focused), + modifier = Modifier, text = text, maxLines = 1, ) @@ -735,7 +733,7 @@ fun NavigationDrawerScope.NavItem( interactionSource = interactionSource, ) { Text( - modifier = Modifier.enableMarquee(focused), + modifier = Modifier, text = library.name(context), maxLines = 1, ) @@ -771,6 +769,7 @@ fun navItemColor( when (theme) { AppThemeColors.UNRECOGNIZED, AppThemeColors.PURPLE, + AppThemeColors.WOLFSKIN, AppThemeColors.BLUE, AppThemeColors.GREEN, AppThemeColors.ORANGE, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt index b17036b8..871264c2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt @@ -25,7 +25,6 @@ import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.PlaylistItem -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PlaylistCreationResult @@ -159,11 +158,7 @@ class PlayExternalViewModel is PlaylistItem.Media -> playlistItem.item } val mediaSource = streamChoiceService.chooseSource(item.data, playbackConfig) - val subtitleStc = - streamChoiceService.getSeriesTrackChoices( - item.data, - SeriesTrackChoiceType.SUBTITLE, - ) + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) if (mediaSource == null) { Timber.w("Media source is null") return@launchDefault @@ -173,14 +168,13 @@ class PlayExternalViewModel val subtitleIndex = streamChoiceService .chooseSubtitleStream( - item = item.data, source = mediaSource, audioStream = null, + seriesId = item.data.seriesId, itemPlayback = playbackConfig, - stc = subtitleStc, + plc = plc, prefs = prefs, - )?.stream - ?.index + )?.index val externalSubtitles = mediaSource.mediaStreams ?.filter { it.isExternal } @@ -243,8 +237,6 @@ class PlayExternalViewModel } // VLC - // A zero position alone does not override VLC's saved resume position. - putExtra("from_start", positionMs == 0L) if (subtitleUrls.isNotEmpty()) { putExtra("subtitles_location", subtitleUrls.first().toString()) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt index 3165246f..491498cb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt @@ -42,10 +42,8 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.TrackIndex -import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent -import com.github.damontecres.wholphin.ui.formatMaxBitrate import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialog import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialogItem @@ -62,7 +60,6 @@ enum class PlaybackDialogType { PLAYBACK_SPEED, VIDEO_SCALE, SUBTITLE_DELAY, - BITRATE, } data class PlaybackSettings( @@ -76,7 +73,6 @@ data class PlaybackSettings( val subtitleDelay: Duration, val hasSubtitleDownloadPermission: Boolean, val playbackSpeedEnabled: Boolean, - val maxBitrate: Long, ) /** @@ -166,13 +162,6 @@ fun PlaybackDialog( ), ) } - add( - BottomDialogItem( - data = PlaybackDialogType.BITRATE, - headline = stringResource(R.string.max_bitrate), - supporting = remember(settings.maxBitrate) { formatMaxBitrate(settings.maxBitrate) }, - ), - ) add( BottomDialogItem( data = PlaybackDialogType.DEBUG, @@ -282,29 +271,6 @@ fun PlaybackDialog( } } } - - PlaybackDialogType.BITRATE -> { - val choices = - AppPreference.bitrateValues.map { bitrate -> - BottomDialogItem( - data = bitrate, - headline = formatMaxBitrate(bitrate), - supporting = null, - ) - } - BottomDialog( - choices = choices, - currentChoice = choices.firstOrNull { it.data == settings.maxBitrate }, - onDismissRequest = { - onControllerInteraction.invoke() - onDismissRequest.invoke() - }, - onSelectChoice = { _, choice -> - onPlaybackActionClick.invoke(PlaybackAction.ChangeBitrate(choice.data)) - }, - gravity = leftGravity, - ) - } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index 86d64651..483083f2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -322,10 +322,6 @@ fun PlaybackPageContent( player.seekToPrevious() } } - - is PlaybackAction.ChangeBitrate -> { - viewModel.changeMaxBitrate(it.maxBitrate) - } } } @@ -756,7 +752,6 @@ fun PlaybackPageContent( // TODO Passing through audio prevents changing playback speed // See https://github.com/damontecres/Wholphin/issues/164 playbackSpeedEnabled = playerBackend == PlayerBackend.MPV || state.currentPlayback?.audioDecoder != null, - maxBitrate = state.maxBitrate, ), onDismissRequest = { playbackDialog = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt index cca3a0d0..b42b7151 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt @@ -24,7 +24,6 @@ data class PlaybackState( val nextUp: BaseItem? = null, val playlistIndex: Int = 0, val playlist: Playlist = Playlist(emptyList()), - val maxBitrate: Long = -1L, ) { val hasNext: Boolean get() = (playlistIndex + 1) < playlist.items.size val hasPrevious: Boolean get() = playlistIndex > 0 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 24bd8ee9..012038a8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -5,7 +5,6 @@ import android.media.MediaCodecList import android.os.Build import android.widget.Toast import androidx.annotation.OptIn -import androidx.annotation.VisibleForTesting import androidx.compose.ui.unit.Density import androidx.core.net.toUri import androidx.lifecycle.ViewModel @@ -35,7 +34,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.PlaylistItem -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.mpv.MpvPlayer import com.github.damontecres.wholphin.preferences.AppPreference @@ -200,7 +198,7 @@ class PlaybackViewModel val currentUserDto = serverRepository.currentUserDtoFlow - @VisibleForTesting + // Exposed for testing val initJob: Job init { @@ -292,16 +290,8 @@ class PlaybackViewModel */ private suspend fun init() { musicService.stop() + _state.update { it.copy(nextUp = null) } this.preferences = userPreferencesService.getCurrent() - val maxBitrate = - preferences.appPreferences.playbackPreferences.maxBitrate - .takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE - _state.update { - it.copy( - nextUp = null, - maxBitrate = maxBitrate, - ) - } if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) { addCloseable { refreshRateService.resetRefreshRate() } } @@ -499,16 +489,7 @@ class PlaybackViewModel null } - val audioStc = - streamChoiceService.getSeriesTrackChoices( - base, - SeriesTrackChoiceType.AUDIO, - ) - val subtitleStc = - streamChoiceService.getSeriesTrackChoices( - base, - SeriesTrackChoiceType.AUDIO, - ) + val plc = streamChoiceService.getPlaybackLanguageChoice(base) if (mediaSource == null && !isLiveTv) { showToast( @@ -537,31 +518,30 @@ class PlaybackViewModel val subtitleStreams = mediaSource?.let { getSubtitleStreams(mediaSource) }.orEmpty() val audioStreams = mediaSource?.let { getAudioStreams(mediaSource) }.orEmpty() - val audioResult = + val audioStream = mediaSource?.let { streamChoiceService .chooseAudioStream( source = mediaSource, - item = base, + seriesId = base.seriesId, itemPlayback = itemPlayback, - stc = audioStc, + plc = plc, prefs = preferences, ) } - val audioIndex = audioResult?.stream?.index + val audioIndex = audioStream?.index val subtitleIndex = mediaSource?.let { streamChoiceService .chooseSubtitleStream( - item = base, source = mediaSource, - audioStream = audioResult?.stream, + audioStream = audioStream, + seriesId = base.seriesId, itemPlayback = itemPlayback, - stc = subtitleStc, + plc = plc, prefs = preferences, - )?.stream - ?.index + )?.index } Timber.d( "Selected mediaSource=%s, audioIndex=%s, subtitleIndex=%s", @@ -636,7 +616,6 @@ class PlaybackViewModel positionMs: Long = 0, enableDirectPlay: Boolean = !this.forceTranscoding, enableDirectStream: Boolean = !this.forceTranscoding, - maxBitrate: Long = this.state.value.maxBitrate, ): Unit = withContext(WholphinDispatchers.IO) { val itemId = item.id @@ -647,8 +626,7 @@ class PlaybackViewModel state.value.currentPlayback?.let { currentPlayback -> if (currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY && - enableDirectPlay && - state.value.maxBitrate == maxBitrate + enableDirectPlay ) { val wasSuccessful = changeStreamsDirectPlay( @@ -661,17 +639,18 @@ class PlaybackViewModel } Timber.i( - "changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s, maxBitrate=%s", + "changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s", itemId, audioIndex, subtitleIndex, enableDirectPlay, enableDirectStream, positionMs, - maxBitrate, ) - _state.update { it.copy(maxBitrate = maxBitrate) } + val maxBitrate = + preferences.appPreferences.playbackPreferences.maxBitrate + .takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE val response by api.mediaInfoApi .getPostedPlaybackInfo( @@ -717,7 +696,6 @@ class PlaybackViewModel api.videosApi.getVideoStreamUrl( itemId = itemId, mediaSourceId = source.id, - liveStreamId = source.liveStreamId, static = true, tag = source.eTag, playSessionId = response.playSessionId, @@ -977,7 +955,7 @@ class PlaybackViewModel streamChoiceService.resolveSubtitleIndex( source = currentPlayback.mediaSourceInfo, audioStreamIndex = index, - item = currentItem.item.data, + seriesId = currentItem.item.data.seriesId, subtitleIndex = currentPlayback.subtitleIndex, prefs = preferences, ) @@ -1013,7 +991,7 @@ class PlaybackViewModel streamChoiceService.resolveSubtitleIndex( source = currentPlayback.mediaSourceInfo, audioStreamIndex = currentPlayback.audioIndex, - item = currentItem.item.data, + seriesId = currentItem.item.data.seriesId, subtitleIndex = index, prefs = preferences, ) @@ -1031,26 +1009,6 @@ class PlaybackViewModel } } - fun changeMaxBitrate(newMaxBitrate: Long) { - viewModelScope.launchIO { - val currentPlayback = state.value.currentPlayback - if (currentPlayback != null) { - Timber.d("Changing bitrate to %s", newMaxBitrate) - changeStreams( - item = currentItem.item, - sourceId = currentPlayback.mediaSourceInfo.id, - audioIndex = currentPlayback.audioIndex, - subtitleIndex = currentPlayback.subtitleIndex, - positionMs = onMain { player.currentPosition }, - enableDirectPlay = true, - maxBitrate = newMaxBitrate, - ) - } else { - Timber.w("Trying to change bitrate, but currentPlayback is null") - } - } - } - internal suspend fun saveTrackSelection( trackIndex: Int, type: MediaStreamType, @@ -1152,7 +1110,7 @@ class PlaybackViewModel } // Variables for tracking segment state - @VisibleForTesting + // Exposed for testing internal var segmentJob: Job? = null private val autoSkippedSegments = mutableSetOf() private val outroShownSegments = mutableSetOf() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt index 45fcdd4c..ba77d8b7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.shape.RoundedCornerShape @@ -123,10 +122,6 @@ sealed interface PlaybackAction { data object Previous : PlaybackAction data object Next : PlaybackAction - - data class ChangeBitrate( - val maxBitrate: Long, - ) : PlaybackAction } /** @@ -566,19 +561,12 @@ fun BottomDialog( gravity: Int, currentChoice: BottomDialogItem? = null, ) { - val initialIndex = - remember(choices, currentChoice) { choices.indexOfFirstOrNull { it == currentChoice } } ?: 0 - val listState = - rememberLazyListState( - initialFirstVisibleItemIndex = initialIndex, - ) val focusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } } - val bringIntoViewRequesters = - remember(choices.size) { List(choices.size) { BringIntoViewRequester() } } if (currentChoice != null) { LaunchedEffect(Unit) { - bringIntoViewRequesters.getOrNull(initialIndex)?.bringIntoView() - focusRequesters.getOrNull(initialIndex)?.tryRequestFocus() + choices.indexOfFirstOrNull { it == currentChoice }?.let { + focusRequesters.getOrNull(it)?.tryRequestFocus() + } } } // TODO enforcing a width ends up ignore the gravity @@ -603,7 +591,6 @@ fun BottomDialog( ), ) { LazyColumn( - state = listState, modifier = Modifier .fillMaxWidth() @@ -637,10 +624,7 @@ fun BottomDialog( } }, interactionSource = interactionSource, - modifier = - Modifier - .bringIntoViewRequester(bringIntoViewRequesters[index]) - .focusRequester(focusRequesters[index]), + modifier = Modifier.focusRequester(focusRequesters[index]), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt index 81f4833b..0a843dcb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt @@ -1,13 +1,6 @@ package com.github.damontecres.wholphin.ui.preferences import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.res.stringResource -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.SeerrAuthMethod -import com.github.damontecres.wholphin.data.model.SeerrServer -import com.github.damontecres.wholphin.data.model.SeerrUser import com.github.damontecres.wholphin.preferences.AppPreference import kotlinx.serialization.Serializable @@ -45,53 +38,9 @@ enum class PreferenceScreenOption { SCREENSAVER, SKIP_SEGMENTS, EXPERIMENTAL, - HDR_FORMATS, ; companion object { fun fromString(name: String?) = entries.firstOrNull { it.name == name } ?: BASIC } } - -@Composable -fun rememberSeerrDialogBody( - user: SeerrUser, - server: SeerrServer, - pluginProvided: Boolean, - ex: Exception? = null, -): String { - val username = - when (user.authMethod) { - SeerrAuthMethod.LOCAL, - SeerrAuthMethod.JELLYFIN, - -> user.username ?: stringResource(R.string.unknown) - - SeerrAuthMethod.API_KEY -> stringResource(R.string.api_key) - } - val method = - when (user.authMethod) { - SeerrAuthMethod.LOCAL -> R.string.seerr_local_user - SeerrAuthMethod.JELLYFIN -> R.string.seerr_jellyfin_user - SeerrAuthMethod.API_KEY -> 0 - }.let { - if (it != 0) " (${stringResource(it)})" else "" - } - - val body = - """ - ${stringResource(R.string.url)}: ${server.url} - ${stringResource(R.string.username)}: $username$method - """.trimIndent() + - if (pluginProvided) { - "\n\n${stringResource(R.string.managed_by_server)}" - } else { - "" - } + - if (ex != null) { - "\n\n${stringResource(R.string.error)}: ${ex.localizedMessage}" - } else { - "" - } - - return remember { body } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index 0e388213..a21405c2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -36,12 +36,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation @@ -53,7 +51,6 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppSwitchPreference import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences import com.github.damontecres.wholphin.preferences.ExperimentalPreference -import com.github.damontecres.wholphin.preferences.HdrFormatPreferences import com.github.damontecres.wholphin.preferences.MpvPreferences import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.ScreensaverPreference @@ -77,11 +74,9 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.playOnClickSound import com.github.damontecres.wholphin.ui.playSoundOnFocus import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings -import com.github.damontecres.wholphin.ui.serverSupportedMessage import com.github.damontecres.wholphin.ui.setup.ReleaseNotes import com.github.damontecres.wholphin.ui.setup.UpdateViewModel -import com.github.damontecres.wholphin.ui.setup.seerr.SeerrDialog -import com.github.damontecres.wholphin.ui.setup.seerr.SeerrDialogMode +import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.tryRequestFocus @@ -103,7 +98,6 @@ fun PreferencesContent( onFocus: (Int, Int) -> Unit = { _, _ -> }, ) { val context = LocalContext.current - val resources = LocalResources.current val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } val firstFocusRequester = remember { FocusRequester() } @@ -116,11 +110,6 @@ fun PreferencesContent( var showPinFlow by remember { mutableStateOf(false) } var showVersionDialog by remember { mutableStateOf(false) } val players by viewModel.externalPlayers.collectAsState() - val serverSupported by viewModel.serverSupported.collectAsState() - val serverVersionMessage = - remember(resources, serverSupported) { - resources.serverSupportedMessage(serverSupported.second, serverSupported.first) - } var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) } val seerrConnection by viewModel.seerrConnection.collectAsState() @@ -166,7 +155,6 @@ fun PreferencesContent( PreferenceScreenOption.SCREENSAVER -> screensaverPreferences PreferenceScreenOption.SKIP_SEGMENTS -> SkipSegmentPreferences PreferenceScreenOption.EXPERIMENTAL -> experimentalPreferences - PreferenceScreenOption.HDR_FORMATS -> HdrFormatPreferences } val screenTitle = when (preferenceScreenOption) { @@ -177,7 +165,6 @@ fun PreferencesContent( PreferenceScreenOption.SCREENSAVER -> R.string.screensaver_settings PreferenceScreenOption.SKIP_SEGMENTS -> R.string.skip_behavior PreferenceScreenOption.EXPERIMENTAL -> R.string.experimental_settings - PreferenceScreenOption.HDR_FORMATS -> R.string.hdr_formats } var visible by remember { mutableStateOf(false) } @@ -237,19 +224,6 @@ fun PreferencesContent( contentPadding = PaddingValues(16.dp), modifier = Modifier.fillMaxSize(), ) { - serverVersionMessage?.let { message -> - item { - ListItem( - selected = false, - enabled = true, - onClick = {}, - headlineContent = { - PreferenceTitle(message, color = MaterialTheme.colorScheme.error) - }, - modifier = Modifier, - ) - } - } if (showUpdate) { item { val interactionSource = remember { MutableInteractionSource() } @@ -285,7 +259,6 @@ fun PreferencesContent( ) } } - prefList.forEachIndexed { groupIndex, group -> item { Text( @@ -502,11 +475,10 @@ fun PreferencesContent( title = stringResource(pref.title), onClick = { seerrDialogMode = - when (val conn = seerrConnection.connection) { + when (val conn = seerrConnection) { is SeerrConnectionStatus.Error -> { SeerrDialogMode.Error( - conn.server, - conn.user, + conn.server.url, conn.ex, ) } @@ -517,20 +489,22 @@ fun PreferencesContent( is SeerrConnectionStatus.Success -> { SeerrDialogMode.Remove( - conn.current, + conn.current.server.url, ) } } }, modifier = focusModifier, summary = - when (seerrConnection.connection) { + when (seerrConnection) { is SeerrConnectionStatus.Error -> { stringResource(R.string.voice_error_server) } SeerrConnectionStatus.NotConfigured -> { - stringResource(R.string.add_server) + stringResource( + R.string.add_server, + ) } is SeerrConnectionStatus.Success -> { @@ -725,14 +699,63 @@ fun PreferencesContent( ) } } - } + when (val mode = seerrDialogMode) { + is SeerrDialogMode.Remove -> { + ConfirmDialog( + title = stringResource(R.string.remove_seerr_server), + body = mode.serverUrl, + onCancel = { seerrDialogMode = SeerrDialogMode.None }, + onConfirm = { + seerrVm.removeServer() + seerrDialogMode = SeerrDialogMode.None + }, + ) + } - SeerrDialog( - seerrDialogMode = seerrDialogMode, - seerrConnection = seerrConnection, - onDismissRequest = { seerrDialogMode = SeerrDialogMode.None }, - seerrVm = seerrVm, - ) + SeerrDialogMode.Add -> { + val currentUser by seerrVm.currentUser.collectAsState(null) + val status by seerrVm.serverConnectionStatus.collectAsState(LoadingState.Pending) + val serverAddedMessage = stringResource(R.string.seerr_server_added) + LaunchedEffect(status) { + if (status == LoadingState.Success) { + Toast.makeText(context, serverAddedMessage, Toast.LENGTH_SHORT).show() + seerrDialogMode = SeerrDialogMode.None + } + } + AddSeerServerDialog( + currentUsername = currentUser?.name, + status = status, + onSubmit = seerrVm::submitServer, + onResetStatus = seerrVm::resetStatus, + onDismissRequest = { seerrDialogMode = SeerrDialogMode.None }, + ) + } + + is SeerrDialogMode.Error -> { + val errorStr = stringResource(R.string.voice_error_server) + val body = + remember(mode) { + """ + ${mode.serverUrl} + + $errorStr: ${mode.ex.localizedMessage} + """.trimIndent() + } + ConfirmDialog( + title = stringResource(R.string.remove_seerr_server), + body = body, + onCancel = { seerrDialogMode = SeerrDialogMode.None }, + onConfirm = { + seerrVm.removeServer() + seerrDialogMode = SeerrDialogMode.None + }, + bodyColor = MaterialTheme.colorScheme.error, + ) + } + + SeerrDialogMode.None -> {} + } + } if (showQuickConnectDialog) { val quickConnectStatus by viewModel.quickConnectStatus.collectAsState(LoadingState.Pending) @@ -815,7 +838,6 @@ fun PreferencesPage( PreferenceScreenOption.SCREENSAVER, PreferenceScreenOption.SKIP_SEGMENTS, PreferenceScreenOption.EXPERIMENTAL, - PreferenceScreenOption.HDR_FORMATS, -> { PreferencesContent( initialPreferences, @@ -835,3 +857,18 @@ data class CacheUsage( val imageMemoryMax: Long, val imageDiskUsed: Long, ) + +private sealed interface SeerrDialogMode { + data object None : SeerrDialogMode + + data object Add : SeerrDialogMode + + data class Remove( + val serverUrl: String, + ) : SeerrDialogMode + + data class Error( + val serverUrl: String, + val ex: Exception, + ) : SeerrDialogMode +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt index 2e0ce392..200bac4e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt @@ -12,7 +12,6 @@ import androidx.datastore.core.DataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.WholphinApplication import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.AppPreferences @@ -26,7 +25,6 @@ import com.github.damontecres.wholphin.services.SeerrServerRepository import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.setup.ServerVersionSupported import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState @@ -35,7 +33,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import org.jellyfin.sdk.api.client.ApiClient @@ -79,23 +76,6 @@ class PreferencesViewModel val externalPlayers = MutableStateFlow>(emptyList()) - val serverSupported = - serverRepository.currentServerFlow - .map { - val serverVersion = it?.serverVersion - serverVersion to - when { - serverVersion == null -> ServerVersionSupported.NOT_SUPPORTED - serverVersion < WholphinApplication.minimumServerVersion -> ServerVersionSupported.NOT_SUPPORTED - serverVersion < WholphinApplication.newMinimumServerVersion -> ServerVersionSupported.NOT_SUPPORTED_IN_FUTURE - else -> ServerVersionSupported.SUPPORTED - } - }.stateIn( - viewModelScope, - SharingStarted.Eagerly, - null to ServerVersionSupported.SUPPORTED, - ) - init { viewModelScope.launchIO { val fakeIntent = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/SwitchPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/SwitchPreference.kt index daf6fa07..43e213ea 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/SwitchPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/SwitchPreference.kt @@ -70,6 +70,7 @@ fun SwitchColors(): SwitchColors { return when (theme) { AppThemeColors.UNRECOGNIZED, AppThemeColors.PURPLE, + AppThemeColors.WOLFSKIN, AppThemeColors.BLUE, -> { SwitchDefaults.colors() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt index bd14c38c..81cc7c2a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt @@ -32,7 +32,6 @@ import androidx.tv.material3.Surface import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.components.CircularProgress import org.jellyfin.sdk.model.api.PublicSystemInfo @@ -178,12 +177,7 @@ fun ServerIconCard( Icon( imageVector = Icons.Default.Warning, contentDescription = (connectionStatus as? ServerConnectionStatus.Error)?.message, - tint = - if (serverVersionSupported == ServerVersionSupported.NOT_SUPPORTED_IN_FUTURE) { - AppColors.GoldenYellow - } else { - MaterialTheme.colorScheme.errorContainer - }, + tint = MaterialTheme.colorScheme.errorContainer, modifier = Modifier.size(cardSize * 0.3f), ) Text( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt index dd972f9b..c25fe776 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt @@ -11,7 +11,6 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -67,12 +66,11 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.TextButton import com.github.damontecres.wholphin.ui.dimAndBlur +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.rememberInt -import com.github.damontecres.wholphin.ui.serverSupportedMessage import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.LoadingState -import org.jellyfin.sdk.model.ServerVersion import timber.log.Timber @Composable @@ -103,8 +101,6 @@ private fun SwitchServerContentInternal( modifier: Modifier = Modifier, ) { val resources = LocalResources.current - val focusRequesters = - remember(state.servers.size) { List(state.servers.size + 1) { FocusRequester() } } var showAddServer by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(null) } var focusedIndex by rememberInt(0) @@ -148,13 +144,10 @@ private fun SwitchServerContentInternal( val firstServerFocus = remember { FocusRequester() } LaunchedEffect(state.loading) { val state = state - when (state.loading) { - is LoadingState.Error, - LoadingState.Loading, - LoadingState.Pending, - -> Unit - - LoadingState.Success -> focusRequesters.firstOrNull()?.tryRequestFocus() + if (state.loading == LoadingState.Success && state.servers.isNotEmpty()) { + firstServerFocus.tryRequestFocus() + } else if (state.loading == LoadingState.Pending) { + firstServerFocus.tryRequestFocus() } } LazyRow( @@ -195,7 +188,10 @@ private fun SwitchServerContentInternal( Modifier .onFocusChanged { if (it.isFocused) focusedIndex = index - }.focusRequester(focusRequesters[index]), + }.ifElse( + index == 0, + Modifier.focusRequester(firstServerFocus), + ), ) } // Add Server card - always rightmost @@ -206,7 +202,10 @@ private fun SwitchServerContentInternal( Modifier .onFocusChanged { if (it.isFocused) focusedIndex = -1 - }.focusRequester(focusRequesters.last()), + }.ifElse( + state.servers.isEmpty(), + Modifier.focusRequester(firstServerFocus), + ), ) } } @@ -236,7 +235,20 @@ private fun SwitchServerContentInternal( .fillMaxWidth() .align(Alignment.BottomCenter), ) { message -> - SwitchErrorMessage(message ?: "") + + Text( + text = message ?: "", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 32.dp) + .align(Alignment.BottomCenter), + ) } // Delete server dialog @@ -315,12 +327,12 @@ private fun AddServerDialog( } } - val buttonFocusRequester = remember { FocusRequester() } + val firstDiscoveredServerFocusRequester = remember { FocusRequester() } // Default focus to first discovered server if available - LaunchedEffect(showEnterAddress) { - if (!showEnterAddress) { - buttonFocusRequester.tryRequestFocus() + LaunchedEffect(state.discoveredServers.isNotEmpty(), showEnterAddress) { + if (!showEnterAddress && state.discoveredServers.isNotEmpty()) { + firstDiscoveredServerFocusRequester.tryRequestFocus() } } @@ -367,8 +379,12 @@ private fun AddServerDialog( key = { state.discoveredServers[it].url }, ) { index -> val server = state.discoveredServers[index] - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { if (index == 0) focusRequester.tryRequestFocus() } + val focusRequester = + if (index == 0) { + firstDiscoveredServerFocusRequester + } else { + remember { FocusRequester() } + } ListItem( enabled = true, @@ -475,38 +491,21 @@ fun serverErrorMessage( server: ServerState?, resources: Resources, ): String? = - if (server == null || server.status == ServerConnectionStatus.Pending) { - null - } else if (server.status is ServerConnectionStatus.Success) { - resources.serverSupportedMessage( - server.versionSupported, - server.status.systemInfo.version - ?.let { ServerVersion.fromString(it) }, - ) - } else if (server.status is ServerConnectionStatus.Error) { - server.status.message - } else { - resources.serverSupportedMessage( - server.versionSupported, - null, - ) - } + when { + server?.status is ServerConnectionStatus.Error -> { + server.status.message + } -@Composable -fun BoxScope.SwitchErrorMessage(message: String?) { - message?.let { - Text( - text = message, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp, bottom = 32.dp) - .align(Alignment.BottomCenter), - ) + server?.status is ServerConnectionStatus.Success && + server.versionSupported == ServerVersionSupported.NOT_SUPPORTED -> { + resources.getString(R.string.server_version_not_supported) + ": ${server.status.systemInfo.version}" + } + + server?.versionSupported == ServerVersionSupported.NOT_SUPPORTED -> { + resources.getString(R.string.server_version_not_supported) + } + + else -> { + null + } } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt index 00beccb3..53190665 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt @@ -97,8 +97,6 @@ class SwitchServerViewModel result.systemInfo.version?.let { ServerVersion.fromString(it) } if (serverVersion == null || serverVersion < WholphinApplication.minimumServerVersion) { ServerVersionSupported.NOT_SUPPORTED - } else if (serverVersion < WholphinApplication.newMinimumServerVersion) { - ServerVersionSupported.NOT_SUPPORTED_IN_FUTURE } else { ServerVersionSupported.SUPPORTED } @@ -309,5 +307,4 @@ enum class ServerVersionSupported { SUPPORTED, NOT_SUPPORTED, UNKNOWN, - NOT_SUPPORTED_IN_FUTURE, } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt index cfe70890..86cf718d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -49,7 +50,6 @@ import com.github.damontecres.wholphin.ui.components.TextButton import com.github.damontecres.wholphin.ui.dimAndBlur import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.serverSupportedMessage import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.launch @@ -183,15 +183,37 @@ fun SwitchUserContent( modifier = Modifier.fillMaxWidth(), ) } - val resources = LocalResources.current - val message = - remember(resources, state.serverVersionSupported) { - resources.serverSupportedMessage( - state.serverVersionSupported, - state.serverVersion, + when (state.serverVersionSupported) { + ServerVersionSupported.SUPPORTED -> {} + + ServerVersionSupported.NOT_SUPPORTED, + ServerVersionSupported.UNKNOWN, + -> { + val resources = LocalResources.current + val message = + remember(resources) { + if (state.serverVersion.isNotNullOrBlank()) { + resources.getString(R.string.server_version_not_supported) + ": ${state.serverVersion}" + } else { + resources.getString(R.string.server_version_not_supported) + ": " + + resources.getString(R.string.unknown) + } + } + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 32.dp) + .align(Alignment.BottomCenter), ) } - SwitchErrorMessage(message) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index b11b0b19..e1a6dd96 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -300,18 +300,16 @@ class SwitchUserViewModel knownUsers + publicUsers } - private suspend fun checkServerVersion(): Pair { + private suspend fun checkServerVersion(): Pair { val api = jellyfin.createApi(server.url) val systemInfo by api.systemApi.getPublicSystemInfo() val serverVersion = systemInfo.version?.let { ServerVersion.fromString(it) } return if (serverVersion == null) { - serverVersion to ServerVersionSupported.UNKNOWN + systemInfo.version to ServerVersionSupported.UNKNOWN } else if (serverVersion < WholphinApplication.minimumServerVersion) { - serverVersion to ServerVersionSupported.NOT_SUPPORTED - } else if (serverVersion < WholphinApplication.newMinimumServerVersion) { - serverVersion to ServerVersionSupported.NOT_SUPPORTED_IN_FUTURE + systemInfo.version to ServerVersionSupported.NOT_SUPPORTED } else { - serverVersion to ServerVersionSupported.SUPPORTED + systemInfo.version to ServerVersionSupported.SUPPORTED } } @@ -343,7 +341,7 @@ data class SwitchUserState( // LoadingState for while adding/switching users val switchUserState: LoadingState = LoadingState.Pending, val loginAttempts: Int = 0, - val serverVersion: ServerVersion? = null, + val serverVersion: String? = null, val serverVersionSupported: ServerVersionSupported = ServerVersionSupported.UNKNOWN, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt index 5faccc05..59c9b2ae 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt @@ -42,7 +42,6 @@ import com.github.damontecres.wholphin.util.LoadingState @Composable fun AddSeerrServerApiKey( - initialUrl: String, onSubmit: (url: String, apiKey: String) -> Unit, status: LoadingState, modifier: Modifier = Modifier, @@ -56,18 +55,12 @@ fun AddSeerrServerApiKey( .padding(16.dp) .wrapContentSize(), ) { - var url by remember { mutableStateOf(seerrUrlPrefill(initialUrl)) } + var url by remember { mutableStateOf(seerrUrlPrefill()) } var apiKey by remember { mutableStateOf("") } val focusRequester = remember { FocusRequester() } val passwordFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - if (url.isBlank()) { - focusRequester.tryRequestFocus() - } else { - passwordFocusRequester.tryRequestFocus() - } - } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Text( text = stringResource(R.string.enter_url_api_key), style = MaterialTheme.typography.titleMedium, @@ -160,7 +153,6 @@ fun AddSeerrServerApiKey( @Composable fun AddSeerrServerUsername( - initialUrl: String, onSubmit: (url: String, username: String, password: String) -> Unit, username: String, status: LoadingState, @@ -175,22 +167,14 @@ fun AddSeerrServerUsername( .padding(16.dp) .wrapContentSize(), ) { - var url by remember { mutableStateOf(seerrUrlPrefill(initialUrl)) } + var url by remember { mutableStateOf(seerrUrlPrefill()) } var username by remember { mutableStateOf(username) } var password by remember { mutableStateOf("") } val focusRequester = remember { FocusRequester() } val usernameFocusRequester = remember { FocusRequester() } val passwordFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - val focusRequesterToUse = - when { - url.isNotNullOrBlank() && username.isNotNullOrBlank() -> passwordFocusRequester - url.isNotNullOrBlank() -> usernameFocusRequester - else -> focusRequester - } - focusRequesterToUse.tryRequestFocus() - } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Text( text = stringResource(R.string.username_or_password), style = MaterialTheme.typography.titleMedium, @@ -334,7 +318,6 @@ fun AddSeerrServerUsername( private fun AddSeerrServerUsernamePreview() { WholphinTheme { AddSeerrServerUsername( - initialUrl = "", onSubmit = { string: String, string1: String, string2: String -> }, username = "test", status = LoadingState.Pending, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt index 5be93b83..bb38aabb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt @@ -1,161 +1,27 @@ package com.github.damontecres.wholphin.ui.setup.seerr -import android.widget.Toast -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.SeerrAuthMethod -import com.github.damontecres.wholphin.data.model.SeerrServer -import com.github.damontecres.wholphin.data.model.SeerrUser -import com.github.damontecres.wholphin.services.CurrentSeerr -import com.github.damontecres.wholphin.services.SeerrState import com.github.damontecres.wholphin.ui.components.BasicDialog -import com.github.damontecres.wholphin.ui.components.ConfirmDialog import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogPopup -import com.github.damontecres.wholphin.ui.components.LoadingPage -import com.github.damontecres.wholphin.ui.components.ScrollableDialog -import com.github.damontecres.wholphin.ui.preferences.rememberSeerrDialogBody import com.github.damontecres.wholphin.util.LoadingState -@Composable -fun SeerrDialog( - seerrDialogMode: SeerrDialogMode, - seerrConnection: SeerrState, - onDismissRequest: () -> Unit, - seerrVm: SwitchSeerrViewModel = hiltViewModel(), -) { - val context = LocalContext.current - when (val mode = seerrDialogMode) { - is SeerrDialogMode.Remove -> { - val body = rememberSeerrDialogBody(mode.current.user, mode.current.server, seerrConnection.pluginProvided) - if (seerrConnection.pluginProvided) { - ScrollableDialog( - onDismissRequest = onDismissRequest, - ) { - item { - Text(body) - } - } - } else { - ConfirmDialog( - title = stringResource(R.string.remove_seerr_server), - body = body, - onCancel = onDismissRequest, - onConfirm = { - seerrVm.removeServer() - onDismissRequest.invoke() - }, - elevation = 3.dp, - ) - } - } - - SeerrDialogMode.Add -> { - val currentUser by seerrVm.currentUser.collectAsState(null) - val state by seerrVm.state.collectAsState() - val serverAddedMessage = stringResource(R.string.seerr_server_added) - LaunchedEffect(state.serverConnectionStatus) { - if (state.serverConnectionStatus == LoadingState.Success) { - Toast.makeText(context, serverAddedMessage, Toast.LENGTH_SHORT).show() - onDismissRequest.invoke() - } - } - when (state.loading) { - is LoadingState.Error, - LoadingState.Loading, - LoadingState.Pending, - -> { - BasicDialog( - onDismissRequest = onDismissRequest, - ) { - LoadingPage(Modifier.padding(16.dp)) - } - } - - LoadingState.Success -> { - AddSeerServerDialog( - initialUrl = state.initialUrl, - currentUsername = currentUser?.name, - status = state.serverConnectionStatus, - onSubmit = seerrVm::submitServer, - onResetStatus = seerrVm::resetStatus, - onDismissRequest = onDismissRequest, - ) - } - } - } - - is SeerrDialogMode.Error -> { - val body = rememberSeerrDialogBody(mode.user, mode.server, seerrConnection.pluginProvided, mode.ex) - if (seerrConnection.pluginProvided) { - ConfirmDialog( - title = stringResource(R.string.error), - body = body, - onCancel = onDismissRequest, - onConfirm = { - seerrVm.retryServerFromPlugin() - onDismissRequest.invoke() - }, - bodyColor = MaterialTheme.colorScheme.error, - elevation = 3.dp, - confirmStringRes = R.string.retry, - ) - } else { - ConfirmDialog( - title = stringResource(R.string.remove_seerr_server), - body = body, - onCancel = onDismissRequest, - onConfirm = { - seerrVm.removeServer() - onDismissRequest.invoke() - }, - bodyColor = MaterialTheme.colorScheme.error, - elevation = 3.dp, - ) - } - } - - SeerrDialogMode.None -> {} - } -} - -sealed interface SeerrDialogMode { - data object None : SeerrDialogMode - - data object Add : SeerrDialogMode - - data class Remove( - val current: CurrentSeerr, - ) : SeerrDialogMode - - data class Error( - val server: SeerrServer, - val user: SeerrUser, - val ex: Exception, - ) : SeerrDialogMode -} - @Composable fun AddSeerServerDialog( currentUsername: String?, - initialUrl: String?, status: LoadingState, onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit, onResetStatus: () -> Unit, @@ -176,7 +42,6 @@ fun AddSeerServerDialog( properties = DialogProperties(usePlatformDefaultWidth = false), ) { AddSeerrServerUsername( - initialUrl = initialUrl ?: "", onSubmit = { url, username, password -> onSubmit.invoke(url, username, password, auth) }, @@ -192,7 +57,6 @@ fun AddSeerServerDialog( onDismissRequest = { authMethod = null }, ) { AddSeerrServerApiKey( - initialUrl = initialUrl ?: "", onSubmit = { url, apiKey -> onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt index 2fde3eae..773f6c74 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt @@ -3,13 +3,11 @@ package com.github.damontecres.wholphin.ui.setup.seerr import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.SeerrAuthMethod import com.github.damontecres.wholphin.services.SeerrServerRepository import com.github.damontecres.wholphin.services.SeerrService -import com.github.damontecres.wholphin.services.ServerPluginApi import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.LoadingState @@ -17,7 +15,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import jakarta.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @@ -31,29 +28,11 @@ class SwitchSeerrViewModel private val seerrServerRepository: SeerrServerRepository, private val seerrService: SeerrService, private val serverRepository: ServerRepository, - private val serverPluginApi: ServerPluginApi, ) : ViewModel() { val currentUser = serverRepository.currentUserFlow val currentSeerrServer = seerrServerRepository.currentServer - private val _state = MutableStateFlow(SwitchSeerrState()) - val state: StateFlow = _state - - init { - if (serverRepository.serverPluginInstalled.value) { - viewModelScope.launchIO { - try { - serverPluginApi.fetchSeerrSettings()?.serverUrl?.let { serverUrl -> - _state.update { it.copy(initialUrl = serverUrl) } - } - } catch (ex: Exception) { - Timber.e(ex, "Error fetching seerr settings") - } finally { - _state.update { it.copy(loading = LoadingState.Success) } - } - } - } - } + val serverConnectionStatus = MutableStateFlow(LoadingState.Pending) fun submitServer( url: String, @@ -62,21 +41,13 @@ class SwitchSeerrViewModel authMethod: SeerrAuthMethod, ) { viewModelScope.launchIO { - _state.update { it.copy(serverConnectionStatus = LoadingState.Loading) } + serverConnectionStatus.update { LoadingState.Loading } val urls = try { createUrls(url) } catch (ex: IllegalArgumentException) { showToast(context, "Invalid URL") - _state.update { - it.copy( - serverConnectionStatus = - LoadingState.Error( - "Invalid URL", - ex, - ), - ) - } + serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) } return@launchIO } Timber.v("Urls to try: %s", urls) @@ -127,7 +98,7 @@ class SwitchSeerrViewModel ) } } - _state.update { it.copy(serverConnectionStatus = LoadingState.Success) } + serverConnectionStatus.update { LoadingState.Success } } else { val message = results @@ -136,7 +107,7 @@ class SwitchSeerrViewModel "$url - ${s?.localizedMessage}" }.joinToString("\n") showToast(context, "Could not connect") - _state.update { it.copy(serverConnectionStatus = LoadingState.Error(message)) } + serverConnectionStatus.update { LoadingState.Error(message) } } } } @@ -150,31 +121,11 @@ class SwitchSeerrViewModel } } - fun retryServerFromPlugin() { - viewModelScope.launchIO { - val settings = serverPluginApi.fetchSeerrSettings() - if (settings != null) { - val result = seerrServerRepository.usePluginProvidedServer(settings) - if (!result) { - showToast(context, context.getString(R.string.voice_error_server)) - } - } else { - seerrServerRepository.clear() - } - } - } - fun resetStatus() { - _state.update { it.copy(serverConnectionStatus = LoadingState.Pending) } + serverConnectionStatus.update { LoadingState.Pending } } } -data class SwitchSeerrState( - val loading: LoadingState = LoadingState.Pending, - val serverConnectionStatus: LoadingState = LoadingState.Pending, - val initialUrl: String? = null, -) - fun createUrls(url: String): List { val urls = mutableListOf() if (url.startsWith("http://") || url.startsWith("https://")) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt index 8172b049..e74ffdf2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt @@ -20,8 +20,6 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.PlayerFactory import com.github.damontecres.wholphin.services.ScreensaverService -import com.github.damontecres.wholphin.services.StreamChoiceReason -import com.github.damontecres.wholphin.services.StreamChoiceResult import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.ui.PhotoItemFields import com.github.damontecres.wholphin.ui.launchIO @@ -143,16 +141,13 @@ class SlideshowViewModel ), ) serverRepository.currentUser?.let { user -> - val filter = - slideshowSettings.parentId?.let { - playbackEffectDao - .getPlaybackEffect( - user.rowId, - slideshowSettings.parentId, - BaseItemKind.PHOTO_ALBUM, - )?.videoFilter - } + playbackEffectDao + .getPlaybackEffect( + user.rowId, + slideshowSettings.parentId, + BaseItemKind.PHOTO_ALBUM, + )?.videoFilter if (filter != null) { Timber.v("Got filter for album %s", slideshowSettings.parentId) albumImageFilter = filter @@ -234,21 +229,12 @@ class SlideshowViewModel source.mediaStreams?.firstOrNull { it.type == MediaStreamType.AUDIO } ChosenStreams( itemPlayback = null, - audioStc = emptyList(), - subtitleStc = emptyList(), + plc = null, itemId = image.id, source = source, videoStream = video, - audioStream = - StreamChoiceResult( - audio, - StreamChoiceReason.Unknown, - ), - subtitleStream = - StreamChoiceResult( - null, - StreamChoiceReason.Unknown, - ), + audioStream = audio, + subtitleStream = null, subtitlesDisabled = false, ) } @@ -444,7 +430,7 @@ class SlideshowViewModel fun saveGalleryFilter() { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { val vf = _imageFilter.value - if (slideshowSettings.parentId != null) { + if (vf != null) { albumImageFilter = vf serverRepository.currentUser?.let { user -> playbackEffectDao diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/Theme.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/Theme.kt index 8ae95b25..8cce37e5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/Theme.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/Theme.kt @@ -13,9 +13,10 @@ import com.github.damontecres.wholphin.ui.theme.colors.OledThemeColors import com.github.damontecres.wholphin.ui.theme.colors.OrangeThemeColors import com.github.damontecres.wholphin.ui.theme.colors.PurpleThemeColors import com.github.damontecres.wholphin.ui.theme.colors.RedThemeColors +import com.github.damontecres.wholphin.ui.theme.colors.WolfskinThemeColors val LocalTheme = - compositionLocalOf { AppThemeColors.PURPLE } + compositionLocalOf { AppThemeColors.WOLFSKIN } fun getThemeColors(appThemeColors: AppThemeColors): ThemeColors = when (appThemeColors) { @@ -27,13 +28,14 @@ fun getThemeColors(appThemeColors: AppThemeColors): ThemeColors = AppThemeColors.BOLD_BLUE -> BoldBlueThemeColors AppThemeColors.RED -> RedThemeColors AppThemeColors.BROWN -> BrownThemeColors - AppThemeColors.UNRECOGNIZED -> PurpleThemeColors + AppThemeColors.WOLFSKIN -> WolfskinThemeColors + AppThemeColors.UNRECOGNIZED -> WolfskinThemeColors } @Composable fun WholphinTheme( darkTheme: Boolean = true, - appThemeColors: AppThemeColors = AppThemeColors.PURPLE, + appThemeColors: AppThemeColors = AppThemeColors.WOLFSKIN, content: @Composable () -> Unit, ) { val themeColors = getThemeColors(appThemeColors) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt new file mode 100644 index 00000000..ee0a77b5 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt @@ -0,0 +1,59 @@ +package com.github.damontecres.wholphin.ui.theme.colors + +import androidx.compose.ui.graphics.Color +import com.github.damontecres.wholphin.ui.theme.ThemeColors + +/** Lilac accents on near-black surfaces, with upstream Purple available separately. */ +val WolfskinThemeColors = + object : ThemeColors { + private val background = Color(0xFF07070A) + private val lilac = Color(0xFFD2BCFF) + private val onLilac = Color(0xFF241334) + private val container = Color(0xFF382A49) + private val onContainer = Color(0xFFEADCFF) + private val surfaceVariant = Color(0xFF24202D) + + override val lightSchemeMaterial = PurpleThemeColors.lightSchemeMaterial + override val lightScheme = PurpleThemeColors.lightScheme + + override val darkSchemeMaterial = + PurpleThemeColors.darkSchemeMaterial.copy( + primary = lilac, + onPrimary = onLilac, + primaryContainer = container, + onPrimaryContainer = onContainer, + secondary = lilac, + onSecondary = onLilac, + secondaryContainer = container, + onSecondaryContainer = onContainer, + tertiary = lilac, + onTertiary = onLilac, + tertiaryContainer = container, + onTertiaryContainer = onContainer, + background = background, + surface = background, + surfaceVariant = surfaceVariant, + inversePrimary = Color(0xFF705090), + ) + + override val darkScheme = + PurpleThemeColors.darkScheme.copy( + primary = lilac, + onPrimary = onLilac, + primaryContainer = container, + onPrimaryContainer = onContainer, + secondary = lilac, + onSecondary = onLilac, + secondaryContainer = container, + onSecondaryContainer = onContainer, + tertiary = lilac, + onTertiary = onLilac, + tertiaryContainer = container, + onTertiaryContainer = onContainer, + background = background, + surface = background, + surfaceVariant = surfaceVariant, + inversePrimary = Color(0xFF705090), + border = lilac.copy(alpha = .75f), + ) + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/RandomButtonUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/RandomButtonUtils.kt deleted file mode 100644 index 31f4f6fa..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/RandomButtonUtils.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.damontecres.wholphin.ui.util - -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.CollectionFolderFilter -import com.github.damontecres.wholphin.data.model.GetItemsFilter -import com.github.damontecres.wholphin.ui.data.SortAndDirection -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.util.BlockingList -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.serializer.toUUIDOrNull -import timber.log.Timber -import kotlin.random.Random - -/** - * Navigate to a random item in the given list - */ -suspend fun goToRandom( - navigateTo: (Destination) -> Unit, - parentId: String, - filter: GetItemsFilter, - sortAndDirection: SortAndDirection, - recursive: Boolean, - list: BlockingList, -) { - val randomIndex = Random.nextInt(list.size) - val random = list.getBlocking(randomIndex) - - Timber.d("Got random item #%s: %s", randomIndex, random?.id) - if (random != null) { - val destination = - if (random.type == BaseItemKind.PHOTO) { - Destination.Slideshow( - parentId = parentId.toUUIDOrNull(), - index = randomIndex, - filter = - CollectionFolderFilter( - filter = filter, - ), - sortAndDirection = sortAndDirection, - recursive = recursive, - startSlideshow = false, - ) - } else { - random.destination() - } - navigateTo(destination) - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt index 2f98fccc..750ba711 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt @@ -22,7 +22,6 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult import org.jellyfin.sdk.model.api.GetProgramsDto -import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest import org.jellyfin.sdk.model.api.request.GetArtistsRequest import org.jellyfin.sdk.model.api.request.GetEpisodesRequest import org.jellyfin.sdk.model.api.request.GetGenresRequest @@ -402,25 +401,6 @@ object GetArtistsHandler : RequestHandler { ): Response = api.artistsApi.getArtists((request)) } -@Serializable -object GetAlbumArtistsHandler : RequestHandler { - override fun prepare( - request: GetAlbumArtistsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetAlbumArtistsRequest = - request.copy( - startIndex = startIndex, - limit = limit, - ) - - override suspend fun execute( - api: ApiClient, - request: GetAlbumArtistsRequest, - ): Response = api.artistsApi.getAlbumArtists(request) -} - val requestSerializersModule = SerializersModule { polymorphic(Any::class) { @@ -436,7 +416,6 @@ val requestSerializersModule = subclass(GetStudiosRequest::class) subclass(GetRecordingsRequest::class) subclass(GetLiveTvChannelsRequest::class) - subclass(GetArtistsRequest::class) - subclass(GetAlbumArtistsRequest::class) + subclass(GetArtistsHandler::class) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt index d83ae269..c334967f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt @@ -1,7 +1,7 @@ package com.github.damontecres.wholphin.util import android.content.Context -import com.github.damontecres.wholphin.data.MostRecentServer +import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.services.hilt.AppModule import com.github.damontecres.wholphin.services.hilt.DeviceModule import com.google.auto.service.AutoService @@ -13,7 +13,6 @@ import org.acra.sender.ReportSender import org.acra.sender.ReportSenderException import org.acra.sender.ReportSenderFactory import org.jellyfin.sdk.Jellyfin -import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.clientLogApi import org.jellyfin.sdk.api.okhttp.OkHttpFactory import org.jellyfin.sdk.createJellyfin @@ -40,10 +39,26 @@ class CrashReportSender : ReportSender { errorContent: CrashReportData, ) { Timber.v("Attempting to send crash report") - try { - val recentServer = AppModule.mostRecentServerProvider(context).get() - val api = recentServer.createApi(context) - if (api != null) { + val prefs = ServerRepository.getServerSharedPreferences(context) + val serverUrl = prefs.getString(ServerRepository.SERVER_URL_KEY, null) + val accessToken = prefs.getString(ServerRepository.ACCESS_TOKEN_KEY, null) + if (serverUrl != null && accessToken != null) { + try { + val okHttpClient = + OkHttpClient + .Builder() + .build() + val okHttpFactory = OkHttpFactory(okHttpClient) + val api = + createJellyfin { + this.context = context + clientInfo = AppModule.clientInfo(context) + deviceInfo = DeviceModule.deviceInfo(context) + apiClientFactory = okHttpFactory + socketConnectionFactory = okHttpFactory + minimumServerVersion = Jellyfin.minimumVersion + }.createApi(baseUrl = serverUrl, accessToken = accessToken) + val obj = JSONObject() for ((key, value) in errorContent.toMap()) { obj.put(key, value) @@ -60,32 +75,13 @@ class CrashReportSender : ReportSender { """.trimIndent() + jsonStr, ).content.fileName - Timber.i("Sent report to %s, filename=%s", api.baseUrl, filename) + Timber.i("Sent report to $serverUrl, filename=$filename") } - } else { - throw ReportSenderException("Could not find valid server and/or credentials to use") + } catch (ex: Exception) { + throw ReportSenderException("Exception while sending crash report", ex) } - } catch (ex: Exception) { - throw ReportSenderException("Exception while sending crash report", ex) + } else { + throw ReportSenderException("Could not find valid server and/or credentials to use") } } } - -private fun MostRecentServer.createApi(context: Context): ApiClient? = - (this as? MostRecentServer.ServerAndUser)?.let { - val okHttpClient = - OkHttpClient - .Builder() - .build() - val okHttpFactory = OkHttpFactory(okHttpClient) - val api = - createJellyfin { - this.context = context - clientInfo = AppModule.clientInfo(context) - deviceInfo = DeviceModule.deviceInfo(context) - apiClientFactory = okHttpFactory - socketConnectionFactory = okHttpFactory - minimumServerVersion = Jellyfin.minimumVersion - }.createApi(baseUrl = serverUrl, accessToken = accessToken) - api - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7Conversion.kt b/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7Conversion.kt deleted file mode 100644 index b4f8e15f..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7Conversion.kt +++ /dev/null @@ -1,71 +0,0 @@ -@file:OptIn(UnstableApi::class) - -package com.github.damontecres.wholphin.util - -import androidx.annotation.OptIn -import androidx.media3.common.Format -import androidx.media3.common.MimeTypes -import androidx.media3.common.util.UnstableApi -import com.github.damontecres.wholphin.preferences.ExperimentalPreferences -import com.github.damontecres.wholphin.preferences.enabled -import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest - -/** - * The one place which decides whether Dolby Vision profile 7 is converted to profile 8.1 on this - * device, and how a converted track is described. - * - * Profile 7 is HDR10 compatible HEVC plus two extra NAL unit types: 62 carries the RPU metadata and - * 63 the enhancement layer. A decoder which only lists profile 8 takes such a stream but does not - * switch the display into Dolby Vision for it, so the picture comes out as the HDR10 base layer. - * Rewriting the RPU into its profile 8.1 form and dropping the enhancement layer produces the - * single layer stream that decoder does switch for. Nothing is re-encoded. - * - * The conversion has two halves which have to agree: - * - [DolbyVisionProfile7Filter] rewrites the access units on their way into the decoder; - * - [DolbyVisionProfile7ExtractorsFactory] rewrites the codec string of the track, because - * media3 picks the decoder by `Format.codecs` and a decoder which does not list profile 7 would - * otherwise never be chosen for it. - * - * Both are switched on by [isWanted], as is direct play of profile 7 in the device profile. - */ -object DolbyVisionProfile7Conversion { - /** - * True when both the user has enabled the setting and this device is one it helps: its Dolby - * Vision decoder lists profile 8 but not profile 7. A device which decodes profile 7 itself - * is left alone, since it can use the enhancement layer the conversion would throw away, and - * a device without a profile 8 decoder has nothing to handle the converted stream - */ - fun isWanted( - experimental: ExperimentalPreferences, - mediaTest: MediaCodecCapabilitiesTest, - ): Boolean = - experimental.enabled { convertDolbyVisionProfile7 } && - !mediaTest.supportsHevcDolbyVisionEL() && - mediaTest.supportsHevcDolbyVisionProfile8() - - /** Whether [format] is a Dolby Vision profile 7 video track, in either sample entry form. */ - fun isProfile7(format: Format): Boolean { - val codecs = format.codecs ?: return false - return MimeTypes.VIDEO_DOLBY_VISION == format.sampleMimeType && - (codecs.startsWith(CODECS_PREFIX_DVHE_07) || codecs.startsWith(CODECS_PREFIX_DVH1_07)) - } - - /** - * The [format] as the converted stream should be advertised: `dvhe.07.06` becomes - * `dvhe.08.06`, the level and the sample entry prefix stay. Encrypted tracks are left as they - * are: their samples reach the decoder through the secure path the filter does not touch, so - * advertising them as converted would be a lie. - */ - fun advertiseAsProfile8(format: Format): Format = - if (isProfile7(format) && format.drmInitData == null) { - format.buildUpon().setCodecs(toProfile8(format.codecs!!)).build() - } else { - format - } - - /** `dvhe.07.06` โ†’ `dvhe.08.06`. The caller checks [isProfile7] first. */ - fun toProfile8(codecs: String): String = codecs.substring(0, 5) + "08" + codecs.substring(7) - - private const val CODECS_PREFIX_DVHE_07 = "dvhe.07" - private const val CODECS_PREFIX_DVH1_07 = "dvh1.07" -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ExtractorsFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ExtractorsFactory.kt deleted file mode 100644 index 632f46c3..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ExtractorsFactory.kt +++ /dev/null @@ -1,79 +0,0 @@ -@file:OptIn(markerClass = [UnstableApi::class]) - -package com.github.damontecres.wholphin.util - -import android.net.Uri -import androidx.annotation.OptIn -import androidx.media3.common.C -import androidx.media3.common.Format -import androidx.media3.common.util.UnstableApi -import androidx.media3.extractor.Extractor -import androidx.media3.extractor.ExtractorOutput -import androidx.media3.extractor.ExtractorsFactory -import androidx.media3.extractor.ForwardingExtractor -import androidx.media3.extractor.ForwardingExtractorOutput -import androidx.media3.extractor.ForwardingExtractorsFactory -import androidx.media3.extractor.ForwardingTrackOutput -import androidx.media3.extractor.TrackOutput -import timber.log.Timber - -/** - * Advertises Dolby Vision profile 7 video tracks as profile 8.1, which is what - * [DolbyVisionProfile7Filter] turns their samples into before the decoder sees them. - * - * media3 chooses the decoder by `Format.codecs`, and a decoder which lists profile 8 but not - * profile 7 answers "not supported" to `dvhe.07`; with decoder fallback on, playback then lands on - * whichever decoder comes first in the list, with a warning that the format exceeds its - * capabilities. Rewriting the codec string at the extractor routes the track to the profile 8 - * decoder, and it also sets `MediaFormat.KEY_PROFILE` right, which media3 derives from the same - * string. - * - * Only the codec string changes. Sample data is not touched here: the two convenience overloads of - * [TrackOutput.sampleData] go straight to the delegate, which is exactly what is wanted. - */ -class DolbyVisionProfile7ExtractorsFactory( - delegate: ExtractorsFactory, -) : ForwardingExtractorsFactory(delegate) { - override fun createExtractors(): Array = wrap(super.createExtractors()) - - override fun createExtractors( - uri: Uri, - responseHeaders: Map>, - ): Array = wrap(super.createExtractors(uri, responseHeaders)) - - private fun wrap(extractors: Array): Array = - Array(extractors.size) { Profile8AdvertisingExtractor(extractors[it]) } -} - -private class Profile8AdvertisingExtractor( - delegate: Extractor, -) : ForwardingExtractor(delegate) { - override fun init(output: ExtractorOutput) { - super.init(Profile8AdvertisingExtractorOutput(output)) - } -} - -private class Profile8AdvertisingExtractorOutput( - output: ExtractorOutput, -) : ForwardingExtractorOutput(output) { - override fun track( - id: Int, - type: Int, - ): TrackOutput { - val trackOutput = super.track(id, type) - return if (type == C.TRACK_TYPE_VIDEO) Profile8AdvertisingTrackOutput(trackOutput) else trackOutput - } -} - -/** Rewrites the codec string of every profile 7 format it is handed; everything else passes unchanged. */ -internal class Profile8AdvertisingTrackOutput( - delegate: TrackOutput, -) : ForwardingTrackOutput(delegate) { - override fun format(format: Format) { - val advertised = DolbyVisionProfile7Conversion.advertiseAsProfile8(format) - if (advertised !== format) { - Timber.i("Advertising Dolby Vision track %s as %s for conversion", format.codecs, advertised.codecs) - } - super.format(advertised) - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7Filter.kt b/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7Filter.kt deleted file mode 100644 index 7cf50622..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7Filter.kt +++ /dev/null @@ -1,313 +0,0 @@ -@file:OptIn(markerClass = [UnstableApi::class]) - -package com.github.damontecres.wholphin.util - -import androidx.annotation.OptIn -import androidx.media3.common.util.UnstableApi -import com.suyashbelekar.exoplayerhdrutils.libdovi.LibDovi -import timber.log.Timber -import java.nio.Buffer -import java.nio.ByteBuffer - -/** - * Rewrites the Dolby Vision RPU of one access unit, in place. The production implementation is - * libdovi, as packaged by ExoplayerHdrUtils; tests substitute their own. - * - * Both methods take a direct buffer whose access unit starts at index 0 and is [size] bytes long. - * That is the shape the native code reads: it takes the buffer's address and capacity, not its - * position and limit. - */ -interface RpuRewriter { - /** - * The Dolby Vision profile of the RPU (NAL unit type 62) in the access unit, or 0 when there is - * none it can parse. - */ - fun profileOf( - frame: ByteBuffer, - size: Int, - ): Int - - /** - * Rewrites the profile 7 RPU in the access unit into its profile 8.1 form and returns the new - * size of the access unit. The bytes after the RPU move to stay contiguous, so the buffer's - * capacity must leave room past [size] for an RPU which comes out longer than it went in. - */ - fun convertToProfile8( - frame: ByteBuffer, - size: Int, - ): Int -} - -/** [RpuRewriter] on libdovi. Constructing it loads the native library, which throws a [LinkageError] on an ABI it does not ship for. */ -class LibDoviRpuRewriter : RpuRewriter { - private val libDovi = LibDovi() - - override fun profileOf( - frame: ByteBuffer, - size: Int, - ): Int = libDovi.getFrameInfo(frame, size)?.doviProfile ?: 0 - - override fun convertToProfile8( - frame: ByteBuffer, - size: Int, - ): Int = libDovi.processHevcFrame(frame, size, DOVI_TRANSFORM_CONVERT_TO_PROFILE_8, false) - - private companion object { - /** `doviTransform` value of `LibDovi.processHevcFrame` for the profile 7 โ†’ 8.1 rewrite. */ - const val DOVI_TRANSFORM_CONVERT_TO_PROFILE_8 = 1 - } -} - -/** - * Converts Dolby Vision profile 7 access units to profile 8.1 on their way into the decoder. - * - * Profile 7 carries its RPU in NAL unit type 62 and its enhancement layer in type 63, after the - * picture's own NAL units. The conversion drops every type 63 unit and has libdovi rewrite the RPU - * into the profile 8.1 form, which is what a profile 8 decoder switches the display into Dolby - * Vision for. The enhancement layer goes because profile 8.1 is single layer by definition: a - * decoder handed a dual layer stream under that name keeps it out of the Dolby Vision pipeline and - * shows the HDR10 base layer, which is the very symptom this filter exists to fix. - * - * Every access unit is judged on its own. One without an RPU, or whose RPU is not profile 7, is - * left exactly as it came, and so is one libdovi refuses. The rewrite happens in a scratch buffer - * and is copied back only once it has succeeded and fits, so a refused access unit reaches the - * decoder untouched rather than half rewritten. - * - * The codec string of the track has to say profile 8 for the decoder to be chosen at all; that is - * [DolbyVisionProfile7ExtractorsFactory]'s job. An instance belongs to one video renderer and is - * called from its playback thread only. - */ -class DolbyVisionProfile7Filter( - private val newRewriter: () -> RpuRewriter = { LibDoviRpuRewriter() }, -) : BitstreamFilter { - private var rewriter: RpuRewriter? = null - private var rewriterUnavailable = false - - /** The access unit as it arrived, for scanning: a heap array is much faster to walk than absolute gets on a direct buffer. */ - private var staging = ByteArray(0) - - /** The access unit without its enhancement layer, where libdovi rewrites the RPU. Direct, because the native code needs its address. */ - private var scratch: ByteBuffer? = null - - private var convertedCount = 0 - private var passedThroughCount = 0 - private var refusedCount = 0 - - override fun filter( - data: ByteBuffer, - offset: Int, - size: Int, - ): Int { - if (!startsWithStartCode(data, offset, size)) { - return size - } - val rewriter = rewriter() ?: return size - - val staging = staging(size) - window(data, offset, size).get(staging, 0, size) - if (!carriesRpu(staging, size)) { - passedThroughCount++ - return size - } - - val scratch = scratch(size + RPU_GROWTH_HEADROOM_BYTES) - val kept = copyWithoutEnhancementLayer(staging, size, scratch) - val converted = - try { - if (rewriter.profileOf(scratch, kept) != DOLBY_VISION_PROFILE_7) { - passedThroughCount++ - return size - } - rewriter.convertToProfile8(scratch, kept) - } catch (e: RuntimeException) { - refuse("the RPU rewrite threw", e) - return size - } - if (converted <= 0 || converted > size) { - refuse("the RPU rewrite returned $converted bytes for a $size byte access unit", null) - return size - } - - window(data, offset, converted).put(window(scratch, 0, converted)) - convertedCount++ - return converted - } - - private fun rewriter(): RpuRewriter? { - if (rewriterUnavailable) { - return null - } - return rewriter ?: try { - newRewriter().also { rewriter = it } - } catch (e: LinkageError) { - // No libdovi for this ABI. Playback goes on exactly as it did without the filter. - Timber.w(e, "libdovi is not available on this device, Dolby Vision profile 7 is left as it is") - rewriterUnavailable = true - null - } - } - - private fun refuse( - reason: String, - e: Throwable?, - ) { - if (refusedCount == 0) { - Timber.w(e, "Leaving a Dolby Vision profile 7 access unit as it is: %s", reason) - } - refusedCount++ - } - - private fun staging(size: Int): ByteArray { - if (staging.size < size) { - staging = ByteArray(maxOf(size, INITIAL_CAPACITY_BYTES)) - } - return staging - } - - private fun scratch(capacity: Int): ByteBuffer { - val current = scratch - val buffer = - if (current == null || current.capacity() < capacity) { - ByteBuffer.allocateDirect(maxOf(capacity, INITIAL_CAPACITY_BYTES)).also { scratch = it } - } else { - current - } - (buffer as Buffer).clear() - return buffer - } - - override fun toString(): String = - "Dolby Vision profile 7 โ†’ 8.1 ($convertedCount access units converted, " + - "$passedThroughCount passed through, $refusedCount refused)" - - private companion object { - const val DOLBY_VISION_PROFILE_7 = 7 - - /** HEVC NAL unit type carrying the Dolby Vision RPU. */ - const val NAL_UNIT_TYPE_RPU = 62 - - /** HEVC NAL unit type carrying the Dolby Vision enhancement layer picture. */ - const val NAL_UNIT_TYPE_ENHANCEMENT_LAYER = 63 - - /** Big enough for a UHD access unit without regrowing on every early frame. */ - const val INITIAL_CAPACITY_BYTES = 512 * 1024 - - /** - * Room past the access unit for an RPU which comes out of the rewrite longer than it went - * in. Dropping the enhancement layer usually frees far more, but a profile 7 stream whose - * enhancement layer the extractor never delivered has none to free. - */ - const val RPU_GROWTH_HEADROOM_BYTES = 4 * 1024 - - private fun startsWithStartCode( - data: ByteBuffer, - offset: Int, - size: Int, - ): Boolean { - if (size < 4) { - return false - } - return data.get(offset) == 0.toByte() && - data.get(offset + 1) == 0.toByte() && - ( - data.get(offset + 2) == 1.toByte() || - (data.get(offset + 2) == 0.toByte() && data.get(offset + 3) == 1.toByte()) - ) - } - - /** Whether the Annex B access unit in `[0, length)` of [source] carries an RPU. */ - private fun carriesRpu( - source: ByteArray, - length: Int, - ): Boolean { - var unit = 0 - while (unit < length) { - val payload = unit + startCodeLength(source, unit) - if (payload >= length) { - return false - } - if (nalUnitType(source[payload]) == NAL_UNIT_TYPE_RPU) { - return true - } - unit = nextStartCode(source, payload, length) - } - return false - } - - /** - * Copies the Annex B access unit in `[0, length)` of [source] into [target] from index 0, - * leaving out every enhancement layer NAL unit, and returns the number of bytes copied. - */ - fun copyWithoutEnhancementLayer( - source: ByteArray, - length: Int, - target: ByteBuffer, - ): Int { - var unit = 0 - var written = 0 - while (unit < length) { - val payload = unit + startCodeLength(source, unit) - if (payload >= length) { - // A start code with nothing behind it. Not something to interpret; kept as it came. - target.put(source, unit, length - unit) - written += length - unit - break - } - val next = nextStartCode(source, payload, length) - if (nalUnitType(source[payload]) != NAL_UNIT_TYPE_ENHANCEMENT_LAYER) { - target.put(source, unit, next - unit) - written += next - unit - } - unit = next - } - return written - } - - /** - * A view of `[offset, offset + length)` of [data] with a position and limit of its own, so - * that bulk copies leave the buffer's untouched. The position and limit calls go through - * [Buffer]: the [ByteBuffer] overrides which return a ByteBuffer are API 34, and `minSdk` is 23. - */ - private fun window( - data: ByteBuffer, - offset: Int, - length: Int, - ): ByteBuffer { - val view = data.duplicate() - val buffer: Buffer = view - buffer.clear() - buffer.position(offset) - buffer.limit(offset + length) - return view - } - - /** nal_unit_type is the 6 bits after forbidden_zero_bit, H.265 section 7.3.1.2. */ - private fun nalUnitType(header: Byte): Int = (header.toInt() shr 1) and 0x3F - - /** Length of the start code at [at], which the caller knows is one: 3 or 4 bytes. */ - private fun startCodeLength( - data: ByteArray, - at: Int, - ): Int = if (data[at + 2] == 1.toByte()) 3 else 4 - - /** - * Index of the next Annex B start code at or after [from], or [limit] if there is none. A - * fourth leading zero byte counts as part of the start code, which is the form media3's - * extractors emit; three byte codes are walked all the same. - */ - private fun nextStartCode( - data: ByteArray, - from: Int, - limit: Int, - ): Int { - var i = from - while (i + 2 < limit) { - if (data[i] == 0.toByte() && data[i + 1] == 0.toByte() && data[i + 2] == 1.toByte()) { - return if (i > from && data[i - 1] == 0.toByte()) i - 1 else i - } - i++ - } - return limit - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index f1384a27..5c5004ac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -12,6 +12,7 @@ import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.MediaStreamProtocol import org.jellyfin.sdk.model.api.ProfileConditionValue import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod +import org.jellyfin.sdk.model.api.VideoRangeType import org.jellyfin.sdk.model.deviceprofile.DeviceProfileBuilder import org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile @@ -70,11 +71,10 @@ fun createDeviceProfile( downMixAudio: Boolean, assDirectPlay: Boolean, pgsDirectPlay: Boolean, + dolbyVisionELDirectPlay: Boolean, decodeAv1: Boolean, jellyfinTenEleven: Boolean, preferAc3ForSurround: Boolean, - forceEnabledHdr: Set, - forceDisabledHdr: Set, maxResolution: Int, ) = buildDeviceProfile { val allowedAudioCodecs = @@ -148,9 +148,9 @@ fun createDeviceProfile( videoCodec(Codec.Video.H264) if (preferAc3ForSurround) { - audioCodec(Codec.Audio.AC3) - } else { audioCodec(*allowedAudioCodecs) + } else { + audioCodec(Codec.Audio.AC3) } copyTimestamps = false @@ -467,56 +467,60 @@ fun createDeviceProfile( // / HDR exclude list + // TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK val unsupportedRangeTypesAv1 = buildSet { - if (jellyfinTenEleven) add(WholphinVideoRangeType.DOVI_INVALID.serialName) + if (jellyfinTenEleven) add("DOVIInvalid") if (!decodeAv1) { if (!supportsAV1DolbyVision) { - add(WholphinVideoRangeType.DOVI.serialName) - if (!supportsAV1HDR10) add(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName) - if (jellyfinTenEleven && !supportsAV1HDR10Plus) add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName) + add(VideoRangeType.DOVI.serialName) + if (!supportsAV1HDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName) + if (jellyfinTenEleven && !supportsAV1HDR10Plus) add("DOVIWithHDR10Plus") } if (!supportsAV1HDR10Plus) { - add(WholphinVideoRangeType.HDR10_PLUS.serialName) + add(VideoRangeType.HDR10_PLUS.serialName) - if (!mediaTest.supportsAV1HDR10()) add(WholphinVideoRangeType.HDR10.serialName) + if (!mediaTest.supportsAV1HDR10()) add(VideoRangeType.HDR10.serialName) } } - } - forceEnabledHdr + forceDisabledHdr + } + // TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK val unsupportedRangeTypesHevc = buildSet { - if (jellyfinTenEleven) add(WholphinVideoRangeType.DOVI_INVALID.serialName) + if (jellyfinTenEleven) add("DOVIInvalid") if (!supportsHevcDolbyVisionEL) { - if (jellyfinTenEleven) { - add(WholphinVideoRangeType.DOVI_WITH_EL.serialName) - if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) { - add(WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName) + if (!dolbyVisionELDirectPlay) { + if (jellyfinTenEleven) { + add("DOVIWithEL") + if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus") } } if (!supportsHevcDolbyVision) { - add(WholphinVideoRangeType.DOVI.serialName) - if (!supportsHevcHDR10) add(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName) + add(VideoRangeType.DOVI.serialName) + if (!supportsHevcHDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName) if (jellyfinTenEleven && !supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) { - add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName) + add( + "DOVIWithHDR10Plus", + ) } } } if (!supportsHevcHDR10Plus) { - add(WholphinVideoRangeType.HDR10_PLUS.serialName) - if (!supportsHevcHDR10) add(WholphinVideoRangeType.HDR10.serialName) + add(VideoRangeType.HDR10_PLUS.serialName) + if (!supportsHevcHDR10) add(VideoRangeType.HDR10.serialName) } if (jellyfinTenEleven && KnownDefects.hevcDoviHdr10PlusBug) { - add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName) - add(WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName) + add("DOVIWithHDR10Plus") + add("DOVIWithELHDR10Plus") } - } - forceEnabledHdr + forceDisabledHdr + } // Display // Note: The codec profiles use a workaround to create correct behavior diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt index a769c400..ab4de03e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt @@ -209,15 +209,6 @@ class MediaCodecCapabilitiesTest( ) && supportsMultiInstance(MediaFormat.MIMETYPE_VIDEO_HEVC) - // Checks for single-layer Dolby Vision Profile 8, which the Profile 7 conversion produces - fun supportsHevcDolbyVisionProfile8(): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && - hasDecoder( - MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION, - DolbyVisionProfiles.Profile8, - CodecProfileLevel.DolbyVisionLevelHd24, - ) - fun supportsHevcHDR10(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && hasDecoder( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/WholphinVideoRangeType.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/WholphinVideoRangeType.kt deleted file mode 100644 index bd49a6c4..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/WholphinVideoRangeType.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.damontecres.wholphin.util.profile - -import org.jellyfin.sdk.model.api.VideoRangeType - -/** - * Complete list of video range types, including ones not yet present in the pinned Jellyfin SDK's - * [VideoRangeType] enum (added server-side in Jellyfin 10.11). - * - * TODO Remove this and use [VideoRangeType] directly once the SDK is upgraded to a version built against Jellyfin 10.11+ - */ -enum class WholphinVideoRangeType( - val serialName: String, -) { - UNKNOWN(VideoRangeType.UNKNOWN.serialName), - SDR(VideoRangeType.SDR.serialName), - HDR10(VideoRangeType.HDR10.serialName), - HLG(VideoRangeType.HLG.serialName), - DOVI(VideoRangeType.DOVI.serialName), - DOVI_WITH_HDR10(VideoRangeType.DOVI_WITH_HDR10.serialName), - DOVI_WITH_HLG(VideoRangeType.DOVI_WITH_HLG.serialName), - DOVI_WITH_SDR(VideoRangeType.DOVI_WITH_SDR.serialName), - HDR10_PLUS(VideoRangeType.HDR10_PLUS.serialName), - DOVI_INVALID("DOVIInvalid"), - DOVI_WITH_EL("DOVIWithEL"), - DOVI_WITH_EL_HDR10_PLUS("DOVIWithELHDR10Plus"), - DOVI_WITH_HDR10_PLUS("DOVIWithHDR10Plus"), -} diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index 910853e9..0c3b5cb3 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -54,15 +54,10 @@ message PlaybackOverrides{ bool direct_play_ass = 3 [deprecated = true]; bool direct_play_pgs = 4; MediaExtensionStatus media_extensions_enabled = 5; - bool direct_play_dolby_vision_e_l = 6 [deprecated = true]; + bool direct_play_dolby_vision_e_l = 6; bool decode_av1 = 7; AssPlaybackMode ass_playback_mode = 8; int32 max_resolution = 9; - HdrOverrideMode dovi_profile_5_override = 10; - HdrOverrideMode dovi_profile_7_override = 11; - HdrOverrideMode dovi_profile_8_override = 12; - HdrOverrideMode hdr10_override = 13; - HdrOverrideMode hdr10_plus_override = 14; } message PlaybackPreferences { @@ -128,7 +123,7 @@ enum AppThemeColors { OLED_BLACK = 5; RED = 6; BROWN = 7; - reserved 8; + WOLFSKIN = 8; } enum EdgeStyle{ @@ -230,24 +225,18 @@ message MusicPreferences { bool show_backdrop = 4; } -enum HdrOverrideMode{ - HDR_OVERRIDE_AUTO = 0; - HDR_OVERRIDE_ENABLE = 1; - HDR_OVERRIDE_DISABLE = 2; -} - message ExperimentalPreferences { bool enabled = 1; bool video_tunneling_enabled = 2; bool prefer_ac3_surround = 3; bool disable_audio_offload = 4; bool prefer_dolby_vision_over_hdr10_plus = 5; - bool convert_dolby_vision_profile_7 = 6; } message AppPreferences { - string current_server_id = 1 [deprecated = true]; - string current_user_id = 2 [deprecated = true]; + // The currently signed in server and user IDs, mostly for restoring a session + string current_server_id = 1; + string current_user_id = 2; PlaybackPreferences playback_preferences = 3; HomePagePreferences home_page_preferences = 4; diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml index 4c5f707b..ec3f55e3 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml @@ -1,9 +1,4 @@ - - - - - - + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 5b7669dc..4ac23262 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index 8e05a03a..de1628cc 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index e6a5972a..df5faba9 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index f44d1093..415cf194 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index 9e9214e8..d9df942a 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 3766935e..dea8ff64 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_banner.png b/app/src/main/res/mipmap-xhdpi/ic_banner.png index 4fa742db..ee42b91e 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_banner.png and b/app/src/main/res/mipmap-xhdpi/ic_banner.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png index 52b72c47..ee42b91e 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png and b/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index ce75a196..924b40eb 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index 22298ad1..e218f8bd 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 3f7c7e29..40674eff 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index e5cef34d..6f5be441 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index fafcb4f1..8c1935c7 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 5c1a8e36..48a4e286 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 90341396..1cf49fb6 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index dbe5821e..afc41a8b 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 2ef5c7d0..122a33e0 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7170a274..f4523d02 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -719,44 +719,4 @@ Von Wiedergabeliste entfernen Anmeldeinformationen abgelaufen, bitte erneut anmelden Du bist aktuell nicht berechtigt auf den Server zuzugreifen - Dolby Vision gegenรผber HDR10+ bevorzugen - Entfernt HDR10+ Metadaten von Dolby Vision Videos fรผr TVs, welche die Dateien sonst als HDR10+ abspielen wรผrden - Maximale Auflรถsung vor dem Transkodieren - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d Folge - %1$d Folgen - - Am besten bewertet - HDR Formate - HDR10 - HDR10+ - Dolby Vision Profile 5 - Dolby Vision Profile 7 - Dolby Vision Profile 8 - Benรถtigte Server Version: %1$s - Benutzer - Standard vom Server laden - Nur auf diesem Gerรคt speichern - Standard - Gerรคte spezifisch - Server Benutzerprofil - Server Standard - Benutzer Webclient - Vom Server verwaltet - Fehler - Server Version wird bald nicht mehr unterstรผtzt - Festgelegt von %1$s - Dolby Vision Profile 7 zu 8.1 konvertieren - Fรผr Gerรคte, die zwar Dolby Vision Profil 8, aber nicht Profil 7 dekodieren kรถnnen, wird das zweischichtige Profil 7 vor der Wiedergabe in das einschichtige Profil 8.1 umgewandelt und dann direkt abgespielt - Interpreten Quelle - Reihen - Aktivieren erzwingen - Deaktivieren erzwingen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 714ae611..696ad927 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -741,19 +741,4 @@ Eliminar de la lista de reproducciรณn Credenciales caducadas, vuelve a iniciar sesiรณn No tienes autorizaciรณn para acceder al servidor en este momento - Preferir Dolby Vision sobre HDR10+ - Elimina los metadatos HDR10+ de vรญdeos en Dolby Vision para televisores que, de lo contrario, los reproducirรญan como HDR10+ - Resoluciรณn mรกxima antes de transcodificar - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d Episodio - %1$d Episodios - %1$d Episodios - diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 2008bf2c..8fc29c91 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -719,44 +719,4 @@ Eemalda esitusloendist Autentimiseandmed on aegunud, palun logi uuesti sisse Sul pole hetkel รตigusi ligipรครคsuks sellele serverile - HDR10+ asemel eelista Dolby Visionit - Kui teler muul juhul kasutaks esituseks HDR10+ vormingut, siis eemaldab HDR10+ metateabe Dolby Visioni videotest - Maksimaalne resolutsioon enne kodeeringu muutmist - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d osa - %1$d osa - - Parima hinnanguga - HDR-vormingud - HDR10 - HDR10+ - Dolby Vision Profile 5 - Dolby Vision Profile 7 - Dolby Vision Profile 8 - Vajalik serveriversioon: %1$s - Serveriversioon ei saa lรคhitulevikus olema toetatud - Kasutaja - Sai - Vaikimisi laadi serverist - Salvesta vaid siia seadmesse - Vaikimisi - Seadmekohane - Kasutajaprofiil serveris - Vaikimisi vรครคrtus serveris - Kasutaja veebiklient - Valija: %1$s - Jรตusta sundkorras - Lรตpeta sundkorras jรตustamine - Hallatud serveri poolt - Viga - Teisenda Dolby Vision Profile 7 โ†’ 8.1 - Muudab kahekihilise โ€žProfile 7โ€œ enne dekooderit รผhekihiliseks profiiliks 8.1 ja esitab seda seadmetel otse, mille dekooder toetab profiili 8, kuid mitte profiili 7 - Esitaja allikas diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 85603d28..898f8a10 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -724,62 +724,4 @@ Livre audio Version du serveur non prise en charge Apparaรฎt dans - %1$dh %2$ds - S%1$d E%2$d - S%1$d E%2$d-E%3$d - S%1$02dE%2$02d - S%1$02dE%2$02d-E%3$02d - E%1$d - Saison %1$d - Dรฉsactiver le dรฉlestage matรฉriel audio - La dรฉsactivation du dรฉchargement du dรฉcodage audio peut rรฉsoudre les problรจmes de blocage de la lecture sur certains appareils - Prรฉfรฉrez Dolby Vision ร  HDR10+ - Supprime les mรฉtadonnรฉes HDR10+ des vidรฉos Dolby Vision, pour les tรฉlรฉviseurs qui, sans cela, les liraient en HDR10+ - ร‰cran en mode veille aprรจs un certain dรฉlai - Pourcentage de rรฉduction de luminositรฉ - ร‰cran sombre - Albums photos - Aucun favori trouvรฉโ€ฏ! - Supprimer de la liste de lecture - Vos identifiants ont expirรฉ. Veuillez vous reconnecter - Vous n\'รชtes pas autorisรฉ(e) ร  accรฉder au serveur pour le moment - Rรฉsolution maximale avant le transcodage - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d ร‰pisode - %1$d ร‰pisodes - %1$d ร‰pisode - - Les mieux notรฉs - Formats HDR - HDR10 - HDR10+ - Dolby Vision Profile 5 - Dolby Vision Profile 7 - Dolby Vision Profile 8 - Activation forcรฉe - Dรฉsactivation forcรฉe - La version du serveur ne sera bientรดt plus prise en charge - Version requise du serveurย : %1$s - Sรฉlectionnรฉ par %1$s - Utilisateur - Sรฉries - Charger les paramรจtres par dรฉfaut depuis le serveur - Enregistrer uniquement sur cet appareil - Dรฉfaut - Spรฉcifique ร  l\'appareil - Profil utilisateur du serveur - Paramรจtres par dรฉfaut du serveur - Client Web utilisateur - Convertir le profil Dolby Vision 7 en 8.1 - Convertit le profil 7 double couche en profil 8.1 simple couche avant le dรฉcodeur et le lit directement, pour les appareils dont le dรฉcodeur prend en charge le profil 8 mais pas le profil 7 - Source de l\'artiste - Gรฉrรฉ par le serveur - Erreur diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f078a5f7..64d8c3b1 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -25,7 +25,7 @@ Rendezล‘ Letiltva Felfedezett szerverek - + Letรถltรฉs & Frissรญtรฉs Letรถltรฉs folyamatbanโ€ฆ Engedรฉlyezve Adja meg a szerver IP-t vagy URL-t @@ -43,7 +43,7 @@ Kiegรฉszรญtล‘ Mลฑfajok Ugrรกs a sorozathoz - Ugrรกs ide + Ugrรกs ide: Lejรกtszรกsi gombok elrejtรฉse Hibakeresรฉsi infรณ elrejtรฉse Elrejt @@ -95,7 +95,7 @@ รšjraindรญtรกs Folytatรกs Mentรฉs - + Keresรฉs & Letรถltรฉs Keresรฉs Keresรฉsโ€ฆ Szerver kivรกlasztรกsa @@ -121,7 +121,7 @@ Vรกltรกs Legjobbra รฉrtรฉkelt, nem megnรฉzett - Elล‘zetes + Elล‘zetesek Elล‘zetesek DVR Idล‘zรญtรฉs @@ -138,7 +138,6 @@ Videรณk Nรฉzze รฉlล‘ben - %1$d รฉves %1$d รฉves Lejรกtszรกs รกtalakรญtรกssal @@ -160,8 +159,8 @@ Egyebek - A kulisszรกk mรถgรถtt - A kulisszรกk mรถgรถtt + Szรญnfalak mรถgรถtt + Szรญnfalak mรถgรถtt Fล‘cรญmdal @@ -201,19 +200,19 @@ %s letรถltรฉs - %s letรถltรฉsek + %s letรถltรฉs %d รณra - %d รณrรกk + %d รณra %d elem - %d elemek + %d elem %d mรกsodperc - %d mรกsodpercek + %d mรกsodperc %1$s-kor รฉr vรฉget Nem talรกlhatรณ szerver @@ -235,7 +234,7 @@ Tartalom alapรฉrtelmezett mรฉretezรฉse Frissรญtรฉs telepรญtรฉse Telepรญtett verziรณ - A kezdล‘lap sorainak maximรกlis elemszรกma + Elemek szรกma soronkรฉnt a kezdล‘lapon Vรกlassza ki az alapรฉrtelmezett elemeket, amelyek megjelennek, a tรถbbi el lesz rejtve Navigรกciรณs menรผ elemeinek testreszabรกsa Kattintson az oldalak kรถzti vรกltรกshoz @@ -304,10 +303,10 @@ PIN-kรณd eltรกvolรญtรกsa Nรฉzetbeรกllรญtรกsok Oszlopok - Tรฉrkรถz + Helykรถz Kรฉparรกny - Rรฉszletek megjelenรญtรฉse - Kรฉptรญpus + Rรฉszletek mutatรกsa + Kรฉp tรญpusa Vendรฉgszereplล‘k Szegรฉly mรฉrete Tartalomhoz igazodรณ kรฉpfrissรญtรฉsi gyakorisรกg @@ -355,10 +354,10 @@ Felirat kรฉsleltetรฉse Hรกttรฉr stรญlusa Adja meg a szerver cรญmรฉt - + Folytatรกs & Kรถvetkezล‘ menรผ รถsszevonรกsa Lejรกtszรกs backend Kรฉp-gyorsรญtรณtรกr mรฉrete (MB) - + Stรกbtagok Navigรกciรณs menรผ oldalainak vรกltรกsa fรณkusznรกl Lejรกtszรกs felรผlbรญrรกlatai Felbontรกsvรกltรกs @@ -373,275 +372,4 @@ DTS Csak kiegรฉszรญtล‘ felirat Sรกvvรกlasztรกs tรถrlรฉse - %1$dรณ %2$dp %3$dmp - %1$dรณ %2$dp - %1$dรณ %2$dmp - %1$dp %2$dmp - %1$dรณ - %1$dp - %1$dmp - Felhasznรกlรณi profilbeรกllรญtรกsok a szerverrล‘l - Hangalapรบ keresรฉs - Indรญtรกs - Keresรฉs hanggal - Feldolgozรกs - A megszakรญtรกshoz nyomd meg a Vissza gombot - Hangfelvรฉteli hiba - Hangfelismerรฉsi hiba - Mikrofonengedรฉly szรผksรฉges - Hรกlรณzati hiba - Hรกlรณzati idล‘tรบllรฉpรฉs - Nem sikerรผlt felismerni a beszรฉdet - A hangfelismerรฉs foglalt - Szerverhiba - Nem รฉszlelhetล‘ beszรฉd - Ismeretlen hiba - Nem sikerรผlt elindรญtani a hangfelismerรฉst - A hangfelismerรฉs idล‘tรบllรฉpรฉs miatt megszakadt - รšjraprรณbรกlรกs - Elล‘zetesek - %1$d. รฉvad - Egyรฉb - Werkfilmek - Fล‘cรญmdalok - Fล‘cรญmvideรณk - Klipek - Tรถrรถlt jelenetek - Interjรบk - Jelenetek - Mintรกk - Rรถvid werkfilmek - Rรถvidfilmek - Kedvenc %s - - %s nap - %s napok - - - %s perc - %s percek - - - Film - Filmek - - - Sorozat - Sorozatok - - - Szemรฉly - Szemรฉlyek - - Bejelentkezรฉs szรผksรฉges a profil hasznรกlatรกhoz - [0โ€“9] - Hardveres hangfeldolgozรกs kikapcsolรกsa - A hardveres hangdekรณdolรกs kikapcsolรกsa egyes eszkรถzรถkรถn megoldhatja a lejรกtszรกs kรถzbeni lefagyรกsokat - ร–sszesรญtett keresรฉsi talรกlatok - Az รถsszes talรกlat megjelenรญtรฉse egyetlen listรกban - Talรกlatok megjelenรญtรฉse kategรณriรกnkรฉnt - Hangalapรบ keresรฉs gomb megjelenรญtรฉse - Lรกthatรณ - Rejtett - Talรกlatok - Dolby Vision 7-es profil kรถzvetlen lejรกtszรกsa - Figyelmen kรญvรผl hagyja az eszkรถzkompatibilitรกs ellenล‘rzรฉsรฉt - A Dolby Vision elล‘nyben rรฉszesรญtรฉse a HDR10+-szal szemben - Eltรกvolรญtja a HDR10+ metaadatokat a Dolby Vision-videรณkbรณl azoknรกl a tรฉvรฉknรฉl, amelyek egyรฉbkรฉnt HDR10+-kรฉnt jรกtszanรกk le ล‘ket - Felfedezรฉs - Kรฉrรฉs - Fรผggล‘ben - Seerr-integrรกciรณ - Seerr-szerver eltรกvolรญtรกsa - Seerr-szerver hozzรกadva - Gyors csatlakozรกs - Engedรฉlyezd egy mรกsik eszkรถznek a bejelentkezรฉst a fiรณkodba - Add meg a gyors csatlakozรกsi kรณdot - A kรณdnak 6 szรกmjegybล‘l kell รกllnia - Az eszkรถz engedรฉlyezรฉse sikeres - Jelszรณ - Felhasznรกlรณnรฉv - URL - Nรฉpszerลฑ most - Hamarosan megjelenล‘ filmek - Hamarosan indulรณ sorozatok - Kรฉrรฉs 4K-ban - AV1 szoftveres dekรณdolรกsa - Az MPV mostantรณl az alapรฉrtelmezett lejรกtszรณ, kivรฉve HDR-tartalmak esetรฉn. \nEzt a beรกllรญtรกsokban mรณdosรญthatod. - HDR-feliratok stรญlusa - Kรฉpalapรบ feliratok รกtlรกtszatlansรกga - Fรฉnyerล‘ - Kontraszt - Telรญtettsรฉg - Szรญnรกrnyalat - Mentรฉs albumhoz - Diavetรญtรฉs indรญtรกsa - Diavetรญtรฉs leรกllรญtรกsa - Az elejรฉn - Nincs tรถbb fรฉnykรฉp - Forgatรกs balra - Forgatรกs jobbra - Nagyรญtรกs - Kicsinyรญtรฉs - Diavetรญtรฉs idล‘tartama - Videรณk lejรกtszรกsa diavetรญtรฉs kรถzben - Mรฉdiaadat-naplรณ kรผldรฉse a szervernek - Korlรกtlan - A โ€žKรถvetkezล‘โ€ listรกban megjelenล‘ napok maximรกlis szรกma - Albumok - Elล‘adรณk - Dalok - Ugrรกs az elล‘adรณhoz - Most jรกtszott tartalom - Azonnali mix - Ugrรกs az albumhoz - Hozzรกadรกs a lejรกtszรกsi sorhoz - Sor hozzรกadรกsa - Mลฑfajok %1$s - Nemrรฉg megjelent %1$s - Magassรกg - Alkalmazรกs az รถsszes sorra - Kezdล‘lap testreszabรกsa - Kezdล‘lap sorai - Betรถltรฉs a szerver felhasznรกlรณi profiljรกbรณl - Mentรฉs a szerver felhasznรกlรณi profiljรกba - Betรถltรฉs a webes kliensbล‘l - Sorozat kรฉpรฉnek hasznรกlata - Sor hozzรกadรกsa ehhez: %1$s - Felรผlรญrod a szerveren lรฉvล‘ beรกllรญtรกsokat? - Felรผlรญrod a helyi beรกllรญtรกsokat? - Epizรณdokhoz - Javaslatok ehhez: %1$s - Az รถsszes kรกrtya mรฉretรฉnek nรถvelรฉse - Az รถsszes kรกrtya mรฉretรฉnek csรถkkentรฉse - Bรฉlyegkรฉpek hasznรกlata - Beรกllรญtรกsok mentve - Kijelzล‘-beรกllรญtรกsok - Beรฉpรญtett elล‘beรกllรญtรกsok az รถsszes sor gyors formรกzรกsรกhoz - Sorok รฉs kรฉpek kivรกlasztรกsa a kezdล‘lapon - - Tรฉrhatรกsรบ hang hasznรกlata AC3 formรกtumban - Kezdรฉs ennyi idล‘ mรบlva - Animรกlรกs - Maximรกlis korhatรกr - Nincs felsล‘ korlรกt - Minden korosztรกly szรกmรกra - %s รฉves korig - Kรฉpernyล‘vรฉdล‘ - Kรฉpernyล‘vรฉdล‘ beรกllรญtรกsok - Kรฉpernyล‘vรฉdล‘ elindรญtรกsa - Idล‘tartam - Fotรณk - Tรญpusok felvรฉtele - Mely elemtรญpusokhoz jelenjenek meg kรฉpek - Illesztรฉs - Kivรกgรกs - Kitรถltรฉs - Szรฉlessรฉg kitรถltรฉse - Magassรกg kitรถltรฉse - Wholphin alapรฉrtelmezett beรกllรญtรกsa - Wholphin kompakt elrendezรฉs - A sorozat miniatลฑrkรฉpei - Az epizรณdok miniatลฑrkรฉpei - Legalacsonyabb - Alacsony - Kรถzepes - Magas - Teljes - Lila - Narancs - ร‰lรฉnk kรฉk - Fekete - Barna - Kihagyรกs - Automatikus รกtugrรกs - Rรกkรฉrdezรฉs a kihagyรกsra - Az FFmpeg hasznรกlata csak akkor, ha nincs beรฉpรญtett dekรณder - Az FFmpeg elล‘nyben rรฉszesรญtรฉse a beรฉpรญtett dekรณderekkel szemben - FFmpeg-dekรณderek hasznรกlatรกnak tiltรกsa - A lejรกtszรกs vรฉgรฉn - HDR-formรกtumok - HDR10 - HDR10+ - Dolby Vision 5-รถs profil - Dolby Vision 7-es profil - Dolby Vision 8-as profil - Kรฉnyszerรญtett engedรฉlyezรฉs - Kรฉnyszerรญtett letiltรกs - Piros - Zรถld - Kรฉk - Elmosรณdรกs - A stรกblista รฉs a zรกrรณjelenetek alatt - Soha - Fehรฉr - Vilรกgosszรผrke - Sรถtรฉtszรผrke - Sรกrga - Ciรกn - Magenta - Kรถrvonal - รrnyรฉk - Sortรถrรฉs - Az MPV elล‘nyben rรฉszesรญtรฉse - Kรผlsล‘ lejรกtszรณ - Az ExoPlayer hasznรกlata HDR-lejรกtszรกshoz - Poszter (2:3) - 16:9 - 4:3 - Nรฉgyzet (1:1) - Elsล‘dleges - Bรฉlyegkรฉp - Rรกcs - Lista - ร–sszefoglalรณ lista - Dinamikus szรญnลฑ kรฉp - Csak kรฉp - - API-kulcs - Bejelentkezรฉs a Seerr-szerverre - Jellyfin-felhasznรกlรณ - Helyi felhasznรกlรณ - - Nem talรกlhatรณ tรกvoli felirat - Jelenlegi - Az alkalmazรกson belรผli kรฉpernyล‘vรฉdล‘ hasznรกlata - Biztosan tรถrรถlni szeretnรฉd ezt az elemet? - Mรฉdiakezelรฉsi lehetล‘sรฉgek megjelenรญtรฉse - Keresรฉs %s - Szรญnรฉsz - Zeneszerzล‘ - รrรณ - Vendรฉgszereplล‘ - Producer - Karmester - Dalszรถvegรญrรณ - Rendezรฉs - Mรฉrnรถk - Hangmรฉrnรถk - Alkotรณ - Mลฑvรฉsz - Album elล‘adรณja - Album - Nรฉpszerลฑ dalok - Lejรกtszรกs kรถvetkezล‘kรฉnt - Videรณklipek - Dalszรถvegek - Dalszรถveg elrejtรฉse - Dalszรถveg megjelenรญtรฉse - Eltรกvolรญtรกs a lejรกtszรกsi sorbรณl - Albumborรญtรณ megjelenรญtรฉse - Vizuรกlis effektusok megjelenรญtรฉse - Hรกttรฉr megjelenรญtรฉse - Lejรกtszรกs mรกskรฉnt? - A jelenleg lejรกtszott hang vizualizรกlรกsรกhoz engedรฉlyezni kell a hangrรถgzรญtรฉst. - Folytatรกs - ร–sszes kijelรถlรฉse - Tรญpusok kรผlรถnvรกlasztรกsa - A cรญmek logรณinak elล‘nyben rรฉszesรญtรฉse - A Wholphin frissรผlt a %s verziรณra - Eltรกvolรญtรกs a โ€žFolytatรกsโ€ listรกbรณl - Sorozat eltรกvolรญtรกsa a โ€žKรถvetkezล‘ epizรณdโ€ listรกbรณl - A โ€žKรถvetkezล‘ epizรณdโ€ listรกbรณl eltรกvolรญtott sorozatok megtekintรฉse - Tovรกbbi informรกciรณk diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 623ef596..d6f20d4b 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -694,20 +694,4 @@ Redupkan layar Album foto Favorit tidak ditemukan! - - %1$d episode - - Utamakan Dolby Vision dibanding HDR10+ - Hapus metadata HDR10+ dari video Dolby Vision, untuk TV yang dapat memutarnya sebagai HDR10+ - Hapus dari playlist - Kredensial kedaluwarsa, silakan login kembali - Kamu tidak diizinkan mengakses server saat ini - Resolusi maksimum sebelum transkode - 8K - 4K - 1080 - 720 - 576 - 480 - 360 diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index be2f53e3..fb5f37ce 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -741,42 +741,4 @@ Rimuovi dalla playlist Credenziali scadute, effettua di nuovo lโ€™accesso Non sei autorizzato ad accedere al server in questo momento - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d Episodio - %1$d Episodi - %1$d Episodi - - Preferisci Dolby Vision a HDR10+ - Rimuove i metadati HDR10+ dai video Dolby Vision, per le TV che altrimenti li riproducono come HDR10+ - Risoluzione massima prima della trascodifica - HDR10 - HDR10+ - I piรน votati - Formati HDR - Dolby Vision Profilo 5 - Dolby Vision Profilo 7 - Dolby Vision Profilo 8 - Forza abilitazione - Forza disabilitazione - La versione del server non sarร  piรน supportata a breve - Versione del server richiesta: %1$s - Serie - Errore - Utente - Carica predefinito dal server - Salva solo su questo dispositivo - Predefinito - Specifico del dispositivo - Profilo utente del server - Predefinito del server - Client web utente - Gestito dal server - Scelto da %1$s diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d05db022..fe4ce431 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -220,7 +220,7 @@ ื‘ื’ื™ืœ ืฉื ืชื™ื™ื ื‘ื’ื™ืœ %1$d - ื ื’ืŸ ืขื ื”ืžืจืชึพืงื™ื“ื•ื“ + ื ื’ืŸ ืขื ื”ืžืจืช ืงื™ื“ื•ื“ ืคืจื˜ื™ ื”ืžื“ื™ื” ื”ืฆื’ ืฉืขื•ืŸ ืกื“ืจ ืฉื™ื“ื•ืจ ื”ืคืจืงื™ื @@ -613,7 +613,7 @@ ื”ืชื ื”ื’ื•ืช ื“ื™ืœื•ื’ ืขื‘ื•ืจ ืคืชื™ื—ื™ื, ื›ืชื•ื‘ื™ื•ืช ืกื™ื•ื ื•ื›ื•\' ื ื’ืŸ ืขื - ื”ืžืจืชึพืงื™ื“ื•ื“ + ื”ืžืจืช ืงื™ื“ื•ื“ NAL AVC SDR @@ -738,22 +738,4 @@ ืขื•ื ื” %1$02d ืคืจืงื™ื %2$02d-%3$02d ืคืจืง %1$d ืขื•ื ื” %1$d - ื”ืขื“ืคืช Dolby Vision ืขืœ ืคื ื™ HDR10+ - ืžื—ื™ืงืช ืžื˜ื-ื ืชื•ื ื™ื ืฉืœ HDR10+ ืžืžื“ื™ื” ืžืกื•ื’ Dolby Vision ืขื‘ื•ืจ ื˜ืœื•ื•ื™ื–ื™ื•ืช ืฉืžืขื“ื™ืคื•ืช ืœื ื’ืŸ HDR10+ ื‘ืžืฆื‘ ื–ื” - ืžื—ื™ืงื” ืžืจืฉื™ืžืชึพื”ืฉืžืขื” - ืคืจื˜ื™ ื”ื”ืชื—ื‘ืจื•ืช ืคื’ื•, ื ื ืœื”ืชื—ื‘ืจ ืžื—ื“ืฉ - ืื™ื ืš ืžื•ืจืฉื” ืœื’ืฉืช ืœืฉืจืช ื‘ืฉืœื‘ ื–ื” - ืจื–ื•ืœื•ืฆื™ื” ืžื™ืจื‘ื™ืช ืœืคื ื™ ื”ืžืจืชึพืงื™ื“ื•ื“ - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - ืคืจืง %1$d - %1$d ืคืจืงื™ื - %1$d ืคืจืงื™ื - diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml deleted file mode 100644 index 045e125f..00000000 --- a/app/src/main/res/values-ja/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3bb8567e..83247c34 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -763,46 +763,4 @@ S%1$02d O%2$02d-%3$02d Wyล‚ฤ…cz sprzฤ™towe wsparcie dลบwiฤ™ku Wyล‚ฤ…czenie sprzฤ™towego wsparcie dลบwiฤ™ku moลผe naprawiฤ‡ bล‚ฤ…d z zatrzymaniem siฤ™ odtwarzania na niektรณrych urzฤ…dzeniach - 8K - 4K - 1080p - 720p - 576p - 480p - 360p - - %1$d odcinek - %1$d odcinki - %1$d odcinkรณw - %1$d odcinkรณw - - Maksymalna rozdzielczoล›ฤ‡ przed transkodowaniem - Preferuj Dolby Vision nad HDR10+ - Usuwa metadane HDR10+ z filmรณw Dolby Vision, dla telewizorรณw, ktรณre w przeciwnym razie odtwarzajฤ… je jako HDR10+ - Najlepiej oceniane - Formaty HDR - HDR10 - HDR10+ - Dolby Vision P5 - Dolby Vision P7 - Dolby Vision P8 - Wymuล› wล‚ฤ…czenie - Wymuล› wyล‚ฤ…czenie - Wersja serwera wkrรณtce przestanie byฤ‡ wspierana - Wymagana wersja serwera: %1$s - Wybrane przez %1$s - Uลผytkownik - Seriale - Zaล‚aduj domyล›lne z serwera - Zapisuj tylko na tym urzฤ…dzeniu - Domyล›lnie - Zaleลผne od urzฤ…dzenia - Profil uลผytkownika na serwerze - Domyล›lnie z serwera - Uลผytkownik klienta webowego - Konwertuj Dolby Vision 7 do 8.1 - W przypadku urzฤ…dzeล„, ktรณrych dekoder obsล‚uguje profil 8, ale nie obsล‚uguje profilu 7, system przeksztaล‚ca dwuwarstwowy profil 7 na jednowarstwowy profil 8.1 przed przekazaniem go do dekodera, umoลผliwiajฤ…c bezpoล›rednie odtwarzanie - ลนrรณdล‚o artysty - Zarzฤ…dzane przez serwer - Bล‚ฤ…d diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index f65e53af..d65ac309 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -741,44 +741,4 @@ Remover da lista de reproduรงรฃo As credenciais expiraram. Inicie sessรฃo novamente Nรฃo tem autorizaรงรฃo para aceder ao servidor neste momento - Mais votados - Formato HDR - HDR10 - HDR10+ - Dolby Vision Profile 5 - Dolby Vision Profile 7 - Dolby Vision Profile 8 - Ativaรงรฃo forรงada - Desativaรงรฃo forรงada - Preferir Dolby Vision sobre HDR10+ - Remove os metadados HDR10+ dos vรญdeos em Dolby Vision para televisores que, de outra forma, os reproduziriam como HDR10+ - Converte Dolby Vision Profile 7 para 8.1 - Converte o Perfil 7 de camada dupla no Perfil 8.1 de camada รบnica antes do descodificador e reproduz-o diretamente, para dispositivos cujo descodificador suporta o Perfil 8, mas nรฃo o Perfil 7 - A versรฃo do servidor deixarรก de ser suportada em breve - Versรฃo do servidor necessรกria: %1$s - Resoluรงรฃo mรกxima antes da transcodificaรงรฃo - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d Episรณdio - %1$d Episรณdios - %1$d Episรณdios - - Escolhido por %1$s - Utilizador - Sรฉries - Carregar as predefiniรงรตes do servidor - Guardar apenas neste dispositivo - Padrรฃo - Especรญfico do dispositivo - Perfil de utilizador do servidor - Padrรฃo do servidor - Utilizador do cliente web - Gerido pelo servidor - Erro diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ca59ccb6..f7cc6e4c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -763,43 +763,4 @@ ะฃะฑั€ะฐั‚ัŒ ะธะท ะฟะปะตะนะปะธัั‚ะฐ ะกั€ะพะบ ะดะตะนัั‚ะฒะธั ัƒั‡ั‘ั‚ะฝั‹ั… ะดะฐะฝะฝั‹ั… ะธัั‚ั‘ะบ. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒะพะนะดะธั‚ะต ัะฝะพะฒะฐ ะฃ ะฒะฐั ะฝะตั‚ ะดะพัั‚ัƒะฟะฐ ะบ ัะตั€ะฒะตั€ัƒ ะฒ ะดะฐะฝะฝั‹ะน ะผะพะผะตะฝั‚ - ะก ะฒั‹ัะพะบะธะผ ั€ะตะนั‚ะธะฝะณะพะผ - ะคะพั€ะผะฐั‚ั‹ HDR - HDR10 - HDR10+ - Dolby Vision, ะฟั€ะพั„ะธะปัŒ 5 - Dolby Vision, ะฟั€ะพั„ะธะปัŒ 7 - Dolby Vision, ะฟั€ะพั„ะธะปัŒ 8 - ะŸั€ะธะฝัƒะดะธั‚ะตะปัŒะฝะพ ะฒะบะปัŽั‡ะธั‚ัŒ - ะŸั€ะธะฝัƒะดะธั‚ะตะปัŒะฝะพ ะพั‚ะบะปัŽั‡ะธั‚ัŒ - ะŸั€ะธะพั€ะธั‚ะตั‚ Dolby Vision ะฝะฐะด HDR10+ - ะฃะดะฐะปัะตั‚ ะผะตั‚ะฐะดะฐะฝะฝั‹ะต HDR10+ ะธะท ะฒะธะดะตะพ Dolby Vision ะดะปั ั‚ะตะปะตะฒะธะทะพั€ะพะฒ, ะบะพั‚ะพั€ั‹ะต ะธะฝะฐั‡ะต ะฒะพัะฟั€ะพะธะทะฒะพะดัั‚ ั‚ะฐะบะธะต ะฒะธะดะตะพ ะฒ ั„ะพั€ะผะฐั‚ะต HDR10+ - ะŸะพะดะดะตั€ะถะบะฐ ัั‚ะพะน ะฒะตั€ัะธะธ ัะตั€ะฒะตั€ะฐ ัะบะพั€ะพ ะฟั€ะตะบั€ะฐั‚ะธั‚ัั - ะขั€ะตะฑัƒะตะผะฐั ะฒะตั€ัะธั ัะตั€ะฒะตั€ะฐ: %1$s - ะœะฐะบัะธะผะฐะปัŒะฝะพะต ั€ะฐะทั€ะตัˆะตะฝะธะต ะฑะตะท ะฟะตั€ะตะบะพะดะธั€ะพะฒะฐะฝะธั - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d ัะฟะธะทะพะด - %1$d ัะฟะธะทะพะดะฐ - %1$d ัะฟะธะทะพะดะพะฒ - %1$d ัะฟะธะทะพะดะพะฒ - - ะ˜ัั‚ะพั‡ะฝะธะบ ะฒั‹ะฑะพั€ะฐ: %1$s - ะŸะพะปัŒะทะพะฒะฐั‚ะตะปัŒ - ะกะตั€ะธะฐะป - ะ—ะฐะณั€ัƒะทะธั‚ัŒ ะฝะฐัั‚ั€ะพะนะบะธ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ ั ัะตั€ะฒะตั€ะฐ - ะกะพั…ั€ะฐะฝะธั‚ัŒ ั‚ะพะปัŒะบะพ ะฝะฐ ัั‚ะพะผ ัƒัั‚ั€ะพะนัั‚ะฒะต - ะŸะพ ัƒะผะพะปั‡ะฐะฝะธัŽ - ะะฐัั‚ั€ะพะนะบะธ ัั‚ะพะณะพ ัƒัั‚ั€ะพะนัั‚ะฒะฐ - ะŸั€ะพั„ะธะปัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะฝะฐ ัะตั€ะฒะตั€ะต - ะะฐัั‚ั€ะพะนะบะธ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ ะฝะฐ ัะตั€ะฒะตั€ะต - ะ’ะตะฑ-ะบะปะธะตะฝั‚ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั - ะฃะฟั€ะฐะฒะปัะตั‚ัั ัะตั€ะฒะตั€ะพะผ - ะžัˆะธะฑะบะฐ diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 50d2edd7..bc78a1f8 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -763,20 +763,4 @@ Odstrรกniลฅ z playlistu Platnosลฅ prihlasovacรญch รบdajov vyprลกala, prihlรกste sa znova Momentรกlne nemรกte oprรกvnenie na prรญstup k serveru - Preferovaลฅ Dolby Vision pred HDR10+ - Odstraลˆuje HDR10+ metadรกta z Dolby Vision videรญ, aby ich televรญzory namiesto HDR10+ prehrรกvali v reลพime Dolby Vision - Maximรกlne rozlรญลกenie pred prekรณdovanรญm - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d epizรณda - %1$d epizรณdy - %1$d epizรณd - %1$d epizรณd - diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 4244a0df..3df78be2 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -1,134 +1,4 @@ O aplikaciji - Aktivni Snimci - Omiljeni - Dodaj Server - Dodaj Korisnika - Audio - Mesto roฤ‘enja - Bitrate - Roฤ‘en - Prekini Snimanje - Prekini Snimanje Serije - Prekini - Poglavlja - Izaberi - Oฤisti keลก slika - Kolekcija - Kolekcije - Reklama - Ocena Zajednice - Greลกka! Pritisni dugme da poลกaljeลก logove serveru. - Potvrdi - Nastavi sa gledanjem - Ocena Kritiฤara - %.2f sekunde - %1$dh %2$dm %3$ds - %1$dh %2$dm - %1$dh %2$ds - %1$dm %2$ds - %1$dh - %1$dm - %1$ds - Podrazumevano - Obriลกi - Umro - Reลพirao %1$s - Reลพiser - Iskljuฤeno - Otkriveni Serveri - - Preuzimanjeโ€ฆ - Ukljuฤeno - Zavrลกava se u %1$s - Upiลกi Server IP ili URL - Upiลกi server adresu - Epizode - Greลกka pri uฤitavanju kolekcije %1$s - Eksterni - Omiljeni - Veliฤina - Forsiran - ลฝanrovi - Uฤ‘i u serije - Idi do - Sakrij kontrole za reprodukciju - Sakrij debug informacije - Sakrij - Kuฤ‡a - Odmah - Intro - #ABCฤŒฤ†DฤEFGHIJKLMNOPRSล TUVZลฝ - Biblioteka - Informacije o licenci - TV Uลพivo - Uฤitavanjeโ€ฆ - Obeleลพi celu seriju kao odgledanu? - Obeleลพi celu seriju kao neodgledanu? - Obeleลพi kao neodgledano - Obeleลพi kao odgledano - Maks bitrate - Sliฤno ovome - Joลก - Filmovi - Ime - Sledeฤ‡e - Nema podataka - Nema rezultata - Server nije pronaฤ‘en - Nema zakazanih snimaka - Nema dostupnih aลพuriranja - Nijedan - Samo Forsirani Titlovi - Outro - Putanja - Ljudi - Broj Prikaza - Pusti odavde - Pusti - Prikaลพi debug info reprodukcije - Brzina Reprodukcije - Reprodukcija - Plejlista - Plejliste - Pregled - Korisniฤka Podeลกavanja - Korisniฤka podeลกavanja sa servera - Red - Rekapitulacija - Nedavno dodato u %1$s - Nedavno dodato - Nedavno Snimljeno - Nedavno Objavljeno - Preporuฤeno - Snimi Program - Snimi Seriju - Ukloni iz omiljenih - Restart - Nastavi - Saฤuvaj - - Pretraลพi - Pretragaโ€ฆ - Glasovna pretraga - Zapoฤinje - Pretraลพi govorom - Obraฤ‘uje se - Pritisni nazad za prekid - Greลกka u audio zapisu - Greลกka u prepoznavanju glasa - Potrebna je dozvola za mikrofon - Greลกka u mreลพi - Mreลพa izgubljena - Nije prepoznat govor - Prepoznavanje govora zauzeto - Greลกka u Serveru - Nije detektovan govor - Nepoznata greลกka - Greลกka u pokretanju prepoznavanja glasa - Greลกka u prepoznavanju glasa - Pokuลกaj ponovo - Odaberi Server - Odaberi Korisnika diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 002d5ece..3b71517a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -686,9 +686,4 @@ Anvรคndarvisningar %1$sfilmer Kvalitetsprofil - HDR-format - HDR10 - HDR10+ - Tvinga aktivering - Tvinga avaktivering diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 70804da5..f591c3fb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -13,7 +13,7 @@ Dizi kaydฤฑnฤฑ iptal et ฤฐptal Bรถlรผmler - %1$s รถgesini seรงin + Seรง: %1$s Resim รถnbelleฤŸini temizle Koleksiyon Koleksiyonlar @@ -718,45 +718,4 @@ Oynatma listesinden kaldฤฑr Oturum sรผresi doldu, lรผtfen tekrar giriลŸ yapฤฑn ลžu anda sunucuya eriลŸim yetkiniz bulunmuyor - HDR10+ yerine Dolby Vision\'ฤฑ tercih et - Aksi takdirde bu videolarฤฑ HDR10+ olarak oynatacak TV\'ler iรงin, Dolby Vision videolardan HDR10+ meta verilerini kaldฤฑrฤฑr - DรถnรผลŸtรผrme รถncesi maksimum รงรถzรผnรผrlรผk - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - - %1$d Bรถlรผm - %1$d Bรถlรผm - - Yer AldฤฑฤŸฤฑ - En Yรผksek Puanlฤฑ - HDR Formatlarฤฑ - HDR10 - HDR10+ - Dolby Vision Profile 5 - Dolby Vision Profile 7 - Dolby Vision Profile 8 - Zorla EtkinleลŸtir - Zorla Devre DฤฑลŸฤฑ Bฤฑrak - Sunucu sรผrรผmรผ yakฤฑnda desteklenmeyecek - Gerekli sunucu sรผrรผmรผ: %1$s - Seรงen: %1$s - Kullanฤฑcฤฑ - Diziler - Sunucudan varsayฤฑlanlarฤฑ yรผkle - Yalnฤฑzca bu cihaza kaydet - Varsayฤฑlan - Cihaza รถzel - Sunucu kullanฤฑcฤฑ profili - Sunucu varsayฤฑlanฤฑ - Kullanฤฑcฤฑ web istemcisi - Sunucu tarafฤฑndan yรถnetiliyor - Hata - Dolby Vision Profile 7\'yi 8.1\'e DรถnรผลŸtรผr - Profil 8\'i destekleyen ancak Profil 7\'yi desteklemeyen รงรถzรผcรผlere sahip cihazlar iรงin; รงift katmanlฤฑ Profil 7\'yi รงรถzรผcรผye ulaลŸmadan รถnce tek katmanlฤฑ Profil 8.1\'e dรถnรผลŸtรผrรผr ve doฤŸrudan oynatฤฑr - Sanatรงฤฑ KaynaฤŸฤฑ diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c82ff0d8..7c10daab 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -697,36 +697,4 @@ ๅพžๆ’ญๆ”พๆธ…ๅ–ฎไธญ็งป้™ค ๆ‚จ็›ฎๅ‰็„กๅญ˜ๅ–ๆญคไผบๆœๅ™จ็š„ๆฌŠ้™ ็™ปๅ…ฅๆ†‘่ญ‰ๅทฒ้ŽๆœŸ๏ผŒ่ซ‹้‡ๆ–ฐ็™ปๅ…ฅ - - %1$d ้›† - - Dolby Vision ๅ„ชๅ…ˆๆ–ผ HDR10+ - ็งป้™ค Dolby Vision ๅฝฑ็‰‡ไธญ็š„ HDR10+ ไธญไป‹่ณ‡ๆ–™๏ผŒไปฅ่งฃๆฑบ้ƒจๅˆ†้›ป่ฆ–่ชคๆ’ญ็‚บ HDR10+ - ๆœ€้ซ˜่งฃๆžๅบฆ๏ผŒ่ถ…้Žๅฐ‡่ฝ‰็ขผ - 8K - 4K - 1080 - 720 - 576 - 480 - 360 - HDR10 - HDR10+ - ๆœๆฏ”่ฆ–็•Œ Profile 5 - ๆœๆฏ”่ฆ–็•Œ Profile 7 - ๆœๆฏ”่ฆ–็•Œ Profile 8 - ๅผทๅˆถๅ•Ÿ็”จ - ๅผทๅˆถๅœ็”จ - ไผบๆœๅ™จ็‰ˆๆœฌๅณๅฐ‡ๅœๆญขๆ”ฏๆด - ไผบๆœๅ™จ็‰ˆๆœฌ้œ€็‚บ๏ผš%1$s - ไฝฟ็”จ่€… - ๅЇ้›† - ๅพžไผบๆœๅ™จ่ผ‰ๅ…ฅ้ ่จญๅ€ผ - ๅƒ…ๅ„ฒๅญ˜ๆ–ผๆญค่ฃ็ฝฎ - ้ ่จญ - ้ซ˜่ฉ•ๅˆ† - HDR ๆ ผๅผ - ็”ฑ %1$s ็ฒพ้ธ - ๅฐ‡ๆœๆฏ”่ฆ–็•Œ Profile 7 ่ฝ‰ๆ›็‚บ 8.1 - ๅœจ่งฃ็ขผๅ™จ่™•็†ๅ‰๏ผŒๅฐ‡้›™ๅฑค Profile 7 ้‡ๆ–ฐๆ”นๅฏซ็‚บๅ–ฎๅฑค Profile 8.1๏ผŒ่ฎ“ไธๆ”ฏๆด Profile 7 ไฝ†ๆ”ฏๆด Profile 8 ็š„่ฃ็ฝฎ่ƒฝๅค ็›ดๆŽฅๆ’ญๆ”พๅฝฑ็‰‡ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fadeac7..7b8a3578 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - Wholphin - Wholphin + Wolfskin + Wolfskin About Active Recordings @@ -154,7 +154,6 @@ Switch %1$s left Top Rated Unwatched - Top Rated Trailers DVR Schedule Guide @@ -165,7 +164,6 @@ S%1$02dE%2$02d S%1$02dE%2$02d-E%3$02d E%1$d - Ep Season %1$d TV Shows Interface @@ -175,7 +173,7 @@ Video Scale Video Videos - Watch live + Watch liveg %1$d years old %1$d year old @@ -415,19 +413,6 @@ Image disk cache size (MB) Disable audio hardware offload Disabling offloading audio decoding may fix playback freezes on some devices - HDR formats - HDR10 - HDR10+ - Dolby Vision Profile 5 - Dolby Vision Profile 7 - Dolby Vision Profile 8 - Force enable - Force disable - - @string/automatic - @string/force_enable - @string/force_disable - View options Columns Spacing @@ -504,8 +489,6 @@ Ignores device compatibility checks Prefer Dolby Vision over HDR10+ Removes HDR10+ metadata from Dolby Vision videos, for TVs that otherwise play them as HDR10+ - Convert Dolby Vision Profile 7 to 8.1 - Rewrites dual-layer Profile 7 into single-layer Profile 8.1 before the decoder and direct plays it, for devices whose decoder lists Profile 8 but not Profile 7 Discover Request @@ -604,8 +587,8 @@ Fill Fill width Fill height - Wholphin Default - Wholphin Compact + Wolfskin Default + Wolfskin Compact Series Thumb images Episode Thumbnail images @@ -637,6 +620,7 @@ @string/black @string/red @string/brown + @string/wolfskin_theme Ignore @@ -807,11 +791,6 @@ Creator Artist Album artist - Artist source - - @string/artist - @string/album_artist - Album Popular songs Play next @@ -830,7 +809,7 @@ Select all Separate types Prefer showing logos for titles - Wholphin updated to %s + Wolfskin updated to %s Remove from continue watching Remove series from next up View series removed from next up @@ -940,8 +919,6 @@ Books Audio books Server version not supported - Server version will not be supported soon - Required server version: %1$s Appears on Dim screen after delay Dim percentage @@ -975,16 +952,4 @@ %1$d Episode %1$d Episodes - Chosen by %1$s - User - Series - Load default from server - Save only to this device - Default - Device specific - Server user profile - Server default - User web client - Managed by server - Error diff --git a/app/src/main/res/values/wolfskin.xml b/app/src/main/res/values/wolfskin.xml index 0308ae9e..2aca4d36 100644 --- a/app/src/main/res/values/wolfskin.xml +++ b/app/src/main/res/values/wolfskin.xml @@ -1,6 +1,6 @@ - Wolfskin screens + Wolfskin based on Wholphin by damontecres https://github.com/damontecres/Wholphin diff --git a/app/src/main/seerr/seerr-api.yml b/app/src/main/seerr/seerr-api.yml index dc493638..d174ccec 100644 --- a/app/src/main/seerr/seerr-api.yml +++ b/app/src/main/seerr/seerr-api.yml @@ -1097,61 +1097,6 @@ components: example: 0 description: Availability of the season in 4K readOnly: true - SeasonDetails: - type: object - description: The answer of the season endpoint. Everything TMDB may not know is nullable. - properties: - id: - type: integer - airDate: - type: string - nullable: true - name: - type: string - nullable: true - overview: - type: string - nullable: true - posterPath: - type: string - nullable: true - seasonNumber: - type: integer - episodes: - type: array - items: - $ref: '#/components/schemas/SeasonEpisode' - SeasonEpisode: - type: object - description: An episode of the season endpoint. voteAverage is TMDB's user score as a decimal, 0 when nobody voted. - properties: - id: - type: integer - name: - type: string - nullable: true - overview: - type: string - nullable: true - airDate: - type: string - nullable: true - episodeNumber: - type: integer - seasonNumber: - type: integer - showId: - type: integer - stillPath: - type: string - nullable: true - voteAverage: - type: number - nullable: true - voteCount: - type: integer - format: int64 - nullable: true TvShowStatus: type: string enum: ["Returning Series", "Planned", "In Production", "Ended", "Canceled", "Pilot"] @@ -6706,7 +6651,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SeasonDetails' + $ref: '#/components/schemas/Season' /tv/{tvId}/recommendations: get: summary: Get recommended TV series diff --git a/app/src/release/java/org/justinvisser/wolfskin/tv/DebugHome.kt b/app/src/release/java/org/justinvisser/wolfskin/tv/DebugHome.kt deleted file mode 100644 index b49e7828..00000000 --- a/app/src/release/java/org/justinvisser/wolfskin/tv/DebugHome.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import org.justinvisser.wolfskin.design.HomeSnapshot - -/** Release Home always reads the selected Jellyfin identity, never prototype data. */ -@Suppress("UNUSED_PARAMETER") -fun debugHomeSnapshot(context: Context, person: String): HomeSnapshot? = null diff --git a/app/src/test/java/com/github/damontecres/wholphin/data/model/BaseItemSubtitleTest.kt b/app/src/test/java/com/github/damontecres/wholphin/data/model/BaseItemSubtitleTest.kt deleted file mode 100644 index 11006322..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/data/model/BaseItemSubtitleTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.damontecres.wholphin.data.model - -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test -import java.util.UUID - -class BaseItemSubtitleTest { - private fun album( - albumArtist: String? = null, - artists: List? = null, - productionYear: Int? = null, - ) = BaseItem( - BaseItemDto( - id = UUID.randomUUID(), - type = BaseItemKind.MUSIC_ALBUM, - name = "Test Album", - albumArtist = albumArtist, - artists = artists, - productionYear = productionYear, - ), - ) - - @Test - fun albumPrefersAlbumArtist() { - val item = album(albumArtist = "Album Artist", artists = listOf("A", "B"), productionYear = 2021) - assertEquals("Album Artist - 2021", item.subtitle) - } - - @Test - fun albumFallsBackToArtists() { - val item = album(artists = listOf("A", "B"), productionYear = 2021) - assertEquals("A, B - 2021", item.subtitle) - } - - @Test - fun albumWithoutYear() { - assertEquals("Album Artist", album(albumArtist = "Album Artist").subtitle) - } - - @Test - fun albumWithoutArtist() { - assertEquals("2021", album(productionYear = 2021).subtitle) - assertNull(album(artists = emptyList()).subtitle) - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/data/model/RelativeMediaStreamTest.kt b/app/src/test/java/com/github/damontecres/wholphin/data/model/RelativeMediaStreamTest.kt deleted file mode 100644 index 283ee8ca..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/data/model/RelativeMediaStreamTest.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.github.damontecres.wholphin.data.model - -import com.github.damontecres.wholphin.test.TestTracks -import com.github.damontecres.wholphin.test.assertNotNull -import org.junit.Assert.assertEquals -import org.junit.Test - -class RelativeMediaStreamTest { - @Test - fun `Test getRelativeIndex`() { - val source = - TestTracks - .Builder() - .addSubtitle(2) - .addAudio(2) - .addVideo() - .buildMediaSourceInfo() - val streams = source.mediaStreams!! - RelativeMediaStream.getRelativeIndex(0, streams).let { relativeIndex -> - assertNotNull(relativeIndex) - assertEquals(0, relativeIndex) - } - RelativeMediaStream.getRelativeIndex(1, streams).let { relativeIndex -> - assertNotNull(relativeIndex) - assertEquals(1, relativeIndex) - } - RelativeMediaStream.getRelativeIndex(2, streams).let { relativeIndex -> - assertNotNull(relativeIndex) - assertEquals(0, relativeIndex) - } - RelativeMediaStream.getRelativeIndex(3, streams).let { relativeIndex -> - assertNotNull(relativeIndex) - assertEquals(1, relativeIndex) - } - } - - @Test - fun `Test getRelativeIndex for stream`() { - val source = - TestTracks - .Builder() - .addSubtitle(2) - .addAudio(2) - .addVideo() - .buildMediaSourceInfo() - val streams = source.mediaStreams!! - RelativeMediaStream.getRelativeIndex(streams[1], streams).let { relativeIndex -> - assertNotNull(relativeIndex) - assertEquals(1, relativeIndex) - } - } - - @Test - fun `Test asRelative basic`() { - val source = - TestTracks - .Builder() - .addSubtitle(2) - .addAudio(2) - .addVideo() - .buildMediaSourceInfo() - val streams = source.mediaStreams!! - assertEquals(5, streams.size) - - val relative = streams.asRelative() - // Subtitle - assertEquals(0, relative[0].relativeIndex) - assertEquals(0, relative[0].stream.index) - assertEquals(1, relative[1].relativeIndex) - assertEquals(1, relative[1].stream.index) - - // Audio - assertEquals(0, relative[2].relativeIndex) - assertEquals(2, relative[2].stream.index) - assertEquals(1, relative[3].relativeIndex) - assertEquals(3, relative[3].stream.index) - - // Video - assertEquals(0, relative[4].relativeIndex) - assertEquals(4, relative[4].stream.index) - } - - @Test - fun `Test asRelative with external`() { - val source = - TestTracks - .Builder() - .addExternalSubtitle(2) - .addSubtitle(2) - .addAudio(2) - .buildMediaSourceInfo() - val streams = source.mediaStreams!! - assertEquals(6, streams.size) - - val relative = streams.asRelative() - // External Subtitle - assertEquals(0, relative[0].relativeIndex) - assertEquals(0, relative[0].stream.index) - assertEquals(1, relative[1].relativeIndex) - assertEquals(1, relative[1].stream.index) - - // Subtitle - assertEquals(0, relative[2].relativeIndex) - assertEquals(2, relative[2].stream.index) - assertEquals(1, relative[3].relativeIndex) - assertEquals(3, relative[3].stream.index) - - // Audio - assertEquals(0, relative[4].relativeIndex) - assertEquals(4, relative[4].stream.index) - assertEquals(1, relative[5].relativeIndex) - assertEquals(5, relative[5].stream.index) - } - - @Test - fun `Test asRelative with external mixed`() { - val source = - TestTracks - .Builder() - .addExternalSubtitle(1) - .addSubtitle(1) - .addAudio(1) - .addExternalSubtitle(1) - .addAudio(1) - .addSubtitle(1) - .buildMediaSourceInfo() - val streams = source.mediaStreams!! - assertEquals(6, streams.size) - - val relative = streams.asRelative() - // External Subtitle - assertEquals(0, relative[0].relativeIndex) - assertEquals(0, relative[0].stream.index) - assertEquals(1, relative[3].relativeIndex) - assertEquals(3, relative[3].stream.index) - - // Subtitle - assertEquals(0, relative[1].relativeIndex) - assertEquals(1, relative[1].stream.index) - assertEquals(1, relative[5].relativeIndex) - assertEquals(5, relative[5].stream.index) - - // Audio - assertEquals(0, relative[2].relativeIndex) - assertEquals(2, relative[2].stream.index) - assertEquals(1, relative[4].relativeIndex) - assertEquals(4, relative[4].stream.index) - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt b/app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt index e5c12a56..41fee583 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt @@ -1,8 +1,10 @@ package com.github.damontecres.wholphin.data.model +import androidx.compose.ui.text.AnnotatedString import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Test @@ -102,103 +104,44 @@ class SourceRatingsTest { imdbVotes = 1, ) assertEquals(listOf("85%", "40%"), ratings.criticEntries.map { it.text }) - assertEquals(listOf(RatingBadge.RT_FRESH, RatingBadge.RT_SPILLED), ratings.criticEntries.map { it.badge }) + assertEquals(listOf(INLINE_RT_FRESH, INLINE_RT_SPILLED), ratings.criticEntries.map { it.inlineContentId }) assertEquals(listOf("8.0"), ratings.communityEntries.map { it.text }) - assertEquals(listOf(RatingBadge.IMDB), ratings.communityEntries.map { it.badge }) + assertEquals(listOf(INLINE_IMDB), ratings.communityEntries.map { it.inlineContentId }) + } + + private val jellyfinDetails = + QuickDetailsData( + basic = AnnotatedString("2020"), + communityRating = AnnotatedString("7.8 jellyfin"), + criticRating = AnnotatedString("59% jellyfin"), + ) + + @Test + fun jellyfinRatingsAreKeptWhenThereAreNoSourceRatings() { + assertSame(jellyfinDetails, jellyfinDetails.withSourceRatings(null)) + assertSame(jellyfinDetails, jellyfinDetails.withSourceRatings(SourceRatings())) } @Test - fun everyRtMarkHasItsOwnBadge() { - val badges = - listOf( - RtMark.FRESH to RatingBadge.RT_FRESH, - RtMark.ROTTEN to RatingBadge.RT_ROTTEN, - RtMark.UPRIGHT to RatingBadge.RT_UPRIGHT, - RtMark.SPILLED to RatingBadge.RT_SPILLED, - RtMark.UNKNOWN to RatingBadge.RT_UNKNOWN, + fun sourceRatingsReplaceJellyfinRatingsAndMapOntoTheExistingToggles() { + val details = + jellyfinDetails.withSourceRatings( + SourceRatings.of(rtCriticsScore = 96, rtCriticsLabel = "Certified Fresh", imdbScore = 8.8, imdbVotes = 10), ) - badges.forEach { (mark, badge) -> - assertEquals(badge, RatingEntry.RottenTomatoes(RtScore(50, mark), true).badge) - } - } - - private val movie = - SourceRatings.of( - rtCriticsScore = 96, - rtCriticsLabel = "Certified Fresh", - rtAudienceScore = 59, - rtAudienceLabel = "Spilled", - imdbScore = 8.8, - imdbVotes = 10, - ) - - @Test - fun visibleEntriesAreInPwaOrderAndFollowTheDisplaySettings() { - // Rotten Tomatoes (critic rating setting) before IMDb (community rating setting) - assertEquals( - listOf("96%", "59%", "8.8"), - movie.visibleEntries(community = true, critic = true).map { it.text }, - ) - assertEquals(listOf("8.8"), movie.visibleEntries(community = true, critic = false).map { it.text }) - assertEquals(listOf("96%", "59%"), movie.visibleEntries(community = false, critic = true).map { it.text }) - assertTrue(movie.visibleEntries(community = false, critic = false).isEmpty()) + // COMMUNITY_RATING toggle -> IMDb, CRITIC_RATING toggle -> Rotten Tomatoes + assertTrue(details.communityRating!!.text.endsWith("8.8")) + assertTrue(details.criticRating!!.text.endsWith("96%")) + assertEquals(jellyfinDetails.basic, details.basic) + // PWA order: Rotten Tomatoes before IMDb + assertTrue(details.criticRatingFirst) + assertFalse(jellyfinDetails.criticRatingFirst) } @Test - fun tmdbIsTheCommunityEntryWhenItIsTheOnlySource() { - val entries = SourceRatings.of(tmdbScore = 7.6, tmdbVotes = 100).visibleEntries(community = true, critic = true) - assertEquals(listOf(RatingBadge.TMDB), entries.map { it.badge }) - assertEquals(listOf("7.6"), entries.map { it.text }) - } - - @Test - fun noEntriesWithoutRatings() { - assertTrue(SourceRatings().isEmpty) - assertTrue(SourceRatings().visibleEntries(community = true, critic = true).isEmpty()) - assertFalse(movie.isEmpty) - } - - @Test - fun episodeScoreFollowsThePwaRulesForAUserScore() { - assertEquals(7.9, SourceRatings.episodeScore(7.9, 120)!!, 0.0) - // unrated, never 0 - assertNull(SourceRatings.episodeScore(0.0, 0)) - assertNull(SourceRatings.episodeScore(8.0, 0)) - assertNull(SourceRatings.episodeScore(8.0, null)) - // not observed or out of range - assertNull(SourceRatings.episodeScore(null, 10)) - assertNull(SourceRatings.episodeScore(10.5, 10)) - assertNull(SourceRatings.episodeScore(-1.0, 10)) - assertEquals(10.0, SourceRatings.episodeScore(10.0, 1)!!, 0.0) - } - - @Test - fun episodeKeepsTheSeriesRottenTomatoesAndHasOneLabelledTmdbNumber() { - val series = - SourceRatings.of(rtCriticsScore = 63, rtCriticsLabel = "Fresh", rtAudienceScore = 71, rtAudienceLabel = "Upright", tmdbScore = 7.6, tmdbVotes = 3000) - // the series only has its TMDB score because it has no other source - val withTmdbOnly = SourceRatings.of(tmdbScore = 7.6, tmdbVotes = 3000) - assertEquals(7.6, withTmdbOnly.tmdb!!, 0.0) - - val episode = withTmdbOnly.withEpisode(8.2) - assertNull(episode.tmdb) - val entries = episode.visibleEntries(community = true, critic = true) - assertEquals(listOf(RatingBadge.TMDB), entries.map { it.badge }) - assertEquals(listOf("8.2"), entries.map { it.text }) - assertEquals(listOf(true), entries.map { it.isEpisode }) - - val full = series.withEpisode(8.2).visibleEntries(community = true, critic = true) - assertEquals(listOf("63%", "71%", "8.2"), full.map { it.text }) - assertEquals(listOf(false, false, true), full.map { it.isEpisode }) - // the display settings treat it as a community (IMDb/TMDB) rating - assertEquals(listOf("8.2"), series.withEpisode(8.2).visibleEntries(community = true, critic = false).map { it.text }) - assertEquals(listOf("63%", "71%"), series.withEpisode(8.2).visibleEntries(community = false, critic = true).map { it.text }) - } - - @Test - fun withoutAnEpisodeScoreTheSeriesRatingsAreUnchanged() { - val series = SourceRatings.of(tmdbScore = 7.6, tmdbVotes = 10) - assertEquals(series, series.withEpisode(null)) - assertFalse(SourceRatings().withEpisode(5.0).isEmpty) + fun jellyfinValueForASourceThatSeerrDoesNotHaveIsNotMixedIn() { + // Seerr has only RT: the Jellyfin star is not shown next to it + val details = jellyfinDetails.withSourceRatings(SourceRatings.of(rtCriticsScore = 50, rtCriticsLabel = "Fresh")) + assertNull(details.communityRating) + assertTrue(details.criticRating!!.text.endsWith("50%")) } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/ImageUrlServiceTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/ImageUrlServiceTest.kt deleted file mode 100644 index d179c7e1..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/services/ImageUrlServiceTest.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.github.damontecres.wholphin.services - -import com.github.damontecres.wholphin.data.model.BaseItem -import io.mockk.every -import io.mockk.mockk -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.imageApi -import org.jellyfin.sdk.api.operations.ImageApi -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.serializer.toUUID -import org.junit.Assert -import org.junit.Before -import org.junit.Test - -class ImageUrlServiceTest { - private val mockApiClient = mockk() - private val imageUrlService = ImageUrlService(mockApiClient) - - private val itemId = UUID.randomUUID() - private val seriesId = UUID.randomUUID() - private val seasonId = UUID.randomUUID() - - @Before - fun setup() { - every { mockApiClient.baseUrl } returns "http://localhost" - every { mockApiClient.createUrl(any(), any(), any(), any()) } answers { callOriginal() } - every { mockApiClient.imageApi } returns ImageApi(mockApiClient) - } - - private fun episode( - imageTags: Map = emptyMap(), - parentLogoItemId: UUID? = null, - parentThumbItemId: UUID? = null, - parentBackdropItemId: UUID? = null, - ) = BaseItemDto( - id = itemId, - type = BaseItemKind.EPISODE, - seriesId = seriesId, - seasonId = seasonId, - imageTags = imageTags, - seriesPrimaryImageTag = "seriesPrimary", - parentLogoItemId = parentLogoItemId, - parentLogoImageTag = parentLogoItemId?.let { "parentLogo" }, - parentThumbItemId = parentThumbItemId, - parentThumbImageTag = parentThumbItemId?.let { "parentThumb" }, - parentBackdropItemId = parentBackdropItemId, - parentBackdropImageTags = parentBackdropItemId?.let { listOf("parentBackdrop0", "parentBackdrop1") }, - ) - - private fun movie( - imageTags: Map = emptyMap(), - backdropImageTags: List? = null, - ) = BaseItemDto( - id = itemId, - type = BaseItemKind.MOVIE, - imageTags = imageTags, - backdropImageTags = backdropImageTags, - ) - - private fun assertImage( - url: String?, - expectedId: UUID, - expectedType: ImageType, - expectedTag: String?, - ) { - Assert.assertNotNull(url) - val path = url!!.substringBefore('?') - Assert.assertEquals(expectedId, path.substringAfter("/Items/").substringBefore('/').toUUID()) - Assert.assertEquals(expectedType.serialName, path.substringAfter("/Images/")) - val tag = - url - .substringAfter('?', "") - .split('&') - .firstOrNull { it.startsWith("tag=") } - ?.substringAfter("tag=") - Assert.assertEquals(expectedTag, tag) - } - - @Test - fun `Own images use own tags`() { - val item = - BaseItem( - movie( - imageTags = - mapOf( - ImageType.PRIMARY to "primary", - ImageType.LOGO to "logo", - ImageType.THUMB to "thumb", - ImageType.BANNER to "banner", - ImageType.ART to "art", - ), - backdropImageTags = listOf("backdrop0", "backdrop1"), - ), - ) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.PRIMARY), itemId, ImageType.PRIMARY, "primary") - assertImage(imageUrlService.getItemImageUrl(item, ImageType.LOGO), itemId, ImageType.LOGO, "logo") - assertImage(imageUrlService.getItemImageUrl(item, ImageType.THUMB), itemId, ImageType.THUMB, "thumb") - assertImage(imageUrlService.getItemImageUrl(item, ImageType.BANNER), itemId, ImageType.BANNER, "banner") - assertImage(imageUrlService.getItemImageUrl(item, ImageType.ART), itemId, ImageType.ART, "art") - assertImage(imageUrlService.getItemImageUrl(item, ImageType.BACKDROP), itemId, ImageType.BACKDROP, "backdrop0") - } - - @Test - fun `Missing tags are omitted`() { - val item = BaseItem(movie()) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.PRIMARY), itemId, ImageType.PRIMARY, null) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.LOGO), itemId, ImageType.LOGO, null) - Assert.assertNull(imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)) - } - - @Test - fun `Thumb falls back to own backdrop`() { - val item = BaseItem(movie(backdropImageTags = listOf("backdrop0"))) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.THUMB), itemId, ImageType.BACKDROP, "backdrop0") - } - - @Test - fun `Episode primary uses series tag`() { - val item = BaseItem(episode(imageTags = mapOf(ImageType.PRIMARY to "primary")), useSeriesForPrimary = true) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.PRIMARY), seriesId, ImageType.PRIMARY, "seriesPrimary") - assertImage(imageUrlService.getItemImageUrl(item, ImageType.BANNER), seriesId, ImageType.BANNER, null) - assertImage( - imageUrlService.getItemImageUrl(item, ImageType.PRIMARY, useSeriesForPrimary = false), - itemId, - ImageType.PRIMARY, - "primary", - ) - } - - @Test - fun `Season without primary uses series tag`() { - val item = BaseItem(episode().copy(id = seasonId, type = BaseItemKind.SEASON, seasonId = null)) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.PRIMARY), seriesId, ImageType.PRIMARY, "seriesPrimary") - } - - @Test - fun `Episode logo uses parent logo tag only if from series`() { - assertImage( - imageUrlService.getItemImageUrl(BaseItem(episode(parentLogoItemId = seriesId)), ImageType.LOGO), - seriesId, - ImageType.LOGO, - "parentLogo", - ) - assertImage( - imageUrlService.getItemImageUrl(BaseItem(episode(parentLogoItemId = seasonId)), ImageType.LOGO), - seriesId, - ImageType.LOGO, - null, - ) - } - - @Test - fun `Episode backdrop uses parent backdrop tag only if from series`() { - assertImage( - imageUrlService.getItemImageUrl(BaseItem(episode(parentBackdropItemId = seriesId)), ImageType.BACKDROP), - seriesId, - ImageType.BACKDROP, - "parentBackdrop0", - ) - assertImage( - imageUrlService.getItemImageUrl(BaseItem(episode(parentBackdropItemId = seasonId)), ImageType.BACKDROP), - seriesId, - ImageType.BACKDROP, - null, - ) - } - - @Test - fun `Episode thumb uses parent tags`() { - assertImage( - imageUrlService.getItemImageUrl( - BaseItem(episode(parentThumbItemId = seriesId, parentBackdropItemId = seriesId), useSeriesForPrimary = true), - ImageType.THUMB, - ), - seriesId, - ImageType.THUMB, - "parentThumb", - ) - assertImage( - imageUrlService.getItemImageUrl( - BaseItem(episode(parentBackdropItemId = seriesId), useSeriesForPrimary = true), - ImageType.THUMB, - ), - seriesId, - ImageType.BACKDROP, - "parentBackdrop0", - ) - assertImage( - imageUrlService.getItemImageUrl( - BaseItem(episode(imageTags = mapOf(ImageType.PRIMARY to "primary")), useSeriesForPrimary = true), - ImageType.THUMB, - ), - itemId, - ImageType.PRIMARY, - "primary", - ) - } - - @Test - fun `Season without thumb uses parent thumb tag`() { - val item = BaseItem(episode(parentThumbItemId = seriesId).copy(id = seasonId, type = BaseItemKind.SEASON)) - assertImage(imageUrlService.getItemImageUrl(item, ImageType.THUMB), seriesId, ImageType.THUMB, "parentThumb") - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt index 2bcc6ab7..ee32c7dd 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt @@ -3,7 +3,7 @@ package com.github.damontecres.wholphin.services import android.app.SearchManager import android.content.Intent import com.github.damontecres.wholphin.data.CurrentUser -import com.github.damontecres.wholphin.data.RestoredSession +import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences @@ -20,6 +20,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest @@ -41,6 +42,7 @@ import org.robolectric.annotation.Config class IntentServiceTest { private val api: ApiClient = mockk() private val serverRepository: ServerRepository = mockk() + private val serverDao: JellyfinServerDao = mockk() private val userPreferencesService: UserPreferencesService = mockk() private val userLibraryApi: UserLibraryApi = mockk() @@ -62,6 +64,7 @@ class IntentServiceTest { intentService = IntentService(api, serverRepository, userPreferencesService) every { api.userLibraryApi } returns userLibraryApi coEvery { userLibraryApi.getItem(itemId) } returns successResponse(movie(itemId)) + every { serverRepository.serverDao } returns serverDao } private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) { @@ -79,11 +82,11 @@ class IntentServiceTest { private fun setupAutoSignInUnprotected() { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.Success(currentUser) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser } @Test @@ -95,7 +98,7 @@ class IntentServiceTest { val result = intentService.prepare(intent) assertNull(result) - coVerify { serverRepository.restoreLastSession() } + coVerify { serverRepository.restoreSession(serverId, userId) } } @Test @@ -103,17 +106,17 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = false + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.Success(currentUser) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser val intent = Intent() val result = intentService.prepare(intent) assertTrue(result is IntentResult.Error) - coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -121,17 +124,17 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(protectedCurrentUser) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.ServerOnly(protectedCurrentUser.server) + coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser val intent = Intent() val result = intentService.prepare(intent) assertTrue(result is IntentResult.Error) - coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -139,17 +142,17 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.ServerOnly(protectedCurrentUser.server) + coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser val intent = Intent() val result = intentService.prepare(intent) assertTrue(result is IntentResult.Error) - coVerify(exactly = 1) { serverRepository.restoreLastSession() } + coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -157,16 +160,17 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = true + currentServerId = "" + currentUserId = "" } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None // coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser val intent = Intent() val result = intentService.prepare(intent) assertTrue(result is IntentResult.Error) - coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -174,11 +178,12 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = true + currentServerId = "" + currentUserId = "" } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.Success(currentUser) + coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns currentUser.user + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser val intent = Intent().apply { @@ -188,7 +193,7 @@ class IntentServiceTest { val result = intentService.prepare(intent) assertNull(result) - coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -196,11 +201,12 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = true + currentServerId = "" + currentUserId = "" } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null -// coEvery { serverRepository.restoreLastSession() } returns -// RestoredSession.ServerOnly(protectedCurrentUser.server) + coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns protectedCurrentUser.user + coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser val intent = Intent().apply { @@ -210,7 +216,7 @@ class IntentServiceTest { val result = intentService.prepare(intent) assertTrue(result is IntentResult.Error) - coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -218,9 +224,12 @@ class IntentServiceTest { runTest { setupPreferences { signInAutomatically = true + currentServerId = "" + currentUserId = "" } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null + coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns null + coEvery { serverRepository.restoreSession(serverId, userId) } returns null val intent = Intent().apply { @@ -230,7 +239,7 @@ class IntentServiceTest { val result = intentService.prepare(intent) assertTrue(result is IntentResult.Error) - coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } } @Test @@ -287,11 +296,12 @@ class IntentServiceTest { every { serverRepository.current } returns MutableStateFlow(currentUser) coEvery { - serverRepository.tryChangeUser( + serverRepository.restoreSession( newServerId, newUserId, ) } returns newCurrentUser + every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user val intent = Intent().apply { @@ -306,8 +316,9 @@ class IntentServiceTest { assertEquals(0, destinations.size) assertTrue(result.addHomeToBackStack) - coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } - coVerify { serverRepository.tryChangeUser(newServerId, newUserId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } + verify { serverDao.getUser(newServerId, newUserId) } + coVerify { serverRepository.restoreSession(newServerId, newUserId) } coVerify(exactly = 0) { userLibraryApi.getItem(itemId) } } @@ -322,11 +333,12 @@ class IntentServiceTest { every { serverRepository.current } returns MutableStateFlow(currentUser) coEvery { - serverRepository.tryChangeUser( + serverRepository.restoreSession( newServerId, newUserId, ) } returns newCurrentUser + every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user val intent = Intent().apply { @@ -346,8 +358,9 @@ class IntentServiceTest { assertEquals(itemId, first.itemId) - coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } - coVerify { serverRepository.tryChangeUser(newServerId, newUserId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } + verify { serverDao.getUser(newServerId, newUserId) } + coVerify { serverRepository.restoreSession(newServerId, newUserId) } coVerify { userLibraryApi.getItem(itemId) } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt deleted file mode 100644 index adb5ca33..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt +++ /dev/null @@ -1,426 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.github.damontecres.wholphin.services - -import com.github.damontecres.wholphin.api.seerr.MoviesApi -import com.github.damontecres.wholphin.api.seerr.SeerrApiClient -import com.github.damontecres.wholphin.api.seerr.TvApi -import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingscombinedGet200Response -import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response -import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException -import com.github.damontecres.wholphin.api.seerr.model.SeasonDetails -import com.github.damontecres.wholphin.api.seerr.model.SeasonEpisode -import com.github.damontecres.wholphin.api.seerr.model.TvTvIdRatingsGet200Response -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.SeerrItemType -import com.github.damontecres.wholphin.ui.successResponse -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.api.operations.UserLibraryApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Test -import java.io.IOException -import java.util.UUID - -/** - * Which Jellyfin items have ratings, how their TMDB id is found, and when Seerr is asked - */ -class SeerrRatingsItemsTest { - private val moviesApi = mockk() - private val tvApi = mockk() - private val seerrApi = - mockk { - every { moviesApi } returns this@SeerrRatingsItemsTest.moviesApi - every { tvApi } returns this@SeerrRatingsItemsTest.tvApi - } - private val active = MutableStateFlow(true) - private val seerrService = - mockk { - every { api } returns seerrApi - every { this@mockk.active } returns this@SeerrRatingsItemsTest.active - } - private val userLibraryApi = mockk() - private val jellyfin = - mockk { - every { userLibraryApi } returns this@SeerrRatingsItemsTest.userLibraryApi - } - private val service = SeerrRatingsService(seerrService, jellyfin) - - private val seriesId = UUID.randomUUID() - private val seriesTmdb = 1396 - - private val rtOnly = MovieMovieIdRatingscombinedGet200Response(rt = MovieMovieIdRatingsGet200Response(criticsScore = 90, criticsRating = "Fresh")) - private val tvRatings = TvTvIdRatingsGet200Response(criticsScore = 80, criticsRating = "Fresh") - - private fun item( - type: BaseItemKind, - providerIds: Map? = null, - seriesId: UUID? = null, - ) = BaseItem(BaseItemDto(id = UUID.randomUUID(), type = type, providerIds = providerIds, seriesId = seriesId)) - - private fun jellyfinHasSeries(tmdb: Int? = seriesTmdb) { - coEvery { userLibraryApi.getItem(seriesId) } returns - successResponse( - BaseItemDto( - id = seriesId, - type = BaseItemKind.SERIES, - providerIds = tmdb?.let { mapOf("Tmdb" to it.toString()) } ?: mapOf("Tvdb" to "1"), - ), - ) - } - - @Test - fun moviesAndSeriesUseTheirOwnProviderIdsWithoutAskingJellyfin() = - runTest { - assertEquals( - RatingsTarget(SeerrItemType.MOVIE, 11), - service.resolveTarget(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "11"))), - ) - assertEquals( - RatingsTarget(SeerrItemType.TV, 22), - service.resolveTarget(item(BaseItemKind.SERIES, mapOf("Tmdb" to "22"))), - ) - coVerify(exactly = 0) { userLibraryApi.getItem(any()) } - } - - @Test - fun episodesAndSeasonsShowTheirSeriesRatingsNotTheirOwnTmdbId() = - runTest { - jellyfinHasSeries() - // The Tmdb id on an episode or season identifies the episode or season, never the series - val episode = item(BaseItemKind.EPISODE, mapOf("Tmdb" to "999999"), seriesId) - val season = item(BaseItemKind.SEASON, mapOf("Tmdb" to "888888"), seriesId) - - assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(episode)) - assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(season)) - } - - @Test - fun itemsThatArePartOfNoPwaTitleHaveNoRatingsAndNeverCallAnything() = - runTest { - val none = - listOf( - BaseItemKind.BOX_SET, - BaseItemKind.MUSIC_ALBUM, - BaseItemKind.MUSIC_ARTIST, - BaseItemKind.AUDIO, - BaseItemKind.PHOTO, - BaseItemKind.PHOTO_ALBUM, - BaseItemKind.PROGRAM, - BaseItemKind.PLAYLIST, - BaseItemKind.VIDEO, - ) - none.forEach { - val item = item(it, mapOf("Tmdb" to "5"), seriesId) - assertNull(it.name, service.resolveTarget(item)) - assertNull(it.name, service.forItem(item).first()) - assertNull(it.name, service.cachedFor(item)) - } - coVerify(exactly = 0) { userLibraryApi.getItem(any()) } - coVerify(exactly = 0) { moviesApi.movieMovieIdRatingscombinedGet(any()) } - coVerify(exactly = 0) { tvApi.tvTvIdRatingsGet(any()) } - } - - @Test - fun episodeWithoutASeriesHasNoRatings() = - runTest { - assertNull(service.resolveTarget(item(BaseItemKind.EPISODE, mapOf("Tmdb" to "3"), null))) - } - - @Test - fun homeItemsWithoutProviderIdsAreResolvedThroughJellyfinOnceAndCached() = - runTest { - jellyfinHasSeries() - val first = item(BaseItemKind.EPISODE, null, seriesId) - val second = item(BaseItemKind.EPISODE, null, seriesId) - - assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(first)) - assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(second)) - - // every other episode of the series is known without a request - coVerify(exactly = 1) { userLibraryApi.getItem(seriesId) } - } - - @Test - fun homeMovieWithoutProviderIdsLooksItUp() = - runTest { - val movie = item(BaseItemKind.MOVIE, null) - coEvery { userLibraryApi.getItem(movie.id) } returns - successResponse(BaseItemDto(id = movie.id, type = BaseItemKind.MOVIE, providerIds = mapOf("Tmdb" to "77"))) - - assertEquals(RatingsTarget(SeerrItemType.MOVIE, 77), service.resolveTarget(movie)) - } - - @Test - fun seriesWithoutTmdbIdHasNoRatingsAndIsNotLookedUpAgain() = - runTest { - jellyfinHasSeries(tmdb = null) - val episode = item(BaseItemKind.EPISODE, null, seriesId) - - assertNull(service.resolveTarget(episode)) - assertNull(service.forItem(episode).first()) - coVerify(exactly = 1) { userLibraryApi.getItem(seriesId) } - } - - @Test - fun failedLookupIsRetriedLater() = - runTest { - coEvery { userLibraryApi.getItem(seriesId) } throws IOException("down") - val episode = item(BaseItemKind.EPISODE, null, seriesId) - assertNull(service.resolveTarget(episode)) - - jellyfinHasSeries() - assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(episode)) - } - - @Test - fun episodeRatingsAreTheSeriesRatingsAndSeedLaterEpisodes() = - runTest { - jellyfinHasSeries() - coEvery { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } returns tvRatings - val episode = item(BaseItemKind.EPISODE, null, seriesId) - val other = item(BaseItemKind.EPISODE, null, seriesId) - assertNull(service.cachedFor(episode)) - - val ratings = service.forItem(episode).first() - - assertEquals(80, ratings?.rtCritics?.percent) - // moving to another episode of the same series needs no wait and no request - assertEquals(ratings, service.cachedFor(other)) - assertEquals(ratings, service.forItem(other).first()) - coVerify(exactly = 1) { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } - } - - @Test - fun scrollingPastItemsNeverAsksSeerr() = - runTest { - coEvery { moviesApi.movieMovieIdRatingscombinedGet(any()) } returns rtOnly - val jobs = - (1..20).map { id -> - val job = launch { service.forItem(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "$id"))).collect { } } - runCurrent() - // focus moves on before the wait has passed - advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS - 50) - job.cancel() - job - } - jobs.forEach(Job::cancel) - runCurrent() - - coVerify(exactly = 0) { moviesApi.movieMovieIdRatingscombinedGet(any()) } - } - - @Test - fun restingOnAnItemAsksSeerrOnceAfterTheWait() = - runTest { - coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 7) } returns rtOnly - var result: Any? = "unset" - val job = launch { service.forItem(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "7"))).collect { result = it } } - - advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS - 1) - runCurrent() - assertEquals("unset", result) - coVerify(exactly = 0) { moviesApi.movieMovieIdRatingscombinedGet(any()) } - - advanceTimeBy(2) - runCurrent() - assertNotNull(result) - coVerify(exactly = 1) { moviesApi.movieMovieIdRatingscombinedGet(movieId = 7) } - job.cancel() - } - - @Test - fun movingOnWhileTheRequestIsRunningCancelsIt() = - runTest { - var cancelled = false - coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 8) } coAnswers { - try { - awaitCancellation() - } finally { - cancelled = true - } - } - val job = launch { service.forItem(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "8"))).collect { } } - advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS + 1) - runCurrent() - coVerify(exactly = 1) { moviesApi.movieMovieIdRatingscombinedGet(movieId = 8) } - - job.cancel() - runCurrent() - - assertEquals(true, cancelled) - assertNull(service.cached(SeerrItemType.MOVIE, 8)) - } - - private fun season(vararg episodes: Triple) = - SeasonDetails( - seasonNumber = 5, - episodes = episodes.map { (number, average, votes) -> SeasonEpisode(episodeNumber = number, voteAverage = average?.toBigDecimal(), voteCount = votes) }, - ) - - private fun episode( - season: Int?, - episode: Int?, - ) = BaseItem(BaseItemDto(id = UUID.randomUUID(), type = BaseItemKind.EPISODE, seriesId = seriesId, parentIndexNumber = season, indexNumber = episode)) - - private fun seriesKnown() { - jellyfinHasSeries() - coEvery { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } returns tvRatings - } - - @Test - fun theSeasonIsFetchedOnceForEveryEpisodeOfIt() = - runTest { - seriesKnown() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 5) } returns - season(Triple(1, 8.24, 120L), Triple(2, 7.0, 80L), Triple(3, 9.5, 10L)) - - val first = service.forItem(episode(5, 1)).first()!! - val second = service.forItem(episode(5, 2)).first()!! - val third = service.forItem(episode(5, 3)).first()!! - - // each episode has its own score next to the series' Rotten Tomatoes - assertEquals(listOf(8.24, 7.0, 9.5), listOf(first, second, third).map { it.episodeTmdb }) - assertEquals(80, first.rtCritics?.percent) - coVerify(exactly = 1) { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 5) } - coVerify(exactly = 1) { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } - // and another episode of the season is known without waiting - assertEquals(7.0, service.cachedFor(episode(5, 2))!!.episodeTmdb!!, 0.0) - } - - @Test - fun eachSeasonIsItsOwnRequest() = - runTest { - seriesKnown() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 1) } returns season(Triple(1, 6.0, 5L)) - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 2) } returns season(Triple(1, 9.0, 5L)) - - assertEquals(6.0, service.forItem(episode(1, 1)).first()!!.episodeTmdb!!, 0.0) - assertEquals(9.0, service.forItem(episode(2, 1)).first()!!.episodeTmdb!!, 0.0) - // the same episode number of another season is not mistaken for it - assertNull(service.cachedFor(episode(3, 1))) - } - - @Test - fun unratedEpisodesShowOnlyTheSeriesRatings() = - runTest { - seriesKnown() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 5) } returns - season(Triple(1, 0.0, 0L), Triple(2, 8.0, null), Triple(3, null, null)) - - listOf(1, 2, 3, 4).forEach { number -> - val ratings = service.forItem(episode(5, number)).first()!! - assertNull("episode $number", ratings.episodeTmdb) - assertEquals("episode $number", 80, ratings.rtCritics?.percent) - } - } - - @Test - fun episodeWithoutSeasonOrEpisodeNumberShowsTheSeriesRatingsWithoutAskingForTheSeason() = - runTest { - seriesKnown() - - assertEquals(80, service.forItem(episode(null, 1)).first()!!.rtCritics?.percent) - assertEquals(80, service.forItem(episode(5, null)).first()!!.rtCritics?.percent) - coVerify(exactly = 0) { tvApi.tvTvIdSeasonSeasonNumberGet(any(), any(), any()) } - } - - @Test - fun episodeTmdbReplacesTheSeriesTmdbWhenThatWasTheOnlySource() = - runTest { - jellyfinHasSeries() - coEvery { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } returns TvTvIdRatingsGet200Response() - coEvery { tvApi.tvTvIdGet(tvId = seriesTmdb) } returns - mockk { - every { voteAverage } returns 7.6.toBigDecimal() - every { voteCount } returns 900 - } - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 5) } returns season(Triple(1, 8.8, 40L)) - - val ratings = service.forItem(episode(5, 1)).first()!! - - assertNull(ratings.tmdb) - assertEquals(8.8, ratings.episodeTmdb!!, 0.0) - } - - @Test - fun failedSeasonRequestKeepsTheSeriesRatingsAndIsRetried() = - runTest { - seriesKnown() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 5) } throws IOException("down") andThen - season(Triple(1, 8.0, 3L)) - - val first = service.forItem(episode(5, 1)).first()!! - assertNull(first.episodeTmdb) - assertEquals(80, first.rtCritics?.percent) - // not cached as "no score": the screen has to wait again - assertNull(service.cachedFor(episode(5, 1))) - - assertEquals(8.0, service.forItem(episode(5, 1)).first()!!.episodeTmdb!!, 0.0) - } - - @Test - fun seasonTmdbDoesNotKnowIsAnAnswer() = - runTest { - seriesKnown() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 9) } throws ClientException("none", 404) - - assertNull(service.forItem(episode(9, 1)).first()!!.episodeTmdb) - service.forItem(episode(9, 2)).first() - - coVerify(exactly = 1) { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 9) } - } - - @Test - fun seriesWithoutATmdbIdShowsNothingExtra() = - runTest { - jellyfinHasSeries(tmdb = null) - - assertNull(service.forItem(episode(5, 1)).first()) - coVerify(exactly = 0) { tvApi.tvTvIdSeasonSeasonNumberGet(any(), any(), any()) } - } - - @Test - fun seasonsStillShowOnlyTheirSeriesRatings() = - runTest { - seriesKnown() - val season = BaseItem(BaseItemDto(id = UUID.randomUUID(), type = BaseItemKind.SEASON, seriesId = seriesId, indexNumber = 5, parentIndexNumber = 5)) - - assertNull(service.forItem(season).first()!!.episodeTmdb) - coVerify(exactly = 0) { tvApi.tvTvIdSeasonSeasonNumberGet(any(), any(), any()) } - } - - @Test - fun scrollingPastEpisodesNeverFetchesASeason() = - runTest { - seriesKnown() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(any(), any(), any()) } returns season(Triple(1, 8.0, 3L)) - repeat(10) { number -> - val job = launch { service.forItem(episode(5, number + 1)).collect { } } - runCurrent() - advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS - 50) - job.cancel() - } - runCurrent() - - coVerify(exactly = 0) { tvApi.tvTvIdSeasonSeasonNumberGet(any(), any(), any()) } - coVerify(exactly = 0) { userLibraryApi.getItem(any()) } - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt index c6b008ea..2be93089 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt @@ -21,7 +21,6 @@ import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.junit.Assert.assertEquals @@ -46,7 +45,7 @@ class SeerrRatingsServiceTest { every { api } returns apiClient every { active } answers { activeFlow } } - private val service = SeerrRatingsService(seerrService, mockk()) + private val service = SeerrRatingsService(seerrService) private fun movieDetails( average: Double?, @@ -254,7 +253,8 @@ class SeerrRatingsServiceTest { // no TMDB id assertNull(service.forItem(jellyfinItem(BaseItemKind.MOVIE, null)).first()) assertNull(service.forItem(jellyfinItem(BaseItemKind.MOVIE, mapOf("Imdb" to "tt1"))).first()) - // not something that has ratings + // not a movie/series + assertNull(service.forItem(jellyfinItem(BaseItemKind.EPISODE, mapOf("Tmdb" to "1"))).first()) assertNull(service.forItem(jellyfinItem(BaseItemKind.BOX_SET, mapOf("Tmdb" to "1"))).first()) // call fails assertNull(service.forItem(jellyfinItem(BaseItemKind.SERIES, mapOf("Tmdb" to "2"))).first()) diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/ServerRepositoryTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/ServerRepositoryTest.kt index 2c2fdffe..499208d6 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/ServerRepositoryTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/ServerRepositoryTest.kt @@ -1,13 +1,11 @@ package com.github.damontecres.wholphin.services import android.content.Context +import android.content.SharedPreferences import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import com.github.damontecres.wholphin.data.CurrentUser import com.github.damontecres.wholphin.data.JellyfinServerDao -import com.github.damontecres.wholphin.data.MostRecentServer -import com.github.damontecres.wholphin.data.MostRecentServerProvider -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServerUsers @@ -16,7 +14,6 @@ import com.github.damontecres.wholphin.data.model.ServerUserConfig import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer import com.github.damontecres.wholphin.test.nonBlankString -import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.successResponse import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.WholphinDispatchers @@ -24,7 +21,6 @@ import com.github.damontecres.wholphin.util.configure import com.github.damontecres.wholphin.util.reset import io.mockk.Runs import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -33,6 +29,7 @@ import io.mockk.verify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest @@ -51,8 +48,6 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo import org.jellyfin.sdk.model.api.UserDto import org.junit.After import org.junit.Assert -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -85,13 +80,14 @@ class ServerRepositoryTest { private val mockUserApi = mockk() private val mockSystemApi = mockk() - private val mockMostRecentServerProvider = mockk() + private val mockSharedPreferences = mockk(relaxed = true) @OptIn(ExperimentalCoroutinesApi::class) @Before fun setup() { WholphinDispatchers.configure(testDispatcher) + every { mockContext.getSharedPreferences(any(), any()) } returns mockSharedPreferences every { mockApiClient.userApi } returns mockUserApi every { mockApiClient.systemApi } returns mockSystemApi coEvery { mockUserApi.getCurrentUser() } returns successResponse(userDto) @@ -106,15 +102,9 @@ class ServerRepositoryTest { emptyMap(), ) coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user - coEvery { mockJellyfinServerDao.getServer(serverId) } returns - JellyfinServerUsers(server, listOf(user)) - coEvery { mockJellyfinServerDao.addOrUpdateServer(server) } just Runs every { mockApiClient.clientInfo } returns ClientInfo("Wholphin test", "0.0.1") every { mockApiClient.deviceInfo } returns DeviceInfo("Wholphin test ID", "Wholphin test device") every { mockApiClient.update(any(), any(), any(), any()) } just Runs - coEvery { mockMostRecentServerProvider.save(any()) } just Runs - coEvery { mockMostRecentServerProvider.clear() } just Runs - coEvery { mockMostRecentServerProvider.clearUser() } just Runs } @OptIn(ExperimentalCoroutinesApi::class) @@ -129,8 +119,8 @@ class ServerRepositoryTest { mockJellyfin, mockJellyfinServerDao, mockApiClient, + dataStore, testDispatcher, - mockMostRecentServerProvider, ) private val serverId = UUID.randomUUID() @@ -204,24 +194,24 @@ class ServerRepositoryTest { Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto) - coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) } + val appPreferences = dataStore.data.first() + Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId) + Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId) + + verify(exactly = 1) { mockSharedPreferences.edit() } verify(exactly = 1) { mockJellyfinServerDao.addOrUpdateUser(user) } } @Test fun `Test restoreUser via changeUser`() = runTest { - val user = user.copy(pin = null) every { mockJellyfinServerDao.addOrUpdateServer(any()) } just Runs every { mockJellyfinServerDao.addOrUpdateUser(user) } returns user every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user)) val serverRepository = create() Assert.assertNull(serverRepository.currentUser) - val result = serverRepository.tryChangeUser(server.id, user.id) - assertTrue(result != null) - assertEquals(serverId, result?.server?.id) - assertEquals(user.id, result?.user?.id) + serverRepository.restoreSession(server.id, user.id) verify(exactly = 1) { mockApiClient.update( @@ -236,6 +226,34 @@ class ServerRepositoryTest { Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto) } + @Test + fun `Test restoreUser via shortcut`() = + runTest { + every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user)) + + val serverRepository = create() + setUpCurrentUser(serverRepository) + + serverRepository.restoreSession(server.id, user.id) + + verify(exactly = 1) { + mockApiClient.update( + baseUrl = server.url, + accessToken = user.accessToken, + clientInfo = any(), + deviceInfo = any(), + ) + } + verify(exactly = 0) { mockJellyfinServerDao.addOrUpdateServer(any()) } + verify(exactly = 0) { mockJellyfinServerDao.addOrUpdateUser(any()) } + + Assert.assertEquals(server, serverRepository.currentServer) + Assert.assertEquals(user, serverRepository.currentUser) + + // TODO fix this, need to check userDto too +// Assert.assertEquals(userDto, serverRepository.currentUserDto) + } + @Test fun `Test remove user`() = runTest { @@ -290,7 +308,6 @@ class ServerRepositoryTest { ) } verify(exactly = 1) { mockJellyfinServerDao.deleteUser(serverId, userId) } - coVerify { mockMostRecentServerProvider.clearUser() } } @Test @@ -334,7 +351,6 @@ class ServerRepositoryTest { ) } verify(exactly = 1) { mockJellyfinServerDao.deleteServer(serverId) } - coVerify { mockMostRecentServerProvider.clear() } } @Test @@ -371,7 +387,11 @@ class ServerRepositoryTest { Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto) - coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) } + val appPreferences = dataStore.data.first() + Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId) + Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId) + + verify(exactly = 1) { mockSharedPreferences.edit() } } @Test @@ -410,7 +430,11 @@ class ServerRepositoryTest { Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto) - coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) } + val appPreferences = dataStore.data.first() + Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId) + Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId) + + verify(exactly = 1) { mockSharedPreferences.edit() } } @Test @@ -460,99 +484,4 @@ class ServerRepositoryTest { } } } - - @Test - fun `Test restoreLastSession, success`() = - runTest { - val user = user.copy(pin = null) - coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user - coEvery { mockJellyfinServerDao.getServer(serverId) } returns - JellyfinServerUsers(server, listOf(user)) - every { mockMostRecentServerProvider.get() } returns - MostRecentServer.from(CurrentUser(server, user)) - - val result = create().restoreLastSession() - assertTrue(result is RestoredSession.Success) - val current = (result as RestoredSession.Success).currentUser - assertEquals(server, current.server) - assertEquals(user, current.user) - - verify(exactly = 1) { - mockApiClient.update( - baseUrl = server.url, - accessToken = user.accessToken, - clientInfo = any(), - deviceInfo = any(), - ) - } - coVerify { mockMostRecentServerProvider.save(any()) } - } - - @Test - fun `Test restoreLastSession, user protected`() = - runTest { - every { mockMostRecentServerProvider.get() } returns - MostRecentServer.ServerAndUser(serverId, server.url, userId, "token") - - val result = create().restoreLastSession() - assertTrue(result is RestoredSession.ServerOnly) - val current = (result as RestoredSession.ServerOnly) - assertEquals(server, current.server) - } - - @Test - fun `Test restoreLastSession, server only`() = - runTest { - every { mockMostRecentServerProvider.get() } returns - MostRecentServer.Server(server.id, server.url) - - val result = create().restoreLastSession() - assertTrue(result is RestoredSession.ServerOnly) - val current = (result as RestoredSession.ServerOnly) - assertEquals(server, current.server) - } - - @Test - fun `Test restoreLastSession, nothing to restore`() = - runTest { - every { mockMostRecentServerProvider.get() } returns MostRecentServer.None - - val result = create().restoreLastSession() - assertTrue(result is RestoredSession.None) - } -} - -class TestMostRecentServerProvider : MostRecentServerProvider { - var mostRecentServer: MostRecentServer = MostRecentServer.None - - override fun get(): MostRecentServer = mostRecentServer - - override suspend fun save(current: CurrentUser) { - mostRecentServer = - if (current.user.accessToken.isNotNullOrBlank()) { - MostRecentServer.ServerAndUser( - current.server.id, - current.server.url, - current.user.id, - current.user.accessToken, - ) - } else { - MostRecentServer.Server( - current.server.id, - current.server.url, - ) - } - } - - override suspend fun clear() { - mostRecentServer = MostRecentServer.None - } - - override suspend fun clearUser() { - when (val s = mostRecentServer) { - MostRecentServer.None -> Unit - is MostRecentServer.Server -> Unit - is MostRecentServer.ServerAndUser -> MostRecentServer.Server(s.serverId, s.serverUrl) - } - } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/StreamChoiceServiceTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/StreamChoiceServiceTest.kt index d8304897..b300e09a 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/StreamChoiceServiceTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/StreamChoiceServiceTest.kt @@ -1,12 +1,10 @@ package com.github.damontecres.wholphin.services -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao +import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.ActivationFlag import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.JellyfinUserPreferences -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType +import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice import com.github.damontecres.wholphin.data.model.ServerUserConfig import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.preferences.AppPreferences @@ -525,54 +523,6 @@ class TestStreamChoiceServiceOnlyForced( ), streamAudioLang = "eng", ), - TestInput( - 0, - SubtitlePlaybackMode.NONE, - userSubtitleLang = null, - subtitles = - listOf( - subtitle(0, "eng", forced = true), - subtitle(1, "spa", forced = false), - ), - streamAudioLang = "eng", - plc = plc(subtitlesOnlyForced = true), - ), - TestInput( - 0, - SubtitlePlaybackMode.DEFAULT, - userSubtitleLang = null, - subtitles = - listOf( - subtitle(0, "eng", forced = true), - subtitle(1, "spa", forced = true), - ), - streamAudioLang = "eng", - plc = plc(subtitlesOnlyForced = true), - ), - TestInput( - 0, - SubtitlePlaybackMode.DEFAULT, - userSubtitleLang = "eng", - subtitles = - listOf( - subtitle(0, "eng", forced = true), - subtitle(1, "spa", forced = false), - ), - streamAudioLang = "eng", - plc = plc(subtitlesOnlyForced = true), - ), - TestInput( - null, - SubtitlePlaybackMode.NONE, - userSubtitleLang = null, - subtitles = - listOf( - subtitle(0, "eng", forced = false), - subtitle(1, "spa", forced = false), - ), - streamAudioLang = "eng", - plc = plc(subtitlesOnlyForced = true), - ), ) } } @@ -935,7 +885,7 @@ data class TestInput( val streamAudioLang: String? = "eng", val subtitles: List, val itemPlayback: ItemPlayback? = null, - val plc: List = emptyList(), + val plc: PlaybackLanguageChoice? = null, val appAudioLang: String = UserProfileSettings.USE_USER_PROFILE, val appSubtitleLang: String = UserProfileSettings.USE_USER_PROFILE, val appSubtitleMode: SubtitleModePreference = SubtitleModePreference.USE_USER_PROFILE, @@ -945,7 +895,7 @@ data class TestInput( private fun MediaStream.toShortString(): String = "$type(index=$index, lang=$language, default=$isDefault, forced=$isForced)" -fun mockServerRepo( +private fun serverRepo( audioLang: String?, subtitleMode: SubtitlePlaybackMode?, subtitleLang: String?, @@ -968,15 +918,15 @@ fun mockServerRepo( private fun runTest(input: TestInput) { val service = StreamChoiceService( - mockServerRepo(input.userAudioLang, input.userSubtitleMode, input.userSubtitleLang), - mockk(), + serverRepo(input.userAudioLang, input.userSubtitleMode, input.userSubtitleLang), + mockk(), ) val result = service.chooseSubtitleStream( audioStreamLang = input.streamAudioLang, candidates = input.subtitles, itemPlayback = input.itemPlayback, - stc = input.plc, + playbackLanguageChoice = input.plc, prefs = UserPreferences( AppPreferences.getDefaultInstance(), @@ -987,7 +937,7 @@ private fun runTest(input: TestInput) { ), ), ) - Assert.assertEquals(input.expectedIndex, result.stream?.index) + Assert.assertEquals(input.expectedIndex, result?.index) } fun subtitle( @@ -995,8 +945,6 @@ fun subtitle( lang: String?, default: Boolean = false, forced: Boolean = false, - sdh: Boolean = false, - external: Boolean = false, title: String? = null, ): MediaStream = MediaStream( @@ -1004,26 +952,6 @@ fun subtitle( language = lang, isDefault = default, isForced = forced, - isHearingImpaired = sdh, - isInterlaced = false, - index = index, - isExternal = external, - isTextSubtitleStream = true, - supportsExternalStream = true, - title = title, - ) - -fun audio( - index: Int, - lang: String?, - default: Boolean = false, - title: String? = null, -): MediaStream = - MediaStream( - type = MediaStreamType.AUDIO, - language = lang, - isDefault = default, - isForced = false, isHearingImpaired = false, isInterlaced = false, index = index, @@ -1050,63 +978,11 @@ private fun plc( audioLang: String? = null, subtitleLang: String? = null, subtitlesDisabled: Boolean? = if (subtitleLang != null) false else null, - subtitlesOnlyForced: Boolean? = if (subtitleLang != null) false else null, -): List = - buildList { - audioLang - ?.let { - SeriesTrackChoice( - userId = 1, - language = audioLang, - parentId = UUID.randomUUID(), - type = SeriesTrackChoiceType.AUDIO, - activation = ActivationFlag.ACTIVATED, - trackFlags = 0, - codec = null, - relativeTrackIndex = null, - title = null, - audioChannels = null, - ) - }?.let(::add) - - if (subtitleLang != null) { - SeriesTrackChoice( - userId = 1, - language = subtitleLang, - parentId = UUID.randomUUID(), - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ACTIVATED, - trackFlags = 0, - codec = null, - relativeTrackIndex = null, - title = null, - audioChannels = null, - ).let(::add) - } else if (subtitlesDisabled == true) { - SeriesTrackChoice( - userId = 1, - language = null, - parentId = UUID.randomUUID(), - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.DISABLED, - trackFlags = 0, - codec = null, - relativeTrackIndex = null, - title = null, - audioChannels = null, - ).let(::add) - } else if (subtitlesOnlyForced == true) { - SeriesTrackChoice( - userId = 1, - language = null, - parentId = UUID.randomUUID(), - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ONLY_FORCED, - trackFlags = 0, - codec = null, - relativeTrackIndex = null, - title = null, - audioChannels = null, - ).let(::add) - } - } +): PlaybackLanguageChoice = + PlaybackLanguageChoice( + userId = 1, + seriesId = UUID.randomUUID(), + audioLanguage = audioLang, + subtitleLanguage = subtitleLang, + subtitlesDisabled = subtitlesDisabled, + ) diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/SuggestionsWorkerTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/SuggestionsWorkerTest.kt index 0e5e0eba..e066e837 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/SuggestionsWorkerTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/SuggestionsWorkerTest.kt @@ -5,10 +5,8 @@ import androidx.work.Data import androidx.work.ListenableWorker import androidx.work.WorkerParameters import com.github.damontecres.wholphin.data.CurrentUser -import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.test.currentUser import com.github.damontecres.wholphin.util.GetItemsRequestHandler import io.mockk.coEvery import io.mockk.coVerify @@ -115,16 +113,16 @@ class SuggestionsWorkerTest { val mockUser = mockk() var restored = false every { mockServerRepository.current } answers { MutableStateFlow(if (restored) mockUser else null) } - coEvery { mockServerRepository.restoreLastSession() } coAnswers { + coEvery { mockServerRepository.restoreSession(testServerId, testUserId) } coAnswers { restored = true - RestoredSession.Success(currentUser()) + mockUser } every { mockPreferences.data } returns flowOf(mockPrefs()) coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult() val result = createWorker().doWork() - coVerify { mockServerRepository.restoreLastSession() } + coVerify { mockServerRepository.restoreSession(testServerId, testUserId) } assertEquals(ListenableWorker.Result.success(), result) } diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestHdrFormat.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestHdrFormat.kt deleted file mode 100644 index e38479f0..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestHdrFormat.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.github.damontecres.wholphin.test - -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.HdrOverrideMode -import com.github.damontecres.wholphin.preferences.PlaybackOverrides -import com.github.damontecres.wholphin.preferences.getHdrRangeTypesFor -import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides -import com.github.damontecres.wholphin.util.profile.WholphinVideoRangeType -import org.junit.Assert -import org.junit.Test - -class TestHdrFormat { - private fun appPreferences(block: PlaybackOverrides.Builder.() -> Unit): AppPreferences = - AppPreferences.getDefaultInstance().updatePlaybackOverrides(block) - - @Test - fun `Test all auto returns empty sets`() { - val prefs = appPreferences { } - Assert.assertEquals(emptySet(), prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true)) - Assert.assertEquals(emptySet(), prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true)) - } - - @Test - fun `Test HDR10 override`() { - val prefs = appPreferences { hdr10Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE } - Assert.assertEquals( - setOf(WholphinVideoRangeType.HDR10.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true), - ) - Assert.assertEquals(emptySet(), prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true)) - } - - @Test - fun `Test Dolby Vision Profile 5 override`() { - val prefs = appPreferences { doviProfile5Override = HdrOverrideMode.HDR_OVERRIDE_DISABLE } - Assert.assertEquals( - setOf(WholphinVideoRangeType.DOVI.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true), - ) - } - - @Test - fun `Test Dolby Vision Profile 7 override requires Jellyfin 10_11`() { - val prefs = appPreferences { doviProfile7Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE } - Assert.assertEquals( - emptySet(), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = false), - ) - Assert.assertEquals( - setOf(WholphinVideoRangeType.DOVI_WITH_EL.serialName, WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true), - ) - } - - @Test - fun `Test Dolby Vision Profile 8 override`() { - val prefs = appPreferences { doviProfile8Override = HdrOverrideMode.HDR_OVERRIDE_DISABLE } - Assert.assertEquals( - setOf(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = false), - ) - Assert.assertEquals( - setOf(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName, WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true), - ) - } - - @Test - fun `Test multiple formats set to the same mode are unioned`() { - val prefs = - appPreferences { - hdr10Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE - hdr10PlusOverride = HdrOverrideMode.HDR_OVERRIDE_ENABLE - doviProfile5Override = HdrOverrideMode.HDR_OVERRIDE_DISABLE - } - Assert.assertEquals( - setOf(WholphinVideoRangeType.HDR10.serialName, WholphinVideoRangeType.HDR10_PLUS.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true), - ) - Assert.assertEquals( - setOf(WholphinVideoRangeType.DOVI.serialName), - prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true), - ) - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestHomeRowSamples.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestHomeRowSamples.kt index a6fe6e2a..e8c2fed1 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestHomeRowSamples.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestHomeRowSamples.kt @@ -1,9 +1,7 @@ package com.github.damontecres.wholphin.test -import com.github.damontecres.wholphin.data.model.HomeGetItemsFilter import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowViewOptions -import com.github.damontecres.wholphin.data.model.KeyValueEntry import com.github.damontecres.wholphin.preferences.PrefContentScale import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.ui.AspectRatio @@ -15,6 +13,7 @@ import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.junit.Assert import org.junit.Test import kotlin.reflect.KClass @@ -62,8 +61,8 @@ class TestHomeRowSamples { ), HomeRowConfig.GetItems( name = "Episodes by date created", - filter = - HomeGetItemsFilter( + getItems = + GetItemsRequest( parentId = UUID.randomUUID(), recursive = true, isFavorite = true, @@ -78,12 +77,6 @@ class TestHomeRowSamples { HomeRowConfig.TvPrograms(), HomeRowConfig.TvChannels(), HomeRowConfig.Suggestions(parentId = UUID.randomUUID()), - HomeRowConfig.CustomEndpoint( - endpoint = "/path/to/items", - title = "Items", - query = listOf(KeyValueEntry("key", "value")), - headers = listOf(KeyValueEntry("X-Header", "1234")), - ), ) } @@ -107,7 +100,6 @@ class TestHomeRowSamples { is HomeRowConfig.Suggestions -> foundTypes.add(it::class) is HomeRowConfig.TvChannels -> foundTypes.add(it::class) is HomeRowConfig.Studios -> foundTypes.add(it::class) - is HomeRowConfig.CustomEndpoint -> foundTypes.add(it::class) } } Assert.assertEquals(HomeRowConfig::class.sealedSubclasses.size, foundTypes.size) @@ -134,10 +126,12 @@ class TestHomeRowSamples { context = mockk(), api = mockk(), serverRepository = mockk(), + userPreferencesService = mockk(), navDrawerService = mockk(), + latestNextUpService = mockk(), + imageUrlService = mockk(), + suggestionService = mockk(), displayPreferencesService = mockk(), - serverPluginApi = mockk(), - serverDao = mockk(), ) val str = """{ @@ -155,7 +149,7 @@ class TestHomeRowSamples { ] }""" - val jsonElement = HomeSettingsService.jsonParser.parseToJsonElement(str) + val jsonElement = service.jsonParser.parseToJsonElement(str) val settings = service.decode(jsonElement) Assert.assertEquals(1, settings.rows.size) } diff --git a/app/src/test/java/com/github/damontecres/wholphin/TestMainActivityViewModel.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestMainActivityViewModel.kt similarity index 61% rename from app/src/test/java/com/github/damontecres/wholphin/TestMainActivityViewModel.kt rename to app/src/test/java/com/github/damontecres/wholphin/test/TestMainActivityViewModel.kt index 1de5becd..556a2fc0 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/TestMainActivityViewModel.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestMainActivityViewModel.kt @@ -1,12 +1,14 @@ -package com.github.damontecres.wholphin +package com.github.damontecres.wholphin.test import android.content.Context import android.content.Intent import android.widget.Toast import androidx.datastore.core.DataStore +import com.github.damontecres.wholphin.MainActivityViewModel import com.github.damontecres.wholphin.data.CurrentUser -import com.github.damontecres.wholphin.data.RestoredSession +import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.model.JellyfinServerUsers import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.update import com.github.damontecres.wholphin.services.AppUpgradeHandler @@ -17,10 +19,8 @@ import com.github.damontecres.wholphin.services.IntentService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupNavigationManager -import com.github.damontecres.wholphin.test.currentUser -import com.github.damontecres.wholphin.test.server -import com.github.damontecres.wholphin.test.user import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.WholphinDispatchers import com.github.damontecres.wholphin.util.configure import com.github.damontecres.wholphin.util.reset @@ -36,11 +36,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.jellyfin.sdk.model.UUID import org.junit.After -import org.junit.Assert -import org.junit.Assert.assertFalse +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -58,6 +58,7 @@ class TestMainActivityViewModel { private val backdropService: BackdropService = mockk(relaxed = true) private val appUpgradeHandler: AppUpgradeHandler = mockk() private val intentService: IntentService = mockk() + private val serverDao: JellyfinServerDao = mockk() val viewModel = MainActivityViewModel( @@ -73,7 +74,6 @@ class TestMainActivityViewModel { ) private val serverId = UUID.randomUUID() - private val server = server(serverId) private val userId = UUID.randomUUID() private val currentUser = currentUser(serverId, userId) @@ -88,6 +88,13 @@ class TestMainActivityViewModel { WholphinDispatchers.configure(testDispatcher) every { appUpgradeHandler.needUpgrade() } returns false every { appUpgradeHandler.copySubfont(any()) } returns Unit + every { serverRepository.serverDao } returns serverDao + coEvery { serverDao.getServer(serverId) } returns + JellyfinServerUsers( + currentUser.server, + // User list is unused + emptyList(), + ) mockkStatic(Toast::class) val mockToast = mockk() every { Toast.makeText(any(), any(), any()) } returns mockToast @@ -114,26 +121,22 @@ class TestMainActivityViewModel { runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) -// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser -// coEvery { serverRepository.restoreLastSession() } returns -// RestoredSession.Success(currentUser) + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser - viewModel.handleAppStart() + viewModel.appStart(null) + advanceUntilIdle() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.AppContent, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent) destination as SetupDestination.AppContent - Assert.assertEquals(currentUser, destination.current) + assertEquals(currentUser, destination.current) } - coVerify(exactly = 0) { serverRepository.restoreLastSession() } - coVerify(exactly = 0) { serverRepository.getMostRecentServer() } } @Test @@ -141,27 +144,27 @@ class TestMainActivityViewModel { runTest { setupPreferences { signInAutomatically = false + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) // coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser - coEvery { serverRepository.getMostRecentServer() } returns - RestoredSession.ServerOnly(server) - viewModel.handleAppStart() + viewModel.appStart(null) + advanceUntilIdle() - coVerify(exactly = 0) { serverRepository.tryChangeUser(any(), any()) } - coVerify(exactly = 0) { serverRepository.restoreLastSession() } - coVerify(exactly = 1) { serverRepository.getMostRecentServer() } + verify(exactly = 1) { serverDao.getServer(serverId) } + coVerify(exactly = 0) { serverRepository.restoreSession(any(), any()) } val args = mutableListOf() verify { setupNavigationManager.navigateTo(capture(args)) } - Assert.assertEquals(1, args.size) + assertEquals(2, args.size) args[0].let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.UserList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading) + } + args[1].let { destination -> + assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList) destination as SetupDestination.UserList - Assert.assertEquals(currentUser.server, destination.server) + assertEquals(currentUser.server, destination.server) } } @@ -170,30 +173,27 @@ class TestMainActivityViewModel { runTest { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } - every { serverRepository.current } returns - MutableStateFlow( - protectedCurrentUser, - ) - coEvery { - serverRepository.tryChangeUser(serverId, userId) - } returns null - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.ServerOnly(server) + every { serverRepository.current } returns MutableStateFlow(protectedCurrentUser) + coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser - viewModel.handleAppStart() + viewModel.appStart(null) + advanceUntilIdle() - coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } + verify { serverDao.getServer(serverId) } val args = mutableListOf() verify { setupNavigationManager.navigateTo(capture(args)) } - Assert.assertEquals(1, args.size) + assertEquals(2, args.size) args[0].let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.UserList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading) + } + args[1].let { destination -> + assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList) destination as SetupDestination.UserList - Assert.assertEquals(currentUser.server, destination.server) + assertEquals(currentUser.server, destination.server) } } @@ -202,26 +202,22 @@ class TestMainActivityViewModel { runTest { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.ServerOnly(server) - coEvery { - serverRepository.tryChangeUser(serverId, userId) - } returns null + coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser - viewModel.handleAppStart() + viewModel.appStart(null) + advanceUntilIdle() - coVerify(exactly = 1) { serverRepository.restoreLastSession() } + coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) } val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.UserList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList) destination as SetupDestination.UserList - Assert.assertEquals(currentUser.server, destination.server) + assertEquals(currentUser.server, destination.server) } } @@ -230,49 +226,45 @@ class TestMainActivityViewModel { runTest { setupPreferences { signInAutomatically = true + currentServerId = "" + currentUserId = "" } every { serverRepository.current } returns MutableStateFlow(null) - coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None + coEvery { serverRepository.restoreSession(null, null) } returns null - viewModel.handleAppStart() + viewModel.appStart(null) + advanceUntilIdle() - coVerify(exactly = 1) { serverRepository.restoreLastSession() } + coVerify(exactly = 1) { serverRepository.restoreSession(null, null) } val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.ServerList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList) } } @Test - fun `Test intent result is noop, hot reload`() = + fun `Test intent result is noop`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) - coEvery { serverRepository.restoreLastSession() } returns - RestoredSession.Success(currentUser) + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.NoOp - val result = viewModel.handleIntent(Intent()) - assertFalse(result) + viewModel.appStart(Intent()) + advanceUntilIdle() -// val slot = slot() -// verify { setupNavigationManager.navigateTo(capture(slot)) } -// slot.captured.let { destination -> -// Assert.assertTrue( -// "destination is ${destination::class}", -// destination is SetupDestination.AppContent, -// ) -// destination as SetupDestination.AppContent -// Assert.assertEquals(currentUser, destination.current) -// } - coVerify(exactly = 0) { serverRepository.restoreLastSession() } - coVerify(exactly = 0) { serverRepository.getMostRecentServer() } + val slot = slot() + verify { setupNavigationManager.navigateTo(capture(slot)) } + slot.captured.let { destination -> + assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent) + destination as SetupDestination.AppContent + assertEquals(currentUser, destination.current) + } } @Test @@ -280,22 +272,22 @@ class TestMainActivityViewModel { runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error") - val result = viewModel.handleIntent(Intent()) - assertTrue(result) + viewModel.appStart(Intent()) + advanceUntilIdle() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.UserList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList) destination as SetupDestination.UserList - Assert.assertEquals(currentUser.server, destination.server) + assertEquals(currentUser.server, destination.server) } } @@ -304,20 +296,20 @@ class TestMainActivityViewModel { runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(null) + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error") - val result = viewModel.handleIntent(Intent()) - assertTrue(result) + viewModel.appStart(Intent()) + advanceUntilIdle() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.ServerList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList) } } @@ -326,9 +318,11 @@ class TestMainActivityViewModel { runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) -// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Target( destinations = listOf(Destination.Favorites), @@ -337,27 +331,21 @@ class TestMainActivityViewModel { val backstack = mutableListOf() every { navigationManager.backStack } returns backstack - val result = viewModel.handleIntent(Intent()) - assertTrue(result) + viewModel.appStart(Intent()) + advanceUntilIdle() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } verify { navigationManager.reloadHome() } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.AppContent, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent) destination as SetupDestination.AppContent - Assert.assertEquals(currentUser, destination.current) + assertEquals(currentUser, destination.current) } // Note: navigation manager internally handles Home destination, so it won't be in this list - Assert.assertEquals(1, backstack.size) + assertEquals(1, backstack.size) backstack[0].let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is Destination.Favorites, - ) + assertTrue("destination is ${destination::class}", destination is Destination.Favorites) } } @@ -366,9 +354,11 @@ class TestMainActivityViewModel { runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(currentUser) -// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Target( destinations = listOf(Destination.Favorites), @@ -377,26 +367,20 @@ class TestMainActivityViewModel { val backstack = mutableListOf() every { navigationManager.backStack } returns backstack - val result = viewModel.handleIntent(Intent()) - assertTrue(result) + viewModel.appStart(Intent()) + advanceUntilIdle() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } verify(exactly = 0) { navigationManager.reloadHome() } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.AppContent, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent) destination as SetupDestination.AppContent - Assert.assertEquals(currentUser, destination.current) + assertEquals(currentUser, destination.current) } - Assert.assertEquals(1, backstack.size) + assertEquals(1, backstack.size) backstack[0].let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is Destination.Favorites, - ) + assertTrue("destination is ${destination::class}", destination is Destination.Favorites) } } @@ -405,9 +389,11 @@ class TestMainActivityViewModel { runTest(testDispatcher) { setupPreferences { signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() } every { serverRepository.current } returns MutableStateFlow(null) -// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Target( destinations = listOf(Destination.Favorites), @@ -416,17 +402,14 @@ class TestMainActivityViewModel { val backstack = mutableListOf() every { navigationManager.backStack } returns backstack - val result = viewModel.handleIntent(Intent()) - assertTrue(result) + viewModel.appStart(Intent()) + advanceUntilIdle() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } verify(exactly = 0) { navigationManager.reloadHome() } slot.captured.let { destination -> - Assert.assertTrue( - "destination is ${destination::class}", - destination is SetupDestination.ServerList, - ) + assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList) } } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestSeriesTrackChoice.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestSeriesTrackChoice.kt deleted file mode 100644 index e58d8aec..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestSeriesTrackChoice.kt +++ /dev/null @@ -1,534 +0,0 @@ -package com.github.damontecres.wholphin.test - -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao -import com.github.damontecres.wholphin.data.model.ActivationFlag -import com.github.damontecres.wholphin.data.model.ItemPlayback -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.data.model.JellyfinUserPreferences -import com.github.damontecres.wholphin.data.model.SeriesTrackChoice -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType -import com.github.damontecres.wholphin.data.model.TrackChoiceParentType -import com.github.damontecres.wholphin.data.model.TrackFlag -import com.github.damontecres.wholphin.data.model.TrackFlag.Companion.calculateFlag -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.StreamChoiceReason -import com.github.damontecres.wholphin.services.StreamChoiceService -import com.github.damontecres.wholphin.services.audio -import com.github.damontecres.wholphin.services.mockServerRepo -import com.github.damontecres.wholphin.services.subtitle -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.MediaStreamType -import org.jellyfin.sdk.model.api.SubtitlePlaybackMode -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -class TestSeriesTrackChoice { - private val mockServerRepository = - mockServerRepo( - audioLang = "eng", - subtitleLang = "eng", - subtitleMode = SubtitlePlaybackMode.DEFAULT, - ) - private val mockSeriesTrackChoiceDao = mockk(relaxed = false) - - private val streamChoiceService = - StreamChoiceService(mockServerRepository, mockSeriesTrackChoiceDao) - - private val seriesId = UUID.randomUUID() - private val season1Id = UUID.randomUUID() - private val season2Id = UUID.randomUUID() - private val season3Id = UUID.randomUUID() - - private val user = - JellyfinUser( - 1, - UUID.randomUUID(), - null, - UUID.randomUUID(), - null, - ) - - @Before - fun before() { - every { mockServerRepository.currentUser } returns user - } - - @Test - fun `Test StreamChoiceService call correct dao function`() = - runTest { - val mockSeriesTrackChoiceDao = mockk(relaxed = true) - val streamChoiceService = - StreamChoiceService(mockServerRepository, mockSeriesTrackChoiceDao) - - val seriesId = UUID.randomUUID() - val seasonId = UUID.randomUUID() - - // Has both series & season - BaseItemDto( - id = UUID.randomUUID(), - type = BaseItemKind.EPISODE, - seriesId = seriesId, - parentId = seasonId, - ).let { item -> - streamChoiceService.getSeriesTrackChoices(item, MediaStreamType.SUBTITLE) - coVerify(exactly = 1) { - mockSeriesTrackChoiceDao.get(any(), seriesId = seriesId, seasonId = seasonId, type = SeriesTrackChoiceType.SUBTITLE) - } - } - - // Has only series - BaseItemDto( - id = UUID.randomUUID(), - type = BaseItemKind.EPISODE, - seriesId = seriesId, - parentId = null, - ).let { item -> - streamChoiceService.getSeriesTrackChoices(item, MediaStreamType.SUBTITLE) - coVerify(exactly = 1) { - mockSeriesTrackChoiceDao.getBySeriesId(any(), seriesId = seriesId, type = SeriesTrackChoiceType.SUBTITLE) - } - } - - // Has only season - BaseItemDto( - id = UUID.randomUUID(), - type = BaseItemKind.EPISODE, - seriesId = null, - parentId = seasonId, - ).let { item -> - streamChoiceService.getSeriesTrackChoices(item, MediaStreamType.SUBTITLE) - coVerify(exactly = 1) { - mockSeriesTrackChoiceDao.getBySeasonId(any(), seasonId = seasonId, type = SeriesTrackChoiceType.SUBTITLE) - } - } - } - - @Test - fun `Test series disabled`() = - runTest { - val result = - streamChoiceService.chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true), - subtitle(1, "spa"), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.DISABLED, - ), - ), - ) - assertNull(result.stream) - } - - @Test - fun `Test series only forced`() = - runTest { - val mockServerRepository = - mockServerRepo( - audioLang = "eng", - subtitleLang = "eng", - subtitleMode = SubtitlePlaybackMode.NONE, - ) - val streamChoiceService = - StreamChoiceService(mockServerRepository, mockSeriesTrackChoiceDao) - - val result = - streamChoiceService.chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = - UserPreferences( - AppPreferences.getDefaultInstance(), - JellyfinUserPreferences(), - ), - candidates = - listOf( - subtitle(0, "eng", forced = true), - subtitle(1, "eng", forced = false), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ONLY_FORCED, - ), - ), - ) - assertNotNull(result.stream) - assertEquals(0, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Series) - } - - @Test - fun `Test series disabled with season`() = - runTest { - val result = - streamChoiceService.chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true), - subtitle(1, "spa"), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.DISABLED, - ), - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.DISABLED, - ), - ), - ) - assertNull(result.stream) - assertTrue(result.reason is StreamChoiceReason.Series) - } - - @Test - fun `Test item playback overrides STC for subtitles`() = - runTest { - val result = - streamChoiceService.chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = - ItemPlayback( - userId = user.rowId, - itemId = UUID.randomUUID(), - subtitleIndex = 1, - ), - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true), - subtitle(1, "spa"), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.DISABLED, - ), - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.DISABLED, - ), - ), - ) - assertNotNull(result.stream) - assertEquals(1, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Item) - } - - @Test - fun `Test item playback overrides STC for audio`() = - runTest { - val result = - streamChoiceService.chooseAudioStream( - itemPlayback = - ItemPlayback( - userId = user.rowId, - itemId = UUID.randomUUID(), - audioIndex = 1, - ), - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - audio(0, "eng", default = true), - audio(1, "spa"), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.AUDIO, - language = "eng", - ), - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.AUDIO, - language = "eng", - ), - ), - ) - assertNotNull(result.stream) - assertEquals(1, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Item) - } - - @Test - fun `Test season STC doesn't apply but series STC does - subtitle`() = - runTest { - val result = - streamChoiceService.chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = - ItemPlayback( - userId = user.rowId, - itemId = UUID.randomUUID(), - audioIndex = 1, - ), - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true), - subtitle(1, "spa"), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ONLY_FORCED, - ), - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ACTIVATED, - language = "spa", - ), - ), - ) - assertNotNull(result.stream) - assertEquals(1, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Series) - } - - @Test - fun `Test season STC doesn't apply but series STC does - audio`() = - runTest { - val result = - streamChoiceService.chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - audio(0, "eng", default = true), - audio(1, "spa"), - ), - stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.AUDIO, - activation = ActivationFlag.ONLY_FORCED, - ), - SeriesTrackChoice( - userId = user.rowId, - parentId = seriesId, - parentType = TrackChoiceParentType.SERIES, - type = SeriesTrackChoiceType.AUDIO, - activation = ActivationFlag.ACTIVATED, - language = "spa", - ), - ), - ) - assertNotNull(result.stream) - assertEquals(1, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Series) - } - - @Test - fun `Test STC has forced flag`() = - runTest { - val stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ACTIVATED, - trackFlags = listOf(TrackFlag.FORCED).calculateFlag(), - ), - ) - - // Choose forced track - streamChoiceService - .chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true, forced = true), - subtitle(1, "spa"), - ), - stc = stc, - ).let { result -> - assertNotNull(result.stream) - assertEquals(0, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Season) - } - - // No forced track to choose from, so nothing should be chosen - streamChoiceService - .chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = false, forced = false), - subtitle(1, "spa"), - ), - stc = stc, - ).let { result -> - assertNull(result.stream) - } - } - - @Test - fun `Test prefer SDH`() = - runTest { - val stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ACTIVATED, - trackFlags = listOf(TrackFlag.SDH).calculateFlag(), - ), - ) - - // Prefer SDH over default and non-SDH - streamChoiceService - .chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true, forced = true), - subtitle(1, "eng", default = false, forced = false, sdh = false), - subtitle(2, "eng", default = false, forced = false, sdh = true), - ), - stc = stc, - ).let { result -> - assertNotNull(result.stream) - assertEquals(2, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Season) - } - - // No SDH, so prefer regular over forced - streamChoiceService - .chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true, forced = true), - subtitle(1, "eng", default = false, forced = false, sdh = false), - ), - stc = stc, - ).let { result -> - assertNotNull(result.stream) - assertEquals(1, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Season) - } - } - - @Test - fun `Test prefer default track with no other flags`() = - runTest { - val stc = - listOf( - SeriesTrackChoice( - userId = user.rowId, - parentId = season1Id, - parentType = TrackChoiceParentType.SEASON, - type = SeriesTrackChoiceType.SUBTITLE, - activation = ActivationFlag.ACTIVATED, - trackFlags = 0, - ), - ) - - streamChoiceService - .chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = true, forced = false), - subtitle(1, "eng", default = false, forced = false), - ), - stc = stc, - ).let { result -> - assertNotNull(result.stream) - assertEquals(0, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Season) - } - - // Test out-of-order tracks - streamChoiceService - .chooseSubtitleStream( - audioStreamLang = "eng", - itemPlayback = null, - prefs = UserPreferences(AppPreferences.getDefaultInstance(), null), - candidates = - listOf( - subtitle(0, "eng", default = false, forced = false), - subtitle(1, "eng", default = true, forced = false), - ), - stc = stc, - ).let { result -> - assertNotNull(result.stream) - assertEquals(1, result.stream!!.index) - assertTrue(result.reason is StreamChoiceReason.Season) - } - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestTrackFlag.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestTrackFlag.kt deleted file mode 100644 index d2013f19..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestTrackFlag.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.damontecres.wholphin.test - -import com.github.damontecres.wholphin.data.model.TrackFlag -import com.github.damontecres.wholphin.data.model.TrackFlag.Companion.calculateFlag -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import java.util.EnumSet - -class TestTrackFlag { - @Test - fun basicTest() { - val flag = listOf(TrackFlag.DEFAULT, TrackFlag.EXTERNAL).calculateFlag() - assertTrue(TrackFlag.DEFAULT.within(flag)) - assertTrue(TrackFlag.EXTERNAL.within(flag)) - assertFalse(TrackFlag.FORCED.within(flag)) - assertFalse(TrackFlag.SDH.within(flag)) - } - - @Test - fun none() { - val flag = EnumSet.noneOf(TrackFlag::class.java).calculateFlag() - assertFalse(TrackFlag.DEFAULT.within(flag)) - assertFalse(TrackFlag.EXTERNAL.within(flag)) - assertFalse(TrackFlag.FORCED.within(flag)) - assertFalse(TrackFlag.SDH.within(flag)) - } - - @Test - fun all() { - val flag = EnumSet.allOf(TrackFlag::class.java).calculateFlag() - TrackFlag.entries.forEach { assertTrue(it.toString(), it.within(flag)) } - } - - @Test - fun checkFlags() { - TrackFlag.entries.forEach { - assertTrue("$it is not a power of two", it.flag > 0 && (it.flag and (it.flag - 1)) == 0) - } - val distinctFlags = TrackFlag.entries.map { it.flag }.distinct() - assertEquals(TrackFlag.entries.size, distinctFlags.size) - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt index c291d067..34d962da 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt @@ -11,6 +11,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.NameGuidPair import org.jellyfin.sdk.model.api.UserDto +import org.junit.Assert.assertNotNull import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -148,16 +149,3 @@ inline fun assertIs(obj: Any?) { Assert.fail("Expected type=${T::class.qualifiedName}, actual type= ${obj!!::class.qualifiedName} ") } } - -/** - * Asserts that the object is not null - * - * Includes a [contract] to smart-cast the object to T - */ -@OptIn(ExperimentalContracts::class) -fun assertNotNull(obj: Any?) { - contract { - returns() implies (obj != null) - } - org.junit.Assert.assertNotNull(obj) -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt index b266bb96..c4a54ec2 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt @@ -17,18 +17,15 @@ import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.Migrations -import com.github.damontecres.wholphin.data.MostRecentServerProvider import com.github.damontecres.wholphin.data.PlaybackEffectDao import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao import com.github.damontecres.wholphin.data.RememberedTabDao import com.github.damontecres.wholphin.data.SeerrServerDao -import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao import com.github.damontecres.wholphin.data.ServerPreferencesDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer import com.github.damontecres.wholphin.services.SeerrApi -import com.github.damontecres.wholphin.services.TestMostRecentServerProvider import com.github.damontecres.wholphin.services.hilt.AppModule import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.services.hilt.DatabaseModule @@ -204,10 +201,6 @@ object TestModule { fun seerrApi( @StandardOkHttpClient okHttpClient: OkHttpClient, ): SeerrApi = mockk() - - @Provides - @Singleton - fun mostRecentServerProvider(): MostRecentServerProvider = TestMostRecentServerProvider() } @Module @@ -267,10 +260,6 @@ object TestDatabaseModule { @Singleton fun rememberedTabDao(db: AppDatabase): RememberedTabDao = db.rememberedTabDao() - @Provides - @Singleton - fun seriesTrackChoiceTabDao(db: AppDatabase): SeriesTrackChoiceDao = db.seriesTrackChoiceDao() - @Provides @Singleton fun userPreferencesDataStore( diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt index e262e2ad..3ef4ad20 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt @@ -2,7 +2,7 @@ package com.github.damontecres.wholphin.ui import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance -import org.justinvisser.wolfskin.design.WolfskinColors +import com.github.damontecres.wholphin.ui.theme.colors.WolfskinThemeColors import org.junit.Assert.assertTrue import org.junit.Test @@ -10,8 +10,8 @@ import org.junit.Test class WolfskinThemeColorsTest { @Test fun darkPaletteHasReadableNormalAndFocusedText() { - val television = WolfskinColors.darkScheme - val material = WolfskinColors.darkSchemeMaterial + val television = WolfskinThemeColors.darkScheme + val material = WolfskinThemeColors.darkSchemeMaterial val textAndBackgrounds = listOf( television.onBackground to television.background, diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/components/CollectionFolderViewModelTest.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/components/CollectionFolderViewModelTest.kt index ba7f1e14..bc879d24 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/components/CollectionFolderViewModelTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/components/CollectionFolderViewModelTest.kt @@ -9,7 +9,6 @@ import com.github.damontecres.wholphin.services.DeletedItem import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.ui.successQueryResult import com.github.damontecres.wholphin.ui.successResponse -import com.github.damontecres.wholphin.util.GetAlbumArtistsHandler import com.github.damontecres.wholphin.util.GetArtistsHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetPersonsHandler @@ -35,7 +34,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest import org.jellyfin.sdk.model.api.request.GetArtistsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.junit.After @@ -59,7 +57,6 @@ class CollectionFolderViewModelTest { private val mockUserLibraryApi = mockk() private val artistsRequest = slot() - private val albumArtistsRequest = slot() private val itemsRequest = slot() private val libraryId = UUID.randomUUID() @@ -86,14 +83,11 @@ class CollectionFolderViewModelTest { // against the relaxed ApiClient mockkObject(GetItemsRequestHandler) mockkObject(GetArtistsHandler) - mockkObject(GetAlbumArtistsHandler) mockkObject(GetPersonsHandler) coEvery { GetItemsRequestHandler.execute(mockApi, capture(itemsRequest)) } returns successQueryResult(totalRecordCount = LIBRARY_ITEM_COUNT) coEvery { GetArtistsHandler.execute(mockApi, capture(artistsRequest)) } returns successQueryResult(totalRecordCount = ARTIST_COUNT) - coEvery { GetAlbumArtistsHandler.execute(mockApi, capture(albumArtistsRequest)) } returns - successQueryResult(totalRecordCount = ALBUM_ARTIST_COUNT) coEvery { GetPersonsHandler.execute(mockApi, any()) } returns successQueryResult(totalRecordCount = PERSON_COUNT) } @@ -103,36 +97,33 @@ class CollectionFolderViewModelTest { WholphinDispatchers.reset() unmockkObject(GetItemsRequestHandler) unmockkObject(GetArtistsHandler) - unmockkObject(GetAlbumArtistsHandler) unmockkObject(GetPersonsHandler) } - private fun createViewModel( - filter: GetItemsFilter, - viewOptions: ViewOptions = ViewOptionsSquare, - ) = CollectionFolderViewModel( - savedStateHandle = SavedStateHandle(), - api = mockApi, - context = mockk(relaxed = true), - serverRepository = mockServerRepository, - libraryDisplayInfoDao = mockk(relaxed = true), - favoriteWatchManager = mockk(relaxed = true), - backdropService = mockk(relaxed = true), - navigationManager = mockk(relaxed = true), - themeSongPlayer = mockk(relaxed = true), - userPreferencesService = mockk(relaxed = true), - mediaManagementService = mockMediaManagementService, - musicService = mockk(relaxed = true), - streamChoiceService = mockk(relaxed = true), - serverReportService = mockk(relaxed = true), - filterOptionCache = mockk(relaxed = true), - itemId = libraryId.toString(), - initialSortAndDirection = null, - recursive = true, - collectionFilter = CollectionFolderFilter(filter = filter), - useSeriesForPrimary = false, - defaultViewOptions = viewOptions, - ) + private fun createViewModel(filter: GetItemsFilter) = + CollectionFolderViewModel( + savedStateHandle = SavedStateHandle(), + api = mockApi, + context = mockk(relaxed = true), + serverRepository = mockServerRepository, + libraryDisplayInfoDao = mockk(relaxed = true), + favoriteWatchManager = mockk(relaxed = true), + backdropService = mockk(relaxed = true), + navigationManager = mockk(relaxed = true), + themeSongPlayer = mockk(relaxed = true), + userPreferencesService = mockk(relaxed = true), + mediaManagementService = mockMediaManagementService, + musicService = mockk(relaxed = true), + streamChoiceService = mockk(relaxed = true), + serverReportService = mockk(relaxed = true), + filterOptionCache = mockk(relaxed = true), + itemId = libraryId.toString(), + initialSortAndDirection = null, + recursive = true, + collectionFilter = CollectionFolderFilter(filter = filter), + useSeriesForPrimary = false, + defaultViewOptions = ViewOptionsSquare, + ) /** Counting via /Items would return every song and album, overshooting the artist grid */ @Test @@ -152,37 +143,6 @@ class CollectionFolderViewModelTest { coVerify(exactly = 0) { GetItemsRequestHandler.execute(any(), any()) } } - @Test - fun `positionOfLetter counts album artists for the ALBUM_ARTIST source`() = - runTest(testDispatcher) { - val viewModel = - createViewModel( - GetItemsFilter(override = GetItemsFilterOverride.ARTIST), - ViewOptionsSquare.copy(extras = ViewOptionsExtras.MusicLibrary(ArtistSource.ALBUM_ARTIST)), - ) - advanceUntilIdle() - - val position = viewModel.positionOfLetter('K') - - assertEquals(ALBUM_ARTIST_COUNT, position) - assertEquals("K", albumArtistsRequest.captured.nameLessThan) - assertEquals(libraryId, albumArtistsRequest.captured.parentId) - coVerify(exactly = 0) { GetArtistsHandler.execute(any(), any()) } - } - - @Test - fun `changing the artist source reloads from the album artists endpoint`() = - runTest(testDispatcher) { - val viewModel = createViewModel(GetItemsFilter(override = GetItemsFilterOverride.ARTIST)) - advanceUntilIdle() - coVerify(exactly = 0) { GetAlbumArtistsHandler.execute(any(), any()) } - - viewModel.saveViewOptions(ViewOptionsSquare.copy(extras = ViewOptionsExtras.MusicLibrary(ArtistSource.ALBUM_ARTIST))) - advanceUntilIdle() - - coVerify { GetAlbumArtistsHandler.execute(any(), any()) } - } - /** * Without SortName, the alphabet highlight falls back to Name and highlights "T" for artists * like "The Beatles" even though the grid sorts them under "B". @@ -237,7 +197,6 @@ class CollectionFolderViewModelTest { companion object { private const val LIBRARY_ITEM_COUNT = 7315 private const val ARTIST_COUNT = 730 - private const val ALBUM_ARTIST_COUNT = 215 private const val PERSON_COUNT = 12 } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt deleted file mode 100644 index 4fb3dc5c..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.github.damontecres.wholphin.ui.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import com.github.damontecres.wholphin.api.seerr.MoviesApi -import com.github.damontecres.wholphin.api.seerr.SeerrApiClient -import com.github.damontecres.wholphin.api.seerr.TvApi -import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response -import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingscombinedGet200Response -import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingscombinedGet200ResponseImdb -import com.github.damontecres.wholphin.api.seerr.model.SeasonDetails -import com.github.damontecres.wholphin.api.seerr.model.SeasonEpisode -import com.github.damontecres.wholphin.api.seerr.model.TvTvIdRatingsGet200Response -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.SourceRatings -import com.github.damontecres.wholphin.preferences.DisplayToggle -import com.github.damontecres.wholphin.services.SeerrRatingsService -import com.github.damontecres.wholphin.services.SeerrService -import com.github.damontecres.wholphin.ui.successResponse -import com.github.damontecres.wholphin.ui.theme.WholphinTheme -import com.github.damontecres.wholphin.ui.util.InterfaceCustomization -import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.api.operations.UserLibraryApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.util.EnumSet -import java.util.UUID - -/** - * What every surface shows: the rating row comes only from Seerr, per the PWA's rules for the kind of item, and - * the Jellyfin rating of an item is never drawn - */ -@Config(sdk = [34]) -@RunWith(RobolectricTestRunner::class) -class SourceRatingsRowTest { - @get:Rule val rule = createComposeRule() - - private val moviesApi = mockk() - private val tvApi = mockk() - private val seerrApi = - mockk { - every { moviesApi } returns this@SourceRatingsRowTest.moviesApi - every { tvApi } returns this@SourceRatingsRowTest.tvApi - } - private val seerrService = - mockk { - every { api } returns seerrApi - every { active } returns flowOf(true) - } - private val userLibraryApi = mockk() - private val jellyfin = mockk { every { userLibraryApi } returns this@SourceRatingsRowTest.userLibraryApi } - private val service = SeerrRatingsService(seerrService, jellyfin) - - private val seriesId = UUID.randomUUID() - private val seriesTmdb = 1396 - - /** Every item carries Jellyfin ratings that must never be drawn */ - private fun item( - type: BaseItemKind, - tmdb: Int? = null, - seriesId: UUID? = null, - ) = BaseItem( - BaseItemDto( - id = UUID.randomUUID(), - type = type, - name = "Title", - providerIds = tmdb?.let { mapOf("Tmdb" to it.toString()) }, - seriesId = seriesId, - communityRating = 7.8f, - criticRating = 59f, - ), - ) - - private fun movieRatings() { - coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 603) } returns - MovieMovieIdRatingscombinedGet200Response( - rt = MovieMovieIdRatingsGet200Response(criticsScore = 96, criticsRating = "Certified Fresh", audienceScore = 90, audienceRating = "Upright"), - imdb = MovieMovieIdRatingscombinedGet200ResponseImdb(criticsScore = 8.8.toBigDecimal(), criticsScoreCount = 10), - ) - } - - private fun seriesRatings() { - coEvery { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } returns - TvTvIdRatingsGet200Response(criticsScore = 81, criticsRating = "Fresh", audienceScore = 45, audienceRating = "Spilled") - coEvery { userLibraryApi.getItem(seriesId) } returns - successResponse(BaseItemDto(id = seriesId, type = BaseItemKind.SERIES, providerIds = mapOf("Tmdb" to "$seriesTmdb"))) - } - - /** - * Shows the quick details of the items, after what a screen's wait for Seerr would have loaded - */ - private fun show( - vararg items: BaseItem, - toggles: EnumSet = EnumSet.allOf(DisplayToggle::class.java), - ) { - runBlocking { - items.forEach { service.forItem(it, 0).collect { } } - } - rule.setContent { - WholphinTheme { - CompositionLocalProvider( - LocalSeerrRatingsService provides service, - LocalInterfaceCustomization provides InterfaceCustomization(toggles), - ) { - Column { - items.forEach { QuickDetails(it, null) } - } - } - } - } - rule.waitForIdle() - } - - private fun assertShown(text: String) = rule.onNodeWithText(text, substring = true).assertExists() - - private fun assertNotShown(text: String) = rule.onNodeWithText(text, substring = true).assertDoesNotExist() - - private fun assertJellyfinRatingNotShown() { - assertNotShown("7.8") - assertNotShown("59%") - } - - @Test - fun movieShowsRottenTomatoesThenImdbAndNotItsJellyfinRating() { - movieRatings() - show(item(BaseItemKind.MOVIE, 603)) - assertShown("96%") - assertShown("90%") - assertShown("8.8") - assertJellyfinRatingNotShown() - } - - @Test - fun seriesShowsItsRatings() { - seriesRatings() - show(item(BaseItemKind.SERIES, seriesTmdb)) - assertShown("81%") - assertShown("45%") - assertJellyfinRatingNotShown() - } - - @Test - fun episodeAndSeasonShowTheirSeriesRatings() { - seriesRatings() - show(item(BaseItemKind.EPISODE, seriesId = seriesId)) - assertShown("81%") - assertJellyfinRatingNotShown() - } - - private fun episodeWithScores(): BaseItem { - seriesRatings() - coEvery { tvApi.tvTvIdSeasonSeasonNumberGet(tvId = seriesTmdb, seasonNumber = 5) } returns - SeasonDetails(episodes = listOf(SeasonEpisode(episodeNumber = 1, voteAverage = 8.1.toBigDecimal(), voteCount = 50))) - return BaseItem( - BaseItemDto( - id = UUID.randomUUID(), - type = BaseItemKind.EPISODE, - providerIds = mapOf("Tmdb" to "331212"), - seriesId = seriesId, - parentIndexNumber = 5, - indexNumber = 1, - communityRating = 7.8f, - criticRating = 59f, - ), - ) - } - - @Test - fun episodeShowsItsOwnLabelledTmdbScoreNextToTheSeriesRottenTomatoes() { - show(episodeWithScores()) - assertShown("81%") - assertShown("45%") - assertShown("Ep 8.1") - assertJellyfinRatingNotShown() - } - - @Test - fun episodeScoreFollowsTheCommunityRatingSetting() { - show(episodeWithScores(), toggles = EnumSet.of(DisplayToggle.CRITIC_RATING)) - assertShown("81%") - assertNotShown("Ep 8.1") - } - - @Test - fun episodeScoreStaysWhenRottenTomatoesIsHidden() { - show(episodeWithScores(), toggles = EnumSet.of(DisplayToggle.COMMUNITY_RATING)) - assertShown("Ep 8.1") - assertNotShown("81%") - } - - @Test - fun seasonShowsItsSeriesRatings() { - seriesRatings() - show(item(BaseItemKind.SEASON, seriesId = seriesId)) - assertShown("45%") - assertJellyfinRatingNotShown() - } - - @Test - fun surfacesWithoutAPwaEquivalentShowNoRatingNotTheJellyfinStar() { - movieRatings() - seriesRatings() - show( - *listOf( - BaseItemKind.BOX_SET, - BaseItemKind.MUSIC_ALBUM, - BaseItemKind.MUSIC_ARTIST, - BaseItemKind.PHOTO, - BaseItemKind.PROGRAM, - ).map { item(it, 603, seriesId) }.toTypedArray(), - ) - assertNotShown("96%") - assertNotShown("81%") - assertJellyfinRatingNotShown() - } - - @Test - fun titleSeerrHasNoRatingsForShowsNoRatingNotTheJellyfinStar() { - coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 5) } returns MovieMovieIdRatingscombinedGet200Response() - coEvery { moviesApi.movieMovieIdGet(movieId = 5) } returns - mockk { - every { voteAverage } returns null - every { voteCount } returns null - } - show(item(BaseItemKind.MOVIE, 5)) - assertJellyfinRatingNotShown() - } - - @Test - fun theDisplaySettingsStillControlWhichSourcesAreDrawn() { - movieRatings() - show(item(BaseItemKind.MOVIE, 603), toggles = EnumSet.of(DisplayToggle.COMMUNITY_RATING)) - assertShown("8.8") - assertNotShown("96%") - } - - @Test - fun criticSettingHidesImdb() { - movieRatings() - show(item(BaseItemKind.MOVIE, 603), toggles = EnumSet.of(DisplayToggle.CRITIC_RATING)) - assertShown("96%") - assertNotShown("8.8") - } - - @Test - fun ratingsTextHasOneBadgeAndNumberPerEntryInOrder() { - val ratings = - SourceRatings.of( - rtCriticsScore = 96, - rtCriticsLabel = "Certified Fresh", - rtAudienceScore = 30, - rtAudienceLabel = "Spilled", - imdbScore = 8.8, - imdbVotes = 10, - ) - val entries = ratings.visibleEntries(community = true, critic = true) - val text = ratingsText(entries, leadingDot = false, episodeLabel = "Ep") - - assertTrue(text.text.indexOf("96%") < text.text.indexOf("30%")) - assertTrue(text.text.indexOf("30%") < text.text.indexOf("8.8")) - // dots only between entries, or also in front when something precedes the ratings - assertEquals(2, text.text.split("\u2022").size - 1) - assertEquals(3, ratingsText(entries, leadingDot = true, episodeLabel = "Ep").text.split("\u2022").size - 1) - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlayExternalViewModelTest.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlayExternalViewModelTest.kt deleted file mode 100644 index 511dbaa2..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlayExternalViewModelTest.kt +++ /dev/null @@ -1,276 +0,0 @@ -package com.github.damontecres.wholphin.ui.playback - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.lifecycle.SavedStateHandle -import com.github.damontecres.wholphin.data.ItemPlaybackDao -import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.Playlist -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.services.PlaylistCreationResult -import com.github.damontecres.wholphin.services.PlaylistCreator -import com.github.damontecres.wholphin.services.StreamChoiceReason -import com.github.damontecres.wholphin.services.StreamChoiceResult -import com.github.damontecres.wholphin.services.StreamChoiceService -import com.github.damontecres.wholphin.services.UserPreferencesService -import com.github.damontecres.wholphin.test.TestTracks -import com.github.damontecres.wholphin.test.movie -import com.github.damontecres.wholphin.test.playlist -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.preferences.ExternalPlayerApp -import com.github.damontecres.wholphin.ui.preferences.getExternalPlayers -import com.github.damontecres.wholphin.ui.successResponse -import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.WholphinDispatchers -import com.github.damontecres.wholphin.util.configure -import com.github.damontecres.wholphin.util.reset -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkStatic -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.playStateApi -import org.jellyfin.sdk.api.client.extensions.subtitleApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.api.client.extensions.videosApi -import org.jellyfin.sdk.api.operations.PlayStateApi -import org.jellyfin.sdk.api.operations.SubtitleApi -import org.jellyfin.sdk.api.operations.UserLibraryApi -import org.jellyfin.sdk.api.operations.VideosApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.extensions.inWholeTicks -import org.junit.After -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import kotlin.time.Duration.Companion.minutes - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, sdk = [30]) -class PlayExternalViewModelTest { - private val testDispatcher = StandardTestDispatcher() - private val context = mockk() - private val api = mockk() - private val userLibraryApi = mockk() - private val videosApi = mockk() - private val subtitleApi = mockk() - private val playStateApi = mockk(relaxed = true) - private val serverRepository = mockk() - private val itemPlaybackDao = mockk() - private val playlistCreator = mockk() - private val streamChoiceService = mockk() - private val navigationManager = mockk(relaxed = true) - private val userPreferencesService = mockk() - - private val movie = movie() - private val subtitleUrl = "http://localhost:8096/subtitles/0.srt" - private val videoUrl = "http://localhost:8096/video/stream" - private val mediaSource = - TestTracks - .Builder() - .addExternalSubtitle() - .buildMediaSourceInfo() - .let { source -> - source.copy( - id = "source-id", - runTimeTicks = 90.minutes.inWholeTicks, - mediaStreams = - source.mediaStreams!!.map { - it.copy(codec = "srt", path = "subtitle.srt", displayTitle = "English") - }, - ) - } - private val subtitle = mediaSource.mediaStreams!!.single() - private val vlc = ComponentName("org.videolan.vlc", "org.videolan.vlc.gui.video.VideoPlayerActivity") - - @OptIn(ExperimentalCoroutinesApi::class) - @Before - fun setUp() { - WholphinDispatchers.configure(testDispatcher) - mockkStatic(::getExternalPlayers) - every { getExternalPlayers(context) } returns - listOf(ExternalPlayerApp("VLC", null, vlc.flattenToString())) - every { api.userLibraryApi } returns userLibraryApi - every { api.videosApi } returns videosApi - every { api.subtitleApi } returns subtitleApi - every { api.playStateApi } returns playStateApi - every { serverRepository.currentUser } returns null - coEvery { userLibraryApi.getItem(movie.id) } returns successResponse(movie) - coEvery { streamChoiceService.chooseSource(any(), any()) } returns mediaSource - coEvery { streamChoiceService.chooseSubtitleStream(any(), any(), any(), any(), any(), any()) } returns - StreamChoiceResult(subtitle, StreamChoiceReason.Unknown) - coEvery { streamChoiceService.getSeriesTrackChoices(any(), SeriesTrackChoiceType.SUBTITLE) } returns emptyList() - every { - videosApi.getVideoStreamUrl( - itemId = movie.id, - mediaSourceId = mediaSource.id, - static = true, - ) - } returns videoUrl - every { - subtitleApi.getSubtitleUrl( - routeItemId = any(), - routeMediaSourceId = mediaSource.id!!, - routeIndex = subtitle.index, - routeFormat = "srt", - ) - } returns subtitleUrl - setupPlayer("") - } - - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun tearDown() { - unmockkStatic(::getExternalPlayers) - WholphinDispatchers.reset() - } - - private fun setupPlayer(playerId: String) { - val preferences = - AppPreferences.getDefaultInstance().updatePlaybackPreferences { - externalPlayer = playerId - } - coEvery { userPreferencesService.getCurrent() } returns UserPreferences(preferences, null) - } - - private fun createIntent(destination: Destination): Intent { - val viewModel = - PlayExternalViewModel( - SavedStateHandle(), - context, - api, - serverRepository, - itemPlaybackDao, - playlistCreator, - streamChoiceService, - navigationManager, - userPreferencesService, - ) - viewModel.init(destination) - testDispatcher.scheduler.advanceUntilIdle() - assertEquals(LoadingState.Success, viewModel.state.value.loading) - return viewModel.state.value.intent - } - - private fun assertStartFromBeginning(intent: Intent) { - assertEquals(true, intent.extras!!.get("from_start")) - assertEquals(0, intent.extras!!.get("position")) - } - - @Test - fun `Explicit VLC starts from beginning at zero position`() = - runTest(testDispatcher) { - setupPlayer(vlc.flattenToString()) - val intent = createIntent(Destination.Playback(movie.id, 0L)) - assertEquals(vlc, intent.component) - assertStartFromBeginning(intent) - } - - @Test - fun `Implicit player starts VLC from beginning at zero position`() = - runTest(testDispatcher) { - val intent = createIntent(Destination.Playback(movie.id, 0L)) - assertNull(intent.component) - assertStartFromBeginning(intent) - } - - @Test - fun `Unavailable selected player falls back to implicit start from beginning`() = - runTest(testDispatcher) { - setupPlayer("missing.player/missing.player.Activity") - val intent = createIntent(Destination.Playback(movie.id, 0L)) - assertNull(intent.component) - assertStartFromBeginning(intent) - } - - @Test - fun `Explicit VLC preserves positive resume position`() = - runTest(testDispatcher) { - setupPlayer(vlc.flattenToString()) - val intent = createIntent(Destination.Playback(movie.id, 42_000L)) - assertEquals(vlc, intent.component) - assertEquals(false, intent.extras!!.get("from_start")) - assertEquals(42_000, intent.extras!!.get("position")) - } - - @Test - fun `Implicit player preserves positive resume position`() = - runTest(testDispatcher) { - val intent = createIntent(Destination.Playback(movie.id, 42_000L)) - assertNull(intent.component) - assertEquals(false, intent.extras!!.get("from_start")) - assertEquals(42_000, intent.extras!!.get("position")) - } - - @Test - fun `Playback list starts from beginning`() = - runTest(testDispatcher) { - val playlist = playlist() - coEvery { userLibraryApi.getItem(playlist.id) } returns successResponse(playlist) - coEvery { playlistCreator.createFrom(any(), any(), any(), any(), any(), any()) } returns - PlaylistCreationResult.Success(Playlist.fromMedia(listOf(BaseItem(movie)))) - - val intent = createIntent(Destination.PlaybackList(playlist.id)) - assertStartFromBeginning(intent) - } - - @Test - fun `VLC starts from beginning without subtitles or duration`() = - runTest(testDispatcher) { - coEvery { streamChoiceService.chooseSource(any(), any()) } returns - mediaSource.copy(mediaStreams = emptyList(), runTimeTicks = null) - coEvery { streamChoiceService.chooseSubtitleStream(any(), any(), any(), any(), any(), any()) } returns null - - val intent = createIntent(Destination.Playback(movie.id, 0L)) - assertStartFromBeginning(intent) - assertFalse(intent.hasExtra("subtitles_location")) - assertFalse(intent.hasExtra("extra_duration")) - assertFalse(intent.hasExtra("subs.enable")) - assertFalse(intent.hasExtra("forcedsrt")) - } - - @Test - fun `Existing player extras and playback-start reporting remain unchanged`() = - runTest(testDispatcher) { - val intent = createIntent(Destination.Playback(movie.id, 42_000L)) - val title = "${BaseItem(movie).title} ${BaseItem(movie).subtitleLong}" - val subtitleUri = Uri.parse(subtitleUrl) - assertEquals(Intent.ACTION_VIEW, intent.action) - assertEquals(Uri.parse(videoUrl), intent.data) - assertEquals("video/*", intent.type) - assertEquals(title, intent.getStringExtra("title")) - assertEquals(42_000, intent.extras!!.get("position")) - assertTrue(intent.getBooleanExtra("return_result", false)) - assertTrue(intent.getBooleanExtra("secure_uri", false)) - assertArrayEquals(arrayOf(subtitleUri), intent.getParcelableArrayExtra("subs")) - assertArrayEquals(arrayOf("English"), intent.getStringArrayExtra("subs.name")) - assertArrayEquals(arrayOf(subtitleUri), intent.getParcelableArrayExtra("subs.enable")) - assertEquals(subtitleUrl, intent.getStringExtra("subtitles_location")) - assertEquals(90.minutes.inWholeMilliseconds, intent.extras!!.get("extra_duration")) - assertEquals(42_000, intent.extras!!.get("startfrom")) - assertFalse(intent.getBooleanExtra("forceresume", true)) - assertEquals(title, intent.getStringExtra("forcename")) - assertEquals(subtitleUri, intent.getParcelableExtra("forcedsrt")) - coVerify(exactly = 1) { playStateApi.reportPlaybackStart(any()) } - coVerify(exactly = 0) { playStateApi.reportPlaybackStopped(any()) } - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt index 87e5670a..21c6179b 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt @@ -11,7 +11,6 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.PlaylistItem -import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType import com.github.damontecres.wholphin.data.model.ServerUserConfig import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.PlaybackPreferences @@ -30,8 +29,6 @@ import com.github.damontecres.wholphin.services.PlaylistCreationResult import com.github.damontecres.wholphin.services.PlaylistCreator import com.github.damontecres.wholphin.services.RefreshRateService import com.github.damontecres.wholphin.services.ScreensaverService -import com.github.damontecres.wholphin.services.StreamChoiceReason -import com.github.damontecres.wholphin.services.StreamChoiceResult import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.test.TestTracks @@ -67,7 +64,6 @@ import org.jellyfin.sdk.api.operations.UserLibraryApi import org.jellyfin.sdk.api.operations.VideosApi import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.api.MediaSegmentDtoQueryResult import org.jellyfin.sdk.model.api.MediaSegmentType @@ -205,12 +201,7 @@ class PlaybackViewModelTests { coEvery { mockItemPlaybackDao.getItem(user, any()) } returns null coEvery { mockStreamChoiceService.chooseSource(any(), any()) } returns mediaSource - coEvery { - mockStreamChoiceService.getSeriesTrackChoices( - any(), - any(), - ) - } returns emptyList() + coEvery { mockStreamChoiceService.getPlaybackLanguageChoice(any()) } returns null coEvery { mockPlayerFactory.createVideoPlayer(any(), any()) } returns PlayerCreation(mockPlayer) every { mockPlayerFactory.createMediaSession(any()) } returns mockk(relaxed = true) @@ -223,26 +214,18 @@ class PlaybackViewModelTests { any(), any(), ) - } returns - StreamChoiceResult( - mediaSource.mediaStreams!!.first { it.type == MediaStreamType.AUDIO }, - StreamChoiceReason.Unknown, - ) + } returns mediaSource.mediaStreams!!.first { it.type == MediaStreamType.AUDIO } coEvery { mockStreamChoiceService.chooseSubtitleStream( - item = any(), source = mediaSource, - audioStream = any(), - itemPlayback = any(), - stc = any(), - prefs = any(), - ) - } returns - StreamChoiceResult( - mediaSource.mediaStreams!!.first { it.type == MediaStreamType.SUBTITLE }, - StreamChoiceReason.Unknown, + any(), + any(), + any(), + any(), + any(), ) + } returns mediaSource.mediaStreams!!.first { it.type == MediaStreamType.SUBTITLE } } @OptIn(ExperimentalCoroutinesApi::class) @@ -292,49 +275,6 @@ class PlaybackViewModelTests { return viewModel } - @Test - fun `Direct play includes the opened live stream ID`() = - runTest(testDispatcher) { - setupPreferences { cinemaMode = false } - val liveStreamId = "opened-live-stream" - val liveSource = mediaSource.copy(liveStreamId = liveStreamId) - coEvery { mockUserLibraryApi.getItem(movie.id) } returns successResponse(movie) - coEvery { mockMediaInfoApi.getPostedPlaybackInfo(any(), any()) } returns - successResponse( - PlaybackInfoResponse( - mediaSources = listOf(liveSource), - playSessionId = playSessionId, - ), - ) - every { - mockVideosApi.getVideoStreamUrl( - itemId = movie.id, - mediaSourceId = liveSource.id, - liveStreamId = liveSource.liveStreamId, - static = true, - tag = liveSource.eTag, - playSessionId = playSessionId, - ) - } returns videoStreamUrl - - createViewModel(Destination.Playback(movie.id, 0L)) - - val mediaItem = slot() - verify(exactly = 1) { mockPlayer.setMediaItem(capture(mediaItem), any()) } - val playbackUri = mediaItem.captured.localConfiguration?.uri - Assert.assertEquals(videoStreamUrl, playbackUri.toString()) - verify(exactly = 1) { - mockVideosApi.getVideoStreamUrl( - itemId = movie.id, - mediaSourceId = liveSource.id, - liveStreamId = liveStreamId, - static = true, - tag = liveSource.eTag, - playSessionId = playSessionId, - ) - } - } - @Test fun `Play intro first`() = runTest(testDispatcher) { diff --git a/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ConversionTest.kt b/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ConversionTest.kt deleted file mode 100644 index 483fa3ef..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ConversionTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.damontecres.wholphin.util - -import androidx.media3.common.DrmInitData -import androidx.media3.common.Format -import androidx.media3.common.MimeTypes -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertSame -import org.junit.Assert.assertTrue -import org.junit.Test - -class DolbyVisionProfile7ConversionTest { - @Test - fun recognisesProfile7InBothSampleEntryForms() { - assertTrue(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvhe.07.06"))) - assertTrue(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvh1.07.06"))) - } - - @Test - fun doesNotMistakeOtherProfilesOrCodecsForProfile7() { - assertFalse(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvhe.08.06"))) - assertFalse(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvhe.05.06"))) - assertFalse(DolbyVisionProfile7Conversion.isProfile7(dolbyVision(null))) - assertFalse( - DolbyVisionProfile7Conversion.isProfile7( - Format - .Builder() - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setCodecs("dvhe.07.06") - .build(), - ), - ) - } - - @Test - fun rewritesOnlyTheProfileOfTheCodecString() { - assertEquals("dvhe.08.06", DolbyVisionProfile7Conversion.toProfile8("dvhe.07.06")) - assertEquals("dvh1.08.09", DolbyVisionProfile7Conversion.toProfile8("dvh1.07.09")) - } - - @Test - fun advertisesAProfile7TrackAsProfile8AndKeepsTheRestOfTheFormat() { - val format = dolbyVision("dvhe.07.06") - - val advertised = DolbyVisionProfile7Conversion.advertiseAsProfile8(format) - - assertEquals("dvhe.08.06", advertised.codecs) - assertEquals(MimeTypes.VIDEO_DOLBY_VISION, advertised.sampleMimeType) - assertEquals(format.width, advertised.width) - assertEquals(format.height, advertised.height) - assertEquals(format.initializationData, advertised.initializationData) - } - - @Test - fun leavesTracksWhichAreNotProfile7AsTheSameInstance() { - val profile8 = dolbyVision("dvhe.08.06") - val hevc = Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H265).build() - - assertSame(profile8, DolbyVisionProfile7Conversion.advertiseAsProfile8(profile8)) - assertSame(hevc, DolbyVisionProfile7Conversion.advertiseAsProfile8(hevc)) - } - - @Test - fun leavesAnEncryptedProfile7TrackAlone() { - // Encrypted samples take the secure path, which the bitstream filter does not touch - val encrypted = - dolbyVision("dvhe.07.06") - .buildUpon() - .setDrmInitData(DrmInitData(DrmInitData.SchemeData(java.util.UUID.randomUUID(), MimeTypes.VIDEO_MP4, byteArrayOf(1)))) - .build() - - assertSame(encrypted, DolbyVisionProfile7Conversion.advertiseAsProfile8(encrypted)) - } - - private companion object { - fun dolbyVision(codecs: String?): Format = - Format - .Builder() - .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) - .setCodecs(codecs) - .setWidth(3840) - .setHeight(2160) - .setInitializationData(listOf(byteArrayOf(0x01, 0x02))) - .build() - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ExtractorsFactoryTest.kt b/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ExtractorsFactoryTest.kt deleted file mode 100644 index d888be15..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7ExtractorsFactoryTest.kt +++ /dev/null @@ -1,119 +0,0 @@ -@file:OptIn(markerClass = [UnstableApi::class]) - -package com.github.damontecres.wholphin.util - -import androidx.annotation.OptIn -import androidx.media3.common.C -import androidx.media3.common.Format -import androidx.media3.common.MimeTypes -import androidx.media3.common.util.ParsableByteArray -import androidx.media3.common.util.UnstableApi -import androidx.media3.extractor.Extractor -import androidx.media3.extractor.ExtractorOutput -import androidx.media3.extractor.ExtractorsFactory -import androidx.media3.extractor.TrackOutput -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import org.junit.Assert.assertEquals -import org.junit.Assert.assertSame -import org.junit.Test - -/** - * The factory has to reach the video track's format through three layers of media3 forwarding, - * rewrite it there and nowhere else, and let sample data through untouched. - */ -class DolbyVisionProfile7ExtractorsFactoryTest { - private val videoTrack = mockk(relaxed = true) - private val audioTrack = mockk(relaxed = true) - private val output = - mockk(relaxed = true) { - every { track(VIDEO_ID, C.TRACK_TYPE_VIDEO) } returns videoTrack - every { track(AUDIO_ID, C.TRACK_TYPE_AUDIO) } returns audioTrack - } - private val extractor = mockk(relaxed = true) - private val delegate = - mockk { - every { createExtractors() } returns arrayOf(extractor) - } - - @Test - fun advertisesTheVideoTrackAsProfile8() { - val video = initialisedVideoTrack() - - video.format(dolbyVision("dvhe.07.06")) - - val published = slot() - verify { videoTrack.format(capture(published)) } - assertEquals("dvhe.08.06", published.captured.codecs) - } - - @Test - fun leavesOtherVideoFormatsAsTheyAre() { - val video = initialisedVideoTrack() - val hevc = - Format - .Builder() - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setCodecs("hvc1.2.4.L153") - .build() - - video.format(hevc) - - verify { videoTrack.format(hevc) } - } - - @Test - fun leavesTheAudioTrackUnwrapped() { - val wrapped = wrappedOutput() - - assertSame(audioTrack, wrapped.track(AUDIO_ID, C.TRACK_TYPE_AUDIO)) - } - - @Test - fun passesSampleDataStraightThrough() { - val video = initialisedVideoTrack() - val data = ParsableByteArray(byteArrayOf(1, 2, 3)) - - video.sampleData(data, 3) - video.sampleMetadata(1_000L, C.BUFFER_FLAG_KEY_FRAME, 3, 0, null) - - verify { videoTrack.sampleData(data, 3) } - verify { videoTrack.sampleMetadata(1_000L, C.BUFFER_FLAG_KEY_FRAME, 3, 0, null) } - } - - @Test - fun wrapsEveryExtractorOfTheDelegate() { - val another = mockk(relaxed = true) - every { delegate.createExtractors() } returns arrayOf(extractor, another) - - val extractors = DolbyVisionProfile7ExtractorsFactory(delegate).createExtractors() - - assertEquals(2, extractors.size) - extractors[1].init(output) - verify { another.init(any()) } - } - - private fun wrappedOutput(): ExtractorOutput { - val extractors = DolbyVisionProfile7ExtractorsFactory(delegate).createExtractors() - extractors.single().init(output) - val wrapped = slot() - verify { extractor.init(capture(wrapped)) } - return wrapped.captured - } - - private fun initialisedVideoTrack(): TrackOutput = wrappedOutput().track(VIDEO_ID, C.TRACK_TYPE_VIDEO) - - private companion object { - const val VIDEO_ID = 1 - const val AUDIO_ID = 2 - - fun dolbyVision(codecs: String): Format = - Format - .Builder() - .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) - .setCodecs(codecs) - .build() - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7FilterTest.kt b/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7FilterTest.kt deleted file mode 100644 index b5c6189c..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/util/DolbyVisionProfile7FilterTest.kt +++ /dev/null @@ -1,293 +0,0 @@ -package com.github.damontecres.wholphin.util - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Test -import java.nio.ByteBuffer - -/** - * The RPU rewrite itself is libdovi's and runs on a device; here it is a fake, and what is under - * test is everything around it: which access units are touched at all, that the enhancement layer - * goes and the rest stays contiguous, that a refused rewrite leaves the decoder's buffer exactly as - * it was, and that the filter keeps to the [BitstreamFilter] contract on a direct buffer. - */ -class DolbyVisionProfile7FilterTest { - @Test - fun dropsTheEnhancementLayerAndRewritesTheRpu() { - val filter = filterWith(FakeRewriter(profile = 7)) - val target = directBuffer(annexB(VPS, VCL, EL, EL, RPU)) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(annexB(VPS, VCL, CONVERTED_RPU).toList(), target.bytes(0, size)) - } - - @Test - fun convertsAnRpuWhichArrivedWithoutAnEnhancementLayer() { - // A dual layer remux whose enhancement layer lives in Matroska block additions, which the - // extractor discards: the RPU still needs its rewrite for the profile 8 decoder - val filter = filterWith(FakeRewriter(profile = 7)) - val target = directBuffer(annexB(VCL, RPU)) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(annexB(VCL, CONVERTED_RPU).toList(), target.bytes(0, size)) - } - - @Test - fun worksFromAnOffsetAndLeavesTheBytesAroundTheAccessUnitAlone() { - val filter = filterWith(FakeRewriter(profile = 7)) - val accessUnit = annexB(VCL, EL, RPU) - val prefix = byteArrayOf(0x7A, 0x7B) - val suffix = byteArrayOf(0x7C) - val target = directBuffer(prefix + accessUnit + suffix) - - val size = filter.filter(target, prefix.size, accessUnit.size) - - assertEquals(annexB(VCL, CONVERTED_RPU).toList(), target.bytes(prefix.size, size)) - assertEquals(prefix.toList(), target.bytes(0, prefix.size)) - assertEquals(suffix.toList(), target.bytes(prefix.size + accessUnit.size, suffix.size)) - } - - @Test - fun doesNotMoveThePositionOrLimitOfTheBuffer() { - val filter = filterWith(FakeRewriter(profile = 7)) - val target = directBuffer(annexB(VCL, EL, RPU)) - target.position(3) - target.limit(target.capacity() - 1) - - filter.filter(target, 0, target.capacity()) - - assertEquals(3, target.position()) - assertEquals(target.capacity() - 1, target.limit()) - } - - @Test - fun leavesAnAccessUnitWithoutAnRpuAlone() { - val rewriter = FakeRewriter(profile = 7) - val filter = filterWith(rewriter) - val accessUnit = annexB(VPS, VCL, EL) - val target = directBuffer(accessUnit) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(accessUnit.size, size) - assertEquals(accessUnit.toList(), target.bytes(0, size)) - assertEquals(0, rewriter.conversions) - } - - @Test - fun leavesAnRpuOfAnotherProfileAlone() { - val rewriter = FakeRewriter(profile = 8) - val filter = filterWith(rewriter) - val accessUnit = annexB(VCL, RPU) - val target = directBuffer(accessUnit) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(accessUnit.size, size) - assertEquals(accessUnit.toList(), target.bytes(0, size)) - assertEquals(0, rewriter.conversions) - } - - @Test - fun leavesAnAccessUnitAloneWhenTheRewriteThrows() { - val filter = filterWith(FakeRewriter(profile = 7, throws = true)) - val accessUnit = annexB(VCL, EL, RPU) - val target = directBuffer(accessUnit) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(accessUnit.size, size) - assertEquals(accessUnit.toList(), target.bytes(0, size)) - } - - @Test - fun leavesAnAccessUnitAloneWhenTheRewriteWouldNotFit() { - // No enhancement layer to make room, and an RPU which comes out longer than it went in - val filter = filterWith(FakeRewriter(profile = 7, converted = RPU + ByteArray(16) { 0x55 })) - val accessUnit = annexB(VCL, RPU) - val target = directBuffer(accessUnit) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(accessUnit.size, size) - assertEquals(accessUnit.toList(), target.bytes(0, size)) - } - - @Test - fun takesALongerRpuWhenTheEnhancementLayerMadeRoomForIt() { - val longerRpu = RPU + byteArrayOf(0x55, 0x56) - val filter = filterWith(FakeRewriter(profile = 7, converted = longerRpu)) - val target = directBuffer(annexB(VCL, EL, RPU)) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(annexB(VCL, longerRpu).toList(), target.bytes(0, size)) - } - - @Test - fun leavesAnAccessUnitAloneWhenTheRewriteReportsNonsense() { - val filter = filterWith(FakeRewriter(profile = 7, reportedSize = -1)) - val accessUnit = annexB(VCL, EL, RPU) - val target = directBuffer(accessUnit) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(accessUnit.size, size) - assertEquals(accessUnit.toList(), target.bytes(0, size)) - } - - @Test - fun leavesDataWhichIsNotAnnexBAlone() { - val rewriter = FakeRewriter(profile = 7) - val filter = filterWith(rewriter) - val data = byteArrayOf(0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x01, 0x7C) - val target = directBuffer(data) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals(data.size, size) - assertEquals(data.toList(), target.bytes(0, size)) - assertEquals(0, rewriter.conversions) - } - - @Test - fun walksThreeByteStartCodesToo() { - val filter = filterWith(FakeRewriter(profile = 7)) - val accessUnit = SHORT_START_CODE + VCL + SHORT_START_CODE + EL + SHORT_START_CODE + RPU - val target = directBuffer(accessUnit) - - val size = filter.filter(target, 0, target.capacity()) - - assertEquals((SHORT_START_CODE + VCL + SHORT_START_CODE + CONVERTED_RPU).toList(), target.bytes(0, size)) - } - - @Test - fun givesUpForGoodWhenTheNativeLibraryIsMissing() { - var attempts = 0 - val filter = - DolbyVisionProfile7Filter { - attempts++ - throw UnsatisfiedLinkError("no libdovi_android for this ABI") - } - val accessUnit = annexB(VCL, EL, RPU) - val target = directBuffer(accessUnit) - - val first = filter.filter(target, 0, target.capacity()) - val second = filter.filter(target, 0, target.capacity()) - - assertEquals(accessUnit.size, first) - assertEquals(accessUnit.size, second) - assertEquals(accessUnit.toList(), target.bytes(0, accessUnit.size)) - assertEquals("the library should be tried once, not on every access unit", 1, attempts) - } - - @Test - fun handsTheRewriterTheAccessUnitWithoutTheEnhancementLayer() { - val rewriter = FakeRewriter(profile = 7) - val filter = filterWith(rewriter) - val target = directBuffer(annexB(VPS, VCL, EL, RPU, EL)) - - filter.filter(target, 0, target.capacity()) - - assertEquals(annexB(VPS, VCL, RPU).toList(), rewriter.lastFrame) - } - - /** - * Stands in for libdovi. It finds the RPU NAL unit the way the native code does, by walking the - * Annex B start codes, and swaps its payload for [converted], moving whatever follows. - */ - private class FakeRewriter( - private val profile: Int, - private val converted: ByteArray = CONVERTED_RPU, - private val throws: Boolean = false, - private val reportedSize: Int? = null, - ) : RpuRewriter { - var conversions = 0 - var lastFrame: List = emptyList() - - override fun profileOf( - frame: ByteBuffer, - size: Int, - ): Int { - assertFalse("libdovi reads the buffer's address, so the frame has to be direct", frame.hasArray()) - return profile - } - - override fun convertToProfile8( - frame: ByteBuffer, - size: Int, - ): Int { - conversions++ - lastFrame = frame.bytes(0, size) - if (throws) { - throw IllegalArgumentException("Unable to parse Dolby Vision RPU") - } - val bytes = frame.bytes(0, size).toByteArray() - val rpuStart = indexOfNal(bytes, RPU) - val rpuEnd = nextStartCode(bytes, rpuStart + 1) - val rewritten = bytes.copyOfRange(0, rpuStart) + converted + bytes.copyOfRange(rpuEnd, bytes.size) - check(rewritten.size <= frame.capacity()) { "the fake outgrew the scratch buffer" } - rewritten.forEachIndexed { index, byte -> frame.put(index, byte) } - return reportedSize ?: rewritten.size - } - - private fun indexOfNal( - bytes: ByteArray, - nal: ByteArray, - ): Int { - for (i in 0..bytes.size - nal.size) { - if (bytes.copyOfRange(i, i + nal.size).contentEquals(nal)) { - return i - } - } - throw AssertionError("RPU not found in the frame handed to the rewriter") - } - - private fun nextStartCode( - bytes: ByteArray, - from: Int, - ): Int { - for (i in from..bytes.size - 3) { - if (bytes[i] == 0.toByte() && bytes[i + 1] == 0.toByte() && bytes[i + 2] == 1.toByte()) { - return if (i > from && bytes[i - 1] == 0.toByte()) i - 1 else i - } - } - return bytes.size - } - } - - companion object { - private val START_CODE = byteArrayOf(0, 0, 0, 1) - private val SHORT_START_CODE = byteArrayOf(0, 0, 1) - - /** NAL unit header for [type]: forbidden_zero_bit 0, nuh_layer_id 0, nuh_temporal_id_plus1 1. */ - private fun nal( - type: Int, - vararg payload: Byte, - ): ByteArray = byteArrayOf((type shl 1).toByte(), 0x01) + payload - - private val VPS = nal(32, 0x0C, 0x01, 0x7F) - private val VCL = nal(1, 0x11, 0x22, 0x33, 0x44) - private val EL = nal(63, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E) - private val RPU = nal(62, 0x19, 0x07, 0x07, 0x07) - private val CONVERTED_RPU = nal(62, 0x19, 0x08, 0x08) - - private fun annexB(vararg nals: ByteArray): ByteArray = nals.fold(byteArrayOf()) { acc, nal -> acc + START_CODE + nal } - - private fun filterWith(rewriter: RpuRewriter) = DolbyVisionProfile7Filter { rewriter } - - /** Like the decoder input buffer on a device: direct, with no backing array. */ - private fun directBuffer(data: ByteArray): ByteBuffer = - ByteBuffer.allocateDirect(data.size).also { buffer -> - data.forEachIndexed { index, byte -> buffer.put(index, byte) } - assertFalse(buffer.hasArray()) - } - - private fun ByteBuffer.bytes( - from: Int, - count: Int, - ): List = (from until from + count).map { get(it) } - } -} diff --git a/app/src/test/java/com/github/damontecres/wholphin/util/profile/TestDeviceProfileUtils.kt b/app/src/test/java/com/github/damontecres/wholphin/util/profile/TestDeviceProfileUtils.kt deleted file mode 100644 index 491280df..00000000 --- a/app/src/test/java/com/github/damontecres/wholphin/util/profile/TestDeviceProfileUtils.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.github.damontecres.wholphin.util.profile - -import io.mockk.every -import io.mockk.mockk -import org.jellyfin.sdk.model.api.CodecType -import org.jellyfin.sdk.model.api.DeviceProfile -import org.jellyfin.sdk.model.api.ProfileConditionValue -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** - * Verifies that forceEnabledHdr/forceDisabledHdr correctly override the device's own - * codec-capability detection when building the unsupported video range type list. - */ -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, sdk = [30]) -class TestDeviceProfileUtils { - private fun unsupportedRangeTypes( - profile: DeviceProfile, - codec: String, - ): Set = - profile.codecProfiles - .filter { it.type == CodecType.VIDEO && it.codec == codec } - .firstNotNullOf { codecProfile -> - codecProfile.applyConditions.firstOrNull { it.property == ProfileConditionValue.VIDEO_RANGE_TYPE }?.value - }.split("|") - .toSet() - - private fun buildProfile( - mediaTest: MediaCodecCapabilitiesTest, - jellyfinTenEleven: Boolean = false, - decodeAv1: Boolean = false, - forceEnabledHdr: Set = emptySet(), - forceDisabledHdr: Set = emptySet(), - ): DeviceProfile = - createDeviceProfile( - mediaTest = mediaTest, - maxBitrate = 100_000_000, - isAC3Enabled = true, - downMixAudio = false, - assDirectPlay = true, - pgsDirectPlay = true, - decodeAv1 = decodeAv1, - jellyfinTenEleven = jellyfinTenEleven, - preferAc3ForSurround = false, - forceEnabledHdr = forceEnabledHdr, - forceDisabledHdr = forceDisabledHdr, - maxResolution = 0, - ) - - @Test - fun `Test HEVC Profile 7 unsupported when device does not support it`() { - val mediaTest = mockk(relaxed = true) - every { mediaTest.supportsHevcDolbyVisionEL() } returns false - every { mediaTest.supportsHevcDolbyVision() } returns true - every { mediaTest.supportsHevcHDR10Plus() } returns true - - val profile = buildProfile(mediaTest, jellyfinTenEleven = true) - Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.HEVC).contains(WholphinVideoRangeType.DOVI_WITH_EL.serialName)) - } - - @Test - fun `Test force enable overrides HEVC Profile 7 device support check`() { - val mediaTest = mockk(relaxed = true) - every { mediaTest.supportsHevcDolbyVisionEL() } returns false - every { mediaTest.supportsHevcDolbyVision() } returns true - every { mediaTest.supportsHevcHDR10Plus() } returns true - - val profile = - buildProfile( - mediaTest, - jellyfinTenEleven = true, - forceEnabledHdr = - setOf( - WholphinVideoRangeType.DOVI_WITH_EL.serialName, - WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName, - ), - ) - Assert.assertFalse(unsupportedRangeTypes(profile, Codec.Video.HEVC).contains(WholphinVideoRangeType.DOVI_WITH_EL.serialName)) - } - - @Test - fun `Test force disable overrides HEVC Profile 7 device support check`() { - val mediaTest = mockk(relaxed = true) - every { mediaTest.supportsHevcDolbyVisionEL() } returns true - every { mediaTest.supportsHevcDolbyVision() } returns true - every { mediaTest.supportsHevcHDR10Plus() } returns true - - val profile = - buildProfile( - mediaTest, - jellyfinTenEleven = true, - forceDisabledHdr = - setOf( - WholphinVideoRangeType.DOVI_WITH_EL.serialName, - WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName, - ), - ) - Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.HEVC).contains(WholphinVideoRangeType.DOVI_WITH_EL.serialName)) - } - - @Test - fun `Test AV1 Profile 5 unsupported when device does not support it`() { - val mediaTest = mockk(relaxed = true) - every { mediaTest.supportsAV1DolbyVision() } returns false - - val profile = buildProfile(mediaTest, decodeAv1 = false) - Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.AV1).contains(WholphinVideoRangeType.DOVI.serialName)) - } - - @Test - fun `Test force enable overrides AV1 Profile 5 device support check`() { - val mediaTest = mockk(relaxed = true) - every { mediaTest.supportsAV1DolbyVision() } returns false - - val profile = - buildProfile( - mediaTest, - decodeAv1 = false, - forceEnabledHdr = setOf(WholphinVideoRangeType.DOVI.serialName), - ) - Assert.assertFalse(unsupportedRangeTypes(profile, Codec.Video.AV1).contains(WholphinVideoRangeType.DOVI.serialName)) - } - - @Test - fun `Test force disable overrides AV1 Profile 5 device support check`() { - val mediaTest = mockk(relaxed = true) - every { mediaTest.supportsAV1DolbyVision() } returns true - - val profile = - buildProfile( - mediaTest, - decodeAv1 = false, - forceDisabledHdr = setOf(WholphinVideoRangeType.DOVI.serialName), - ) - Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.AV1).contains(WholphinVideoRangeType.DOVI.serialName)) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/DiscoverLoadingFocusTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/DiscoverLoadingFocusTest.kt deleted file mode 100644 index b18f6a22..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/DiscoverLoadingFocusTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import kotlinx.coroutines.CompletableDeferred -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.DiscoverFixture -import org.justinvisser.wolfskin.design.DiscoverScreen -import org.justinvisser.wolfskin.design.DiscoverState -import org.justinvisser.wolfskin.design.DiscoverStatus -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.ShellSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -/** The real asynchronous host must return focus from its loading nav to the first loaded action. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class DiscoverLoadingFocusTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test fun firstOkAfterLoadingOpensTheHeroInsteadOfSelectingTheNav() { - val pending = CompletableDeferred() - val shell = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply { - pickerVisible = false - enterDestination("discover") - } - lateinit var current: DiscoverState - rule.setContent { - val state = rememberDiscoverState(shell, shell::enterDestination, { shell.enterDestination("discover") }, loadFixture = { pending.await() }) - current = state - WolfskinTheme { ShellSurface(shell) { DiscoverScreen(state, it) } } - } - rule.runOnIdle { - assertEquals(DiscoverStatus.Loading, current.snapshot.status) - assertTrue(shell.navFocused) - val json = rule.activity.assets.open("wolfskin-discover/fixture.json").bufferedReader().use { it.readText() } - pending.complete(DiscoverFixture(json) { "file:///android_asset/wolfskin-discover/$it" }) - } - rule.waitForIdle() - rule.runOnIdle { - assertEquals(DiscoverStatus.Ready, current.snapshot.status) - assertFalse(shell.navFocused) - assertEquals(0, current.focusedRow) - } - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionCenter) } - rule.runOnIdle { - assertNotNull(current.title) - assertEquals("The Amazing Spider-Man", current.title!!.title) - assertEquals("discover-title/${current.title!!.key}", shell.route) - } - } - - @Test fun finishingLoadPreservesANavTargetChosenWhileLoading() { - val pending = CompletableDeferred() - val shell = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply { - pickerVisible = false - enterDestination("discover") - } - lateinit var current: DiscoverState - rule.setContent { - val state = rememberDiscoverState(shell, shell::enterDestination, { shell.enterDestination("discover") }, loadFixture = { pending.await() }) - current = state - WolfskinTheme { ShellSurface(shell) { DiscoverScreen(state, it) } } - } - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionRight) } - rule.runOnIdle { - assertEquals(DiscoverStatus.Loading, current.snapshot.status) - assertEquals(2, shell.navIndex) - val json = rule.activity.assets.open("wolfskin-discover/fixture.json").bufferedReader().use { it.readText() } - pending.complete(DiscoverFixture(json) { "file:///android_asset/wolfskin-discover/$it" }) - } - rule.waitForIdle() - rule.runOnIdle { - assertEquals(DiscoverStatus.Ready, current.snapshot.status) - assertTrue(shell.navFocused) - assertEquals(2, shell.navIndex) - } - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionCenter) } - rule.runOnIdle { - assertEquals("movies", shell.route) - assertEquals(null, current.title) - } - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/HomeLibraryCacheTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/HomeLibraryCacheTest.kt deleted file mode 100644 index 49fc269f..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/HomeLibraryCacheTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.justinvisser.wolfskin.design.HomeLibrary -import org.justinvisser.wolfskin.design.HomeTitle -import org.justinvisser.wolfskin.tv.read.HomeLibraryCache - -/** A reopened Home cache preserves playable identity and episode/resume state without network reads. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class) -class HomeLibraryCacheTest { - @Test fun savedHomeReopensWithEpisodeMetadataAndFullShelfCount() { - val context = ApplicationProvider.getApplicationContext() - val episode = HomeTitle("episode-id", "The next episode", "episode", year = 2025, runtimeSeconds = 1800, overview = "Overview", poster = "poster", backdrop = "backdrop", seriesTitle = "A series", season = 2, episode = 3, resumeSeconds = 125.5, addedAt = "2026-11-05", genres = listOf("Drama"), held = true, tmdbId = 42) - val film = HomeTitle("film-id", "A film", "movie", year = 2024, runtimeSeconds = 5400, played = true, held = true, tmdbId = 43) - val library = HomeLibrary(listOf(episode), listOf(episode.copy(id = "next", resumeSeconds = 0.0)), listOf(film), mapOf("movies" to listOf(film)), mapOf("movies" to 84)) - HomeLibraryCache(context).write("server:person", library, 12345L) - val saved = HomeLibraryCache(context).read("server:person")!! - assertEquals(library, saved.first) - assertEquals(12345L, saved.second) - assertNull(HomeLibraryCache(context).read("server:someone-else")) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/HomeRecoveryTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/HomeRecoveryTest.kt deleted file mode 100644 index 864c1743..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/HomeRecoveryTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.test.core.app.ApplicationProvider -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.services.NavigationManager -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import java.io.IOException -import java.util.UUID -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.* -import org.justinvisser.wolfskin.tv.read.HomeLibraryCache -import org.justinvisser.wolfskin.tv.read.HomeReadFacade -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** Saved Home projections survive outages but never survive an authenticated identity change. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class) -class HomeRecoveryTest { - private val context = ApplicationProvider.getApplicationContext() - private val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null) - private val personA = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null) - private val personB = personA.copy(id = UUID.fromString("00000000-0000-0000-0000-000000000003"), name = "Mindel") - private val film = HomeTitle("private-film", "Person A film", "movie", held = true, runtimeSeconds = 5400) - private val library = HomeLibrary(resume = listOf(film)) - - @Test fun savedHomePaintsBeforeRefreshAndStaysAfterFailedRetry() = runBlocking { - val cache = HomeLibraryCache(context) - cache.write("${server.id}:${personA.id}", library, 1000L) - val started = CompletableDeferred() - val response = CompletableDeferred() - val facade = mockk() - coEvery { facade.read(personA.id) } coAnswers { started.complete(Unit); response.await() } - val model = WolfskinHomeViewModel(context, facade, cache, NavigationManager(), WolfskinSeeAllLists()) - val refresh = async(Dispatchers.IO) { model.loadLibrary(server, personA, "justin", retry = true) } - started.await() - assertEquals(film.id, model.snapshot.rows.first().cards.first().id) - response.completeExceptionally(IOException("fixture offline")) - refresh.await() - assertFalse(model.snapshot.jellyfinAvailable) - assertEquals(film.id, model.snapshot.rows.first().cards.first().id) - assertNotNull(model.snapshot.savedAt) - assertEquals(RecoveryMessage.StillUnreachable, model.toast) - } - - @Test fun personBSeesNeitherPersonAsSavedHomeNorTheirLateRefreshResult() = runBlocking { - val facade = mockk() - val cache = HomeLibraryCache(context) - cache.write("${server.id}:${personA.id}", library, 1000L) - val startedA = CompletableDeferred() - val responseA = CompletableDeferred() - coEvery { facade.read(personA.id) } coAnswers { startedA.complete(Unit); responseA.await() } - coEvery { facade.read(personB.id) } throws IOException("fixture offline") - val model = WolfskinHomeViewModel(context, facade, cache, NavigationManager(), WolfskinSeeAllLists()) - val requestA = async(Dispatchers.IO) { model.loadLibrary(server, personA, "justin") } - startedA.await() - assertTrue(model.snapshot.rows.any { row -> row.cards.any { it.id == film.id } }) - model.loadLibrary(server, personB, "mindel") - assertFalse(model.snapshot.rows.any { row -> row.cards.any { it.id == film.id } }) - assertNull(model.snapshot.savedAt) - assertTrue(model.snapshot.rows.any { it.kind == HomeRowKind.State && it.focusable }) - responseA.complete(library) - requestA.await() - assertFalse(model.snapshot.rows.any { row -> row.cards.any { it.id == film.id } }) - assertFalse(model.snapshot.jellyfinAvailable) - } - - @Test fun readableJellyfinDoesNotBecomeAnOutageWhenLocalCacheCannotBeWritten() = runBlocking { - val facade = mockk() - coEvery { facade.read(personA.id) } returns library - val cache = mockk() - every { cache.read(any()) } returns null - every { cache.write(any(), any(), any()) } throws IOException("fixture disk full") - val model = WolfskinHomeViewModel(context, facade, cache, NavigationManager(), WolfskinSeeAllLists()) - model.loadLibrary(server, personA, "justin") - assertTrue(model.snapshot.jellyfinAvailable) - assertEquals(film.id, model.snapshot.rows.first().cards.first().id) - assertNull(model.toast) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/HomeSeeAllNavigationTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/HomeSeeAllNavigationTest.kt deleted file mode 100644 index 47f843b2..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/HomeSeeAllNavigationTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import io.mockk.mockk -import java.util.UUID -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assert.assertSame -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.* -import org.justinvisser.wolfskin.tv.read.LibraryReadFacade -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** Home opens the complete shared grid and Back restores its original continuation card. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -class HomeSeeAllNavigationTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test fun personalMovieContinuationReachesTheEighteenthTitleAndReturnsToHome() = continuation("pk-m", 5) { grid -> - assertEquals(18, grid.items.size) - repeat(2) { press(Key.DirectionDown) } - repeat(5) { press(Key.DirectionRight) } - rule.onNodeWithText("Bleach the Movie: Fade to Black").assertIsDisplayed() - assertEquals("movie-bleach-the-movie-fade-to-black-2008", grid.focusedTitle?.id) - } - - @Test fun horizontallyScrolledContinuationRestoresTheExactHomeViewport() = continuation("trending", 20) { grid -> - press(Key.DirectionDown) - assertEquals(6, grid.index) - rule.onNodeWithTag("library-grid").assertIsDisplayed() - } - - private fun continuation(rowId: String, expectedColumn: Int, exerciseGrid: (LibraryGridState) -> Unit) { - rule.mainClock.autoAdvance = false - val snapshot = HomeFixture(rule.activity).snapshot("justin") - val row = snapshot.rows.first { it.id == rowId } - val cardTag = "home-card:$rowId:$expectedColumn" - val navigation = NavigationManager() - val lists = WolfskinSeeAllLists() - val shell = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin") { - if (it == ShellAction.Pop) navigation.goBack() - }.apply { - pickerVisible = false - view.row = snapshot.rows.indexOf(row) - view.home.focusedKey = row.id - view.cards[row.id] = row.cards.size - } - val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null) - val user = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null) - val model = WolfskinHomeViewModel(rule.activity, mockk(), mockk(), navigation, lists) - val facade = mockk() - rule.setContent { - val destination = navigation.backStack.last() - val route = (destination as? Destination.Wolfskin)?.route ?: "home" - LaunchedEffect(destination) { shell.enterDestination(route) } - WolfskinTheme { ShellSurface(shell) { modifier -> - if (destination is Destination.Home) HomeScreen(shell, snapshot, modifier) { model.act(it, shell, server, user) } - else WolfskinLibraryContent(rule.activity, route, shell, user, "fixture", facade, navigation, lists, modifier) - } } - } - settle() - rule.onNodeWithTag(cardTag).assertIsDisplayed() - val home = shell.view - val track = home.home.tracks.getValue(rowId) - val verticalIndex = home.listState.firstVisibleItemIndex - val verticalOffset = home.listState.firstVisibleItemScrollOffset - val rowOffset = home.listState.layoutInfo.visibleItemsInfo.first { it.key == rowId }.offset - val horizontalIndex = track.firstVisibleItemIndex - val horizontalOffset = track.firstVisibleItemScrollOffset - press(Key.DirectionCenter) - assertEquals("seeall/justin/$rowId", (navigation.backStack.last() as Destination.Wolfskin).route) - val grid = requireNotNull(lists.get("justin/$rowId")) - try { - rule.onNodeWithTag("library-grid").assertIsDisplayed() - exerciseGrid(grid) - press(Key.Back) - rule.waitForIdle() - assertTrue(navigation.backStack.last() is Destination.Home) - assertEquals(row.id, snapshot.rows[shell.view.row].id) - assertEquals(expectedColumn, shell.view.cards[row.id]) - assertSame("Home retains its original route memory", home, shell.view) - assertSame("Home retains its original horizontal state", track, home.home.tracks.getValue(rowId)) - assertEquals("Vertical first item", verticalIndex, home.listState.firstVisibleItemIndex) - assertEquals("Vertical pixel offset", verticalOffset, home.listState.firstVisibleItemScrollOffset) - assertEquals("Selected row pixel offset", rowOffset, home.listState.layoutInfo.visibleItemsInfo.first { it.key == rowId }.offset) - assertEquals("Horizontal first item", horizontalIndex, track.firstVisibleItemIndex) - assertEquals("Horizontal pixel offset", horizontalOffset, track.firstVisibleItemScrollOffset) - rule.onNodeWithTag(cardTag).assertIsDisplayed() - } catch (failure: AssertionError) { - throw AssertionError( - "Expected $rowId:$expectedColumn vertical=$verticalIndex/$verticalOffset row-offset=$rowOffset horizontal=$horizontalIndex/$horizontalOffset; " + - "actual route=${shell.route} row=${home.row} card=${home.cards[rowId]} nav=${shell.navFocused} handler=${shell.contentFocus?.javaClass?.simpleName} grid=${grid.index} " + - "vertical=${home.listState.firstVisibleItemIndex}/${home.listState.firstVisibleItemScrollOffset} horizontal=${track.firstVisibleItemIndex}/${track.firstVisibleItemScrollOffset} " + - "visible-rows=${home.listState.layoutInfo.visibleItemsInfo.map { it.key to it.offset }}", - failure, - ) - } - } - - private fun press(key: Key) { - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(key) } - settle() - } - - private fun settle() { - repeat(3) { - rule.mainClock.advanceTimeByFrame() - rule.waitForIdle() - } - rule.mainClock.advanceTimeBy(1_000) - rule.waitForIdle() - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/LibrarySnapshotStoreTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/LibrarySnapshotStoreTest.kt deleted file mode 100644 index 5363848c..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/LibrarySnapshotStoreTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.justinvisser.wolfskin.conductarr.MemoryCacheStore -import org.justinvisser.wolfskin.design.LibraryTitle -import org.justinvisser.wolfskin.tv.read.LibrarySnapshotStore - -/** A stale library remains readable offline without crossing server/person boundaries. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class) -class LibrarySnapshotStoreTest { - @Test fun savedCopyArrivesBeforeFailedRefreshAndFreshCopySkipsNetwork() = runBlocking { - var now = 1000L - val store = LibrarySnapshotStore(MemoryCacheStore()) { now } - val items = listOf(LibraryTitle("a", "Film A", "movie", year = 2007, runtimeSeconds = 6000, played = true)) - store.read("server:justin", "movies") { items }.toList() - val fresh = store.read("server:justin", "movies") { error("Fresh snapshot must not read") }.toList() - assertEquals(items, fresh.single().items) - now += 11 * 60_000 - val offline = store.read("server:justin", "movies") { throw java.io.IOException("offline") }.toList() - assertEquals(2, offline.size) - assertFalse(offline.first().unreachable) - assertTrue(offline.last().unreachable) - assertEquals(1000L, offline.last().savedAtMillis) - assertEquals(items, offline.last().items) - for (scope in listOf("server:mindel", "other-server:justin")) { - val isolated = store.read(scope, "movies") { throw java.io.IOException("offline") }.toList().single() - assertNull(isolated.savedAtMillis) - assertTrue(isolated.items.isEmpty()) - } - } - - @Test fun explicitRecoveryRefreshesEvenFreshEmptyDestination() = runBlocking { - val store = LibrarySnapshotStore(MemoryCacheStore()) { 1000L } - store.read("server:justin", "movies") { emptyList() }.toList() - val current = listOf(LibraryTitle("b", "New Film", "movie")) - val reads = store.read("server:justin", "movies", force = true) { current }.toList() - assertTrue(reads.first().items.isEmpty()) - assertEquals(current, reads.last().items) - assertFalse(reads.last().unreachable) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/PlayerHandoffTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/PlayerHandoffTest.kt deleted file mode 100644 index 69fbe6ba..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/PlayerHandoffTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.os.Looper -import android.view.View -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.takahirom.roborazzi.captureRoboImage -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import java.io.File -import java.io.IOException -import java.util.UUID -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.Shadows.shadowOf -import org.justinvisser.wolfskin.design.* -import org.justinvisser.wolfskin.tv.read.HomeReadFacade -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -/** Actual Home actions either open upstream playback or leave focus and the saved Home intact. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class PlayerHandoffTest { - @get:Rule val rule = createAndroidComposeRule() - private val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null) - private val user = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Mindel", serverId = server.id, accessToken = null) - private val navigation = NavigationManager() - private val facade = mockk() - private lateinit var model: WolfskinHomeViewModel - private lateinit var shell: ShellState - - @Before fun setup() { - rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) - @Suppress("DEPRECATION") - rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - shell = ShellState(listOf(ShellPerson("mindel", "Mindel", "M", false)), "mindel").apply { pickerVisible = false } - model = WolfskinHomeViewModel(rule.activity, facade, mockk(), navigation, WolfskinSeeAllLists()) - } - - @Test fun failedPlaybackReadLeavesSavedHomeFocusedAndDrawsTheExactOfflineToast() { - val snapshot = HomeFixture(rule.activity).snapshot("mindel", "offline") - val title = snapshot.rows.first().cards.first() - coEvery { facade.playableId(user.id, title) } throws IOException("fixture offline") - rule.setContent { WolfskinTheme { - ShellSurface(shell, Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight)) { modifier -> - HomeScreen(shell, snapshot, modifier) { model.act(it, shell, server, user) } - model.toast?.let { RecoveryToast(it) } - } - } } - rule.mainClock.advanceTimeBy(1000) - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionCenter) } - awaitModel { model.toast != null } - rule.onNodeWithText(RecoveryMessage.PlaybackUnavailable).assertIsDisplayed() - assertTrue(navigation.backStack.single() is Destination.Home) - assertFalse(shell.navFocused) - assertEquals("hero", snapshot.rows[shell.view.row].id) - assertEquals(0, shell.view.cards["hero"] ?: 0) - val directory = File(System.getProperty("wolfskin.captureDirectory"), "recovery").apply { mkdirs() } - rule.onRoot().captureRoboImage(File(directory, "st-090-integrated.png").absolutePath) - } - - @Test fun successfulPlaybackUsesTheRealFullscreenDestinationAndBackKeepsTheResumeCard() { - val title = HomeFixture(rule.activity).snapshot("mindel").rows.first().cards.first() - val id = UUID.fromString("00000000-0000-0000-0000-000000000003") - coEvery { facade.playableId(user.id, title) } returns id - val view = shell.view - view.cards["hero"] = 0 - rule.runOnIdle { model.act(HomeAction.Play(title, resume = true), shell, server, user) } - awaitModel { navigation.backStack.last() is Destination.Playback } - val destination = navigation.backStack.last() as Destination.Playback - assertEquals(id, destination.itemId) - assertEquals((title.resumeSeconds * 1000).toLong(), destination.positionMs) - assertTrue(destination.fullScreen) - navigation.goBack() - assertTrue(navigation.backStack.single() is Destination.Home) - assertSame(view, shell.view) - assertEquals(0, shell.view.cards["hero"]) - assertNull(model.toast) - } - - @Test fun anotherPersonsSessionCannotResolveOrStartPlayback() { - val title = HomeFixture(rule.activity).snapshot("mindel").rows.first().cards.first() - val otherUser = user.copy(name = "Justin") - rule.runOnIdle { model.act(HomeAction.Play(title, resume = true), shell, server, otherUser) } - awaitModel { model.toast != null } - assertEquals(RecoveryMessage.PlaybackUnavailable, model.toast) - assertTrue(navigation.backStack.single() is Destination.Home) - coVerify(exactly = 0) { facade.playableId(any(), any()) } - } - private fun awaitModel(condition: () -> Boolean) { - rule.waitUntil(5000) { - shadowOf(Looper.getMainLooper()).idle() - condition() - } - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/RequestsRefreshTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/RequestsRefreshTest.kt deleted file mode 100644 index a90bb4b1..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/RequestsRefreshTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Test -import org.justinvisser.wolfskin.design.ComingInRequest -import org.justinvisser.wolfskin.design.ComingInSnapshot -import org.justinvisser.wolfskin.design.ComingInStage -import org.justinvisser.wolfskin.design.ComingInStatus -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellState - -/** The fake clock checks refresh boundaries and cancellation without a server or a real wait. */ -@OptIn(ExperimentalCoroutinesApi::class) -class RequestsRefreshTest { - @Test fun foregroundOpenUnsettledSettledAndStoppedTransitions() = runTest { - val state = ShellState(listOf(ShellPerson("justin", "Justin", "J", false)), "justin") - val request = ComingInRequest("one", "movie", 1, "One", "justin", "Justin", "shared", "Shared", ComingInStage.Waiting) - state.requests.update(ComingInSnapshot(status = ComingInStatus.Ready, signedIn = true)) - var reads = 0 - val active = launch { refreshRequestsWhileStarted(state) { reads++ } } - runCurrent() - assertEquals(1, reads) - advanceTimeBy(30_000); runCurrent() - assertEquals("Closed and settled does not poll", 1, reads) - state.openRequests() - advanceTimeBy(29_999); runCurrent() - assertEquals(1, reads) - advanceTimeBy(1); runCurrent() - assertEquals("An open empty sheet still refreshes at 30 seconds", 2, reads) - state.closeRequests() - state.requests.update(ComingInSnapshot(listOf(request), ComingInStatus.Ready, signedIn = true)) - advanceTimeBy(30_000); runCurrent() - assertEquals("An unsettled request refreshes after the sheet closes", 3, reads) - state.requests.update(ComingInSnapshot(listOf(request.copy(stage = ComingInStage.Ready)), ComingInStatus.Ready, signedIn = true)) - advanceTimeBy(30_000); runCurrent() - assertEquals("Ready requests stop polling", 3, reads) - active.cancel(); runCurrent() - state.openRequests() - advanceTimeBy(60_000); runCurrent() - assertEquals("A stopped lifecycle cannot poll", 3, reads) - val resumed = launch { refreshRequestsWhileStarted(state) { reads++ } } - runCurrent() - assertEquals("Returning to the foreground refreshes immediately", 4, reads) - resumed.cancel() - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/SettingsHandOffCaptureTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/SettingsHandOffCaptureTest.kt deleted file mode 100644 index dd5ee44c..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/SettingsHandOffCaptureTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.view.View -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.services.ImageUrlService -import com.github.damontecres.wholphin.services.SeerrRatingsService -import com.github.damontecres.wholphin.ui.LocalImageUrlService -import com.github.damontecres.wholphin.ui.components.LocalSeerrRatingsService -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.ui.main.settings.HomePageSettingsState -import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsPage -import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsViewModel -import com.github.damontecres.wholphin.util.LoadingState -import com.github.takahirom.roborazzi.captureRoboImage -import io.mockk.every -import io.mockk.mockk -import java.io.File -import kotlinx.coroutines.flow.MutableStateFlow -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.justinvisser.wolfskin.design.WolfskinTokens -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -/** The frozen st-094 state opens User profile settings, not the separate Playback press path. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class SettingsHandOffCaptureTest { - @get:Rule val rule = createAndroidComposeRule() - - @Before fun fullscreen() { - rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) - @Suppress("DEPRECATION") - rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - } - - @Test fun st094_realUserProfileHomeSettingsHandOff() { - val model = mockk(relaxed = true) - every { model.state } returns MutableStateFlow(HomePageSettingsState.EMPTY.copy(loading = LoadingState.Success)) - every { model.serverPluginActive } returns MutableStateFlow(false) - every { model.settingsChanged } returns MutableStateFlow(false) - val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null) - val ratings = SeerrRatingsService(mockk(relaxed = true), mockk(relaxed = true)) - val images = mockk(relaxed = true) - rule.setContent { - WolfskinTheme { - CompositionLocalProvider(LocalSeerrRatingsService provides ratings, LocalImageUrlService provides images) { - HomeSettingsPage(preferences, Modifier.fillMaxSize().background(WolfskinTokens.Color.bg), viewModel = model) - } - } - } - rule.mainClock.advanceTimeBy(1000) - rule.waitForIdle() - rule.onNodeWithText(rule.activity.getString(R.string.customize_home)).assertIsDisplayed() - val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))) - directory.mkdirs() - rule.onRoot().captureRoboImage(File(directory, "st-094.png").absolutePath) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/ShellRoutingTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/ShellRoutingTest.kt deleted file mode 100644 index a5b181f7..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/ShellRoutingTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.test.core.app.ApplicationProvider -import com.github.damontecres.wholphin.preferences.AppPreference -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption -import java.util.UUID -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import org.junit.After -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** The actual Advanced preference persists its value and changes the shell's renderer policy. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class) -class ShellRoutingTest { - private val context: Application get() = ApplicationProvider.getApplicationContext() - private val movies = Destination.MediaItem(UUID.fromString("00000000-0000-0000-0000-000000000001"), BaseItemKind.COLLECTION_FOLDER, CollectionType.MOVIES) - private val libraries = mapOf("movies" to movies, "shows" to movies, "anime" to movies) - - @Before fun initialize() { WolfskinScreenPreference.initialize(context); WolfskinScreenPreference.enabled = true } - @After fun restore() { WolfskinScreenPreference.enabled = true } - - @Test fun advancedSwitchRestoresUpstreamHomeAndLibrariesAndPersistsAcrossReload() { - val preference = AppPreference.WolfskinScreens - val preferences = AppPreferences.getDefaultInstance() - assertNull(resolveUpstreamDestination(Destination.Home(), true, libraries)) - assertNull(resolveUpstreamDestination(Destination.Wolfskin("movies"), true, libraries)) - preference.setter(preferences, false) - WolfskinScreenPreference.initialize(context) - assertFalse(preference.getter(preferences)) - assertTrue(resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, libraries) is Destination.Home) - listOf("movies", "shows", "anime").forEach { route -> - assertEquals(movies, resolveUpstreamDestination(Destination.Wolfskin(route), WolfskinScreenPreference.enabled, libraries)) - } - preference.setter(preferences, true) - assertNull(resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, libraries)) - assertNull(resolveUpstreamDestination(Destination.Wolfskin("movies"), WolfskinScreenPreference.enabled, libraries)) - } - - @Test fun settingsAndPickerNeverBecomeUpstreamScreensButUnrelatedRoutesPassThrough() { - val settings = Destination.Wolfskin("settings") - val upstream = Destination.Settings(PreferenceScreenOption.ADVANCED) - for (enabled in listOf(false, true)) { - assertNull(resolveUpstreamDestination(settings, enabled, libraries)) - assertEquals(upstream, resolveUpstreamDestination(upstream, enabled, libraries)) - assertEquals(Destination.Search("name"), resolveUpstreamDestination(Destination.Search("name"), enabled, libraries)) - } - } - - @Test fun titleRolloutOwnsOnlyFilmSeriesEpisodeAndPerson() { - val id = UUID.fromString("00000000-0000-0000-0000-000000000001") - for (kind in listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE, BaseItemKind.PERSON)) { - val destination = Destination.MediaItem(id, kind) - assertNull(resolveUpstreamDestination(destination, true, libraries)) - assertEquals(destination, resolveUpstreamDestination(destination, false, libraries)) - } - for (kind in listOf(BaseItemKind.COLLECTION_FOLDER, BaseItemKind.AUDIO, BaseItemKind.MUSIC_ALBUM, BaseItemKind.BOX_SET)) { - val destination = Destination.MediaItem(id, kind) - assertEquals(destination, resolveUpstreamDestination(destination, true, libraries)) - } - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleAsyncFocusTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleAsyncFocusTest.kt deleted file mode 100644 index 8316538b..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleAsyncFocusTest.kt +++ /dev/null @@ -1,165 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.NavigationManager -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.cancel -import org.junit.After -import org.junit.Assert.* -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.RecoveryMessage -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.TitleItem -import org.justinvisser.wolfskin.design.TitlePageData -import org.justinvisser.wolfskin.design.TitleReadStatus -import org.justinvisser.wolfskin.design.TitleSheet -import org.justinvisser.wolfskin.design.TitleTrack -import org.justinvisser.wolfskin.design.TitleVersion -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.justinvisser.wolfskin.tv.read.TitleReadFacade -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.util.UUID - -/** A delayed service reply exercises the production destination, view-model and Android focus transfer. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -class TitleAsyncFocusTest { - @get:Rule val rule = createAndroidComposeRule() - private lateinit var viewModel: WolfskinTitleViewModel - - @After fun dispose() { - if (::viewModel.isInitialized) viewModel.viewModelScope.cancel() - } - - @Test fun loadingToReadyRestoresRealKeyFocusAndSheetReturn() { - val id = UUID.fromString("00000000-0000-0000-0000-000000000012") - val ready = CompletableDeferred() - val facade = mockk() - coEvery { facade.readSaved(id, any()) } returns null - coEvery { facade.read(id, any(), any()) } coAnswers { ready.await() } - viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), mockk(relaxed = true)) - val person = ShellPerson("justin", "Justin", "J", false) - val route = titleRoute(id.toString()) - val shell = ShellState(listOf(person), person.key).apply { - pickerVisible = false - pickerLaunch = false - enterDestination(route) - } - val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null) - rule.setContent { WolfskinTheme { TitleDestination(route, shell, preferences, viewModel = viewModel) } } - rule.waitUntil { viewModel.title?.data?.status == TitleReadStatus.Loading } - rule.waitForIdle() - assertTrue(shell.navFocused) - press(Key.DirectionDown) - assertTrue("The initial skeleton cannot receive nav Down", shell.navFocused) - - val source = TitleVersion("first", "Original", "1080p", "", "", "MKV", listOf(TitleTrack(3, "English", true)), emptyList()) - rule.runOnIdle { - ready.complete(TitlePageData(TitleItem(id.toString(), "A held film", "movie", held = true), versions = listOf(source, source.copy(id = "second", name = "Alternate")))) - } - rule.waitUntil { viewModel.title?.data?.status == TitleReadStatus.Ready && !shell.navFocused } - press(Key.DirectionRight) - val state = requireNotNull(viewModel.title) - assertEquals("version", state.options[state.column].id) - press(Key.DirectionCenter) - assertEquals(TitleSheet.Version, state.sheet) - rule.runOnIdle { rule.activity.onBackPressedDispatcher.onBackPressed() } - assertNull(state.sheet) - press(Key.DirectionRight) - assertEquals("tracks", state.options[state.column].id) - assertFalse(shell.navFocused) - } - - @Test fun loadingPersonFocusesItsFirstCreditBeforeRealRemoteNavigation() { - val id = UUID.fromString("00000000-0000-0000-0000-000000000013") - val reply = CompletableDeferred() - val facade = mockk() - coEvery { facade.readPersonSaved(id) } returns null - coEvery { facade.readPerson(id, 0) } coAnswers { reply.await() } - viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), mockk(relaxed = true)) - val viewer = ShellPerson("justin", "Justin", "J", false) - val route = personRoute(id.toString()) - val shell = ShellState(listOf(viewer), viewer.key).apply { - pickerVisible = false - pickerLaunch = false - enterDestination(route) - } - rule.setContent { WolfskinTheme { - TitleDestination(route, shell, UserPreferences(AppPreferences.getDefaultInstance(), null), viewModel = viewModel) - } } - rule.waitUntil { viewModel.title?.data?.status == TitleReadStatus.Loading } - rule.waitForIdle() - assertTrue(shell.navFocused) - press(Key.DirectionDown) - assertTrue(shell.navFocused) - rule.runOnIdle { - reply.complete(org.justinvisser.wolfskin.tv.read.PersonRead( - org.justinvisser.wolfskin.design.TitlePerson(id.toString(), "Actor"), - listOf(TitleItem("first-credit", "First credit", "movie"), TitleItem("second-credit", "Second credit", "movie")), - )) - } - rule.waitUntil(timeoutMillis = 5000) { viewModel.person != null || viewModel.title?.data?.status == TitleReadStatus.Unavailable } - assertNotNull("A real person reply must populate the performer destination", viewModel.person) - rule.waitForIdle() - assertFalse("The loaded person must transfer nav ownership to its first credit", shell.navFocused) - assertEquals(0, viewModel.person!!.index) - press(Key.DirectionRight) - assertEquals(1, viewModel.person!!.index) - assertFalse(shell.navFocused) - } - - @Test fun failedRetryKeepsThePersonsSavedTitleAndShowsSharedRecovery() { - val id = UUID.fromString("00000000-0000-0000-0000-000000000014") - val reads = java.util.concurrent.atomic.AtomicInteger() - val saved = TitlePageData(TitleItem(id.toString(), "Saved title", "movie"), - connection = TitleConnection.Offline, savedAt = "20:42") - val facade = mockk() - coEvery { facade.readSaved(id, any()) } returns saved - coEvery { facade.read(id, any(), any()) } coAnswers { - reads.incrementAndGet() - throw java.io.IOException("offline") - } - viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), mockk(relaxed = true)) - val viewer = ShellPerson("justin", "Justin", "J", false) - val route = titleRoute(id.toString()) - val shell = ShellState(listOf(viewer), viewer.key).apply { - pickerVisible = false; pickerLaunch = false; enterDestination(route) - } - rule.setContent { WolfskinTheme { - TitleDestination(route, shell, UserPreferences(AppPreferences.getDefaultInstance(), null), viewModel = viewModel) - } } - rule.waitUntil(timeoutMillis = 5000) { reads.get() == 1 && viewModel.title?.toast != null } - rule.waitForIdle() - assertEquals("20:42", shell.navSavedAt) - assertEquals("retry", viewModel.title!!.options[viewModel.title!!.column].id) - rule.runOnIdle { viewModel.title!!.toast = null } - press(Key.DirectionCenter) - rule.waitUntil(timeoutMillis = 5000) { reads.get() == 2 && viewModel.title?.toast != null } - rule.waitForIdle() - assertEquals("Saved title", viewModel.title!!.data.item.name) - assertEquals("20:42", shell.navSavedAt) - assertEquals(RecoveryMessage.StillUnreachable, viewModel.title!!.toast) - assertFalse(shell.navFocused) - rule.onNodeWithTag("recovery-toast").assertIsDisplayed() - } - - private fun press(key: Key) { rule.onRoot().performKeyInput { pressKey(key) } } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt deleted file mode 100644 index 90d6fcc1..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt +++ /dev/null @@ -1,218 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.view.View -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performSemanticsAction -import androidx.compose.ui.semantics.SemanticsActions -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.graphics.toArgb -import com.github.takahirom.roborazzi.captureRoboImage -import org.json.JSONObject -import org.junit.Before -import org.junit.After -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.LocalTitleBitmaps -import org.justinvisser.wolfskin.design.PersonState -import org.justinvisser.wolfskin.design.PersonSurface -import org.justinvisser.wolfskin.design.ShellKey -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitlePageData -import org.justinvisser.wolfskin.design.TitleReadStatus -import org.justinvisser.wolfskin.design.TitleState -import org.justinvisser.wolfskin.design.TitleSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.justinvisser.wolfskin.design.WolfskinTokens -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode -import java.io.File -import java.util.Locale - -/** Frozen local art feeds the shipped Compose surface; no test can call a live server. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class TitleCaptureTest { - @get:Rule val rule = createAndroidComposeRule() - private lateinit var fixture: TitleFixture - private var previousLocale = Locale.getDefault() - - @Before fun fullscreen() { - previousLocale = Locale.getDefault() - Locale.setDefault(Locale.US) - rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) - @Suppress("DEPRECATION") - rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - fixture = TitleFixture(rule.activity.assets.open("wolfskin-titles.json").bufferedReader().use { JSONObject(it.readText()) }) - } - @After fun restoreLocale() { Locale.setDefault(previousLocale) } - - private fun capture(number: Int, title: String = "Locke", person: String = "justin", transform: (TitlePageData) -> TitlePageData = { it }, configure: (TitleState, ShellState) -> Unit = { _, _ -> }) { - val data = transform(fixture.title(title, person)) - val shell = ShellState(fixture.people, person).apply { pickerVisible = false; pickerLaunch = false; enterDestination("title/${data.item.id}") } - val state = TitleState(data, shell.person) {} - configure(state, shell) - val bitmaps = decodeArt(data) - rule.setContent { - WolfskinTheme { CompositionLocalProvider(LocalTitleBitmaps provides bitmaps) { - TitleSurface(state, shell, Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight)) - } } - } - if (number == 55) rule.runOnIdle { - shell.view.listState.requestScrollToItem(1, -(WolfskinTokens.Layout.pinnedLine.value * 2).toInt()) - } - save(number) - } - - private fun decodeArt(data: TitlePageData): Map { - val requests = buildList> { - data.item.backdrop?.let { add(Triple(it, 1280, 720)) } - if (data.item.backdrop == null || data.item.isPosterBackdrop) data.item.poster?.let { add(Triple(it, 420, 630)) } - data.cast.forEach { person -> person.portrait?.let { add(Triple(it, 200, 200)) } } - data.related.forEach { item -> item.poster?.let { add(Triple(it, 256, 384)) } } - data.episodes.forEach { item -> - (if (item.isPosterBackdrop) item.poster else item.backdrop)?.let { add(Triple(it, 416, 234)) } - } - } - return requests.distinctBy { it.first }.associate { (path, width, height) -> - val bitmap = rule.activity.assets.open(path.removePrefix("file:///android_asset/")).use { BitmapFactory.decodeStream(it)!! } - val scale = maxOf(width.toFloat() / bitmap.width, height.toFloat() / bitmap.height).coerceAtMost(1f) - val scaled = Bitmap.createScaledBitmap(bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), true) - if (scaled !== bitmap) bitmap.recycle() - path to scaled.asImageBitmap() - } - } - - private fun save(number: Int) { - rule.mainClock.advanceTimeBy(1000) - rule.waitForIdle() - val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))).apply { mkdirs() } - rule.onRoot().captureRoboImage(File(directory, "st-${number.toString().padStart(3, '0')}.png").absolutePath) - val heading = rule.onAllNodesWithTag("title-heading").fetchSemanticsNodes().singleOrNull() - if (heading != null) { - val layouts = mutableListOf() - rule.onNodeWithTag("title-heading").performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> action(layouts) } - val layout = layouts.single() - val metrics = JSONObject().put("title", layout.layoutInput.text.text).put("lineCount", layout.lineCount) - .put("lineWidthsPx", org.json.JSONArray(List(layout.lineCount) { layout.getLineRight(it) - layout.getLineLeft(it) })) - .put("ellipsized", org.json.JSONArray(List(layout.lineCount) { layout.isLineEllipsized(it) })) - .put("maxWidthPx", layout.layoutInput.constraints.maxWidth).put("fontSizeSp", layout.layoutInput.style.fontSize.value) - .put("requestedWeight", layout.layoutInput.style.fontWeight?.weight) - .put("boundsPx", org.json.JSONArray(listOf(heading.boundsInRoot.left, heading.boundsInRoot.top, heading.boundsInRoot.right, heading.boundsInRoot.bottom))) - File(directory, "st-${number.toString().padStart(3, '0')}-layout.json").writeText(metrics.toString(2)) - } - recordTextMetrics(directory, number) - recordElements(directory, number) - } - - private fun recordTextMetrics(directory: File, number: Int) { - val elements = rule.onAllNodes(SemanticsMatcher.keyIsDefined(SemanticsActions.GetTextLayoutResult), useUnmergedTree = true) - val records = org.json.JSONArray() - elements.fetchSemanticsNodes().forEachIndexed { index, node -> - if (node.boundsInRoot.width > 0 && node.boundsInRoot.height > 0) { - val layouts = mutableListOf() - elements[index].performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> action(layouts) } - val layout = layouts.single() - records.put(JSONObject().put("text", layout.layoutInput.text.text) - .put("fontSizeSp", layout.layoutInput.style.fontSize.value) - .put("weight", layout.layoutInput.style.fontWeight?.weight) - .put("colourArgb", Integer.toUnsignedString(layout.layoutInput.style.color.toArgb(), 16)) - .put("lineHeightSp", layout.layoutInput.style.lineHeight.value.takeIf { it.isFinite() } ?: JSONObject.NULL) - .put("lineCount", layout.lineCount) - .put("boundsPx", org.json.JSONArray(listOf(node.boundsInRoot.left, node.boundsInRoot.top, node.boundsInRoot.right, node.boundsInRoot.bottom))) - .put("spans", org.json.JSONArray(layout.layoutInput.text.spanStyles.map { span -> - JSONObject().put("start", span.start).put("end", span.end).put("weight", span.item.fontWeight?.weight) - }))) - } - } - File(directory, "st-${number.toString().padStart(3, '0')}-text.json").writeText(records.toString(2)) - } - - private fun recordElements(directory: File, number: Int) { - val tag = androidx.compose.ui.semantics.SemanticsProperties.TestTag - val description = androidx.compose.ui.semantics.SemanticsProperties.ContentDescription - val matcher = SemanticsMatcher.keyIsDefined(tag) or SemanticsMatcher.keyIsDefined(description) - val records = org.json.JSONArray() - rule.onAllNodes(matcher, useUnmergedTree = true).fetchSemanticsNodes().forEach { node -> - records.put(JSONObject() - .put("tag", if (node.config.contains(tag)) node.config[tag] else JSONObject.NULL) - .put("description", if (node.config.contains(description)) org.json.JSONArray(node.config[description]) else JSONObject.NULL) - .put("boundsPx", org.json.JSONArray(listOf(node.boundsInRoot.left, node.boundsInRoot.top, node.boundsInRoot.right, node.boundsInRoot.bottom)))) - } - File(directory, "st-${number.toString().padStart(3, '0')}-elements.json").writeText(records.toString(2)) - } - - private fun option(state: TitleState, id: String) { state.focus("header", state.options.indexOfFirst { it.id == id }) } - private fun open(state: TitleState, shell: ShellState, id: String) { - option(state, id) - state.key(ShellKey.Ok, true, 0, shell) - state.key(ShellKey.Ok, false, 0, shell) - } - - @Test fun st042_heldMovie() = capture(42, "The Girl Who Leapt Through Time") - @Test fun st043_options() = capture(43) { state, _ -> option(state, "version") } - @Test fun st044_versions() = capture(44) { state, shell -> open(state, shell, "version") } - @Test fun st045_rememberedVersion() = capture(45) { state, _ -> state.versionId = "v2"; option(state, "version") } - @Test fun st046_tracks() = capture(46) { state, shell -> open(state, shell, "tracks") } - @Test fun st047_mediaInfo() = capture(47) { state, shell -> open(state, shell, "more") } - @Test fun st048_trailerNoApp() = capture(48) { state, _ -> option(state, "trailer"); state.toast = "No YouTube app is available to play this trailer." } - @Test fun st049_watched() = capture(49, transform = { it.copy(watchTarget = it.watchTarget.copy(watched = true)) }) { state, _ -> option(state, "watched") } - @Test fun st050_seriesConfirm() = capture(50, "Andor") { state, shell -> open(state, shell, "watched") } - @Test fun st051_episodeRestart() = capture(51, "episode-sex-and-the-country-2001-s4e9", "mindel") { state, _ -> option(state, "restart") } - @Test fun st052_filmCast() = capture(52) { state, shell -> - state.focus("cast", 0) - state.key(ShellKey.Right, true, 0, shell) - state.key(ShellKey.Right, false, 0, shell) - } - @Test fun st053_seriesCast() = capture(53, "Andor") { state, _ -> state.focus("cast", 0) } - @Test fun st054_initialsCast() = capture(54, "Spirited Away", "shared") { state, _ -> state.focus("cast", 0) } - @Test fun st055_castLoading() = capture(55, transform = { it.copy(cast = emptyList(), castStatus = TitleReadStatus.Loading) }) - @Test fun st056_castUnavailable() = capture(56, transform = { it.copy(cast = emptyList(), castStatus = TitleReadStatus.Unavailable) }) { state, _ -> state.focus("related", 0) } - @Test fun st057_person() { - val (person, credits) = fixture.person("Tom Holland", "justin") - val shell = ShellState(fixture.people, "justin").apply { pickerVisible = false; enterDestination("person/Tom Holland") } - val state = PersonState(person, credits, {}) - val data = TitlePageData(credits.first(), cast = listOf(person), related = credits) - val art = decodeArt(data) - rule.setContent { WolfskinTheme { CompositionLocalProvider(LocalTitleBitmaps provides art) { - PersonSurface(state, shell, Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight)) - } } } - save(57) - } - @Test fun st058_hiddenReceipt() = capture(58, "The Amazing Spider-Man") { state, shell -> open(state, shell, "hide") } - @Test fun st059_hiddenPick() = capture(59, "The Amazing Spider-Man", transform = { it.copy(hidden = true) }) { state, _ -> option(state, "unhide") } - @Test fun st060_titleNav() = capture(60) { _, shell -> shell.focusNav(0) } - @Test fun st061_loading() = capture(61, transform = { it.copy(status = TitleReadStatus.Loading) }) { _, shell -> shell.focusNav(0) } - @Test fun st062_error() = capture(62, transform = { it.copy(status = TitleReadStatus.Unavailable) }) - @Test fun st063_signedOut() = capture(63, "The Amazing Spider-Man", transform = { it.copy(connection = TitleConnection.SignedOut) }) - @Test fun st064_seriesNext() = capture(64, "Andor") - @Test fun st065_resumeEpisode() = capture(65, "episode-sex-and-the-country-2001-s4e9", "mindel") - @Test fun st066_notHeld() = capture(66, "The Amazing Spider-Man") - @Test fun st067_trailerFallback() = capture(67, "The Amazing Spider-Man") - @Test fun st068_requested() = capture(68, "The Amazing Spider-Man", transform = { it.copy(requestedStage = "Searching", requestActor = "Justin") }) - @Test fun st069_missingArt() = capture(69, "The Amazing Spider-Man", transform = { it.copy(item = it.item.copy(poster = null, backdrop = null)) }) - @Test fun st070_offline() = capture(70, "The Amazing Spider-Man", transform = { - it.copy(connection = TitleConnection.Offline, savedAt = "20:42", item = it.item.copy(ratings = emptyList())) - }) - @Test fun st104_relatedPlate() = capture(104, "The Girl Who Leapt Through Time") { state, _ -> state.focus("related", state.data.related.indexOfFirst { it.name == "INVINCIBLE" }.coerceAtLeast(0)) } - @Test fun st106_heldRatings() = capture(106, "American Gangster", "shared") - @Test fun st107_seriesEpisodeRatings() = capture(107, "Andor") - @Test fun st108_resumingEpisodeRatings() = capture(108, "episode-sex-and-the-country-2001-s4e9", "mindel") - @Test fun st109_noRatings() = capture(109, "The Amazing Spider-Man") -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleKeysTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleKeysTest.kt deleted file mode 100644 index 6df8426d..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleKeysTest.kt +++ /dev/null @@ -1,345 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performMouseInput -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performSemanticsAction -import androidx.compose.ui.semantics.SemanticsActions -import androidx.compose.ui.text.TextLayoutResult -import org.justinvisser.wolfskin.design.WolfskinTokens -import org.json.JSONArray -import org.json.JSONObject -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.ShellAction -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.TitleAction -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitleReadStatus -import org.justinvisser.wolfskin.design.TitleSheet -import org.justinvisser.wolfskin.design.TitleState -import org.justinvisser.wolfskin.design.TitleSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -/** Approved title-entry suffixes replay Android keys through the rendered title and shared shell. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -class TitleKeysTest { - @get:Rule val rule = createAndroidComposeRule() - private lateinit var fixture: TitleFixture - private val effects = mutableListOf() - private val shellEffects = mutableListOf() - private lateinit var state: TitleState - private lateinit var shell: ShellState - private lateinit var paths: List - - @Before fun fixtures() { - fixture = TitleFixture(rule.activity.assets.open("wolfskin-titles.json").bufferedReader().use { JSONObject(it.readText()) }) - val data = javaClass.getResourceAsStream("/title-press-paths.json")!!.bufferedReader().use { JSONArray(it.readText()) } - paths = List(data.length()) { data.getJSONObject(it) } - } - - private fun render(title: String = "Locke", transform: (org.justinvisser.wolfskin.design.TitlePageData) -> org.justinvisser.wolfskin.design.TitlePageData = { it }) { - shell = ShellState(fixture.people, "justin", shellEffects::add).apply { pickerVisible = false; pickerLaunch = false; enterDestination(titleRoute(title)) } - state = TitleState(transform(fixture.title(title, "justin")), shell.person, effects::add) - rule.setContent { WolfskinTheme { - BackHandler { state.back(shell) } - TitleSurface(state, shell) - } } - } - - private fun press(key: Key) { rule.onRoot().performKeyInput { pressKey(key) } } - private fun replay(end: String): Int { - val path = paths.single { it.getString("end") == end } - render(path.getString("title")) - val keys = path.getJSONArray("keys") - var presses = 0 - for (index in path.getInt("entryPresses") until keys.length()) { - val key = when (keys.getString(index)) { - "UP" -> Key.DirectionUp - "DOWN" -> Key.DirectionDown - "LEFT" -> Key.DirectionLeft - "RIGHT" -> Key.DirectionRight - "OK" -> Key.DirectionCenter - else -> error("Unknown replay key") - } - press(key) - presses++ - } - return presses - } - - @Test fun titleToNavOnePressFromEntry() { - assertEquals(1, replay("nav")) - assertTrue(shell.navFocused) - assertEquals(shell.currentTab, shell.navIndex) - } - - @Test fun switchPersonFivePressesFromTitleEntry() { - assertEquals(5, replay("person-mindel")) - assertEquals("mindel", shell.person.key) - assertEquals("home", shell.route) - assertFalse(shell.pickerVisible) - assertTrue(shellEffects.last() is ShellAction.Person) - } - - @Test fun lockeSecondVersionSixPressesFromTitleEntry() { - assertEquals(6, replay("play-version-2")) - assertEquals("v2", state.versionId) - assertNull(state.sheet) - assertEquals("play", state.options[state.column].id) - assertTrue(effects.last() is TitleAction.Play) - assertEquals("Locke", (effects.last() as TitleAction.Play).item.name) - } - - @Test fun lockeCastTwoPressesFromTitleEntry() { - assertEquals(2, replay("person-Tom Hardy")) - assertEquals("cast", state.row) - assertEquals("Tom Hardy", (effects.single() as TitleAction.Person).person.name) - } - - @Test fun sheetBackRestoresItsOpeningOptionAndChosenVersion() { - render() - press(Key.DirectionRight) - press(Key.DirectionCenter) - assertEquals(TitleSheet.Version, state.sheet) - press(Key.DirectionDown) - press(Key.DirectionCenter) - assertEquals("v2", state.versionId) - assertEquals("Version", state.options[state.column].label) - press(Key.DirectionCenter) - assertEquals(1, state.sheetIndex) - press(Key.Back) - assertNull(state.sheet) - assertEquals("Version", state.options[state.column].label) - assertFalse(shell.navFocused) - } - - @Test fun trackChoiceStaysInSheetAndOffRemainsAnExplicitChoice() { - render() - press(Key.DirectionRight); press(Key.DirectionRight); press(Key.DirectionCenter) - assertEquals(TitleSheet.Tracks, state.sheet) - press(Key.DirectionDown); press(Key.DirectionCenter) - assertEquals(1, state.audioIndex) - assertEquals(TitleSheet.Tracks, state.sheet) - press(Key.DirectionDown); press(Key.DirectionDown); press(Key.DirectionCenter) - assertEquals(2, state.subtitleIndex) - press(Key.DirectionUp); press(Key.DirectionCenter) - assertEquals(-1, state.subtitleIndex) - press(Key.Back) - assertNull(state.sheet) - assertEquals("tracks", state.options[state.column].id) - } - - @Test fun castLoadingAndUnavailableAreNeverFocusable() { - render(transform = { it.copy(cast = emptyList(), castStatus = TitleReadStatus.Loading) }) - press(Key.DirectionDown) - assertEquals("related", state.row) - assertFalse(state.rows.contains("cast")) - } - - @Test fun wholeSeriesRequiresConfirmationButEpisodesDoNot() { - render("Andor") - rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "watched" }) } - press(Key.DirectionCenter) - assertEquals(TitleSheet.ConfirmWatched, state.sheet) - assertFalse(state.watched) - press(Key.DirectionRight); press(Key.DirectionCenter) - assertFalse(state.watched) - assertTrue(effects.none { it is TitleAction.Watched }) - press(Key.DirectionCenter); press(Key.DirectionCenter) - assertTrue(state.watched) - assertTrue(effects.last() is TitleAction.Watched) - } - - @Test fun titlesNobodyHoldsHaveNoTrailerUntilTheCatalogueFieldExists() { - render("The Amazing Spider-Man") - assertTrue(state.options.none { it.id == "trailer" }) - assertEquals("Request", state.options.first().label) - rule.runOnIdle { state.update(state.data.copy(connection = TitleConnection.SignedOut)) } - assertEquals(listOf("retry"), state.options.map { it.id }) - } - - @Test fun hiddenReceiptUndoRestoresTitleAndOptionInPlace() { - render("The Amazing Spider-Man") - press(Key.DirectionRight); press(Key.DirectionRight); press(Key.DirectionCenter) - assertTrue(state.data.hidden) - assertEquals(TitleSheet.HiddenReceipt, state.sheet) - press(Key.DirectionCenter) - assertFalse(state.data.hidden) - assertNull(state.sheet) - assertEquals("hide", state.options[state.column].id) - } - - @Test fun heldOkCannotChooseTheVersionInTheSheetItOpened() { - render() - press(Key.DirectionRight) - rule.runOnIdle { - rule.activity.dispatchKeyEvent(android.view.KeyEvent(0, 0, android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_DPAD_CENTER, 0)) - repeat(5) { index -> rule.activity.dispatchKeyEvent(android.view.KeyEvent(0, 0, android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_DPAD_CENTER, index + 1)) } - rule.activity.dispatchKeyEvent(android.view.KeyEvent(0, 0, android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_DPAD_CENTER, 0)) - } - assertEquals(TitleSheet.Version, state.sheet) - assertTrue(effects.none { it is TitleAction.Version }) - } - - @Test fun episodeWatchedToggleKeepsItsIdentityAndDoesNotAskAboutTheSeries() { - val episode = requireNotNull(fixture.title("Andor", "justin").playTarget) - render(episode.id) - rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "watched" }) } - press(Key.DirectionCenter) - assertNull(state.sheet) - val change = effects.single() as TitleAction.Watched - assertEquals(episode.id, change.item.id) - assertEquals(!episode.watched, change.watched) - } - - @Test fun aPendingSeasonSkipsItsEpisodeSkeletons() { - render("Andor") - rule.runOnIdle { state.showSeasonLoading() } - press(Key.DirectionDown); press(Key.DirectionDown) - assertEquals("cast", state.row) - rule.runOnIdle { state.restoreSeason() } - press(Key.DirectionUp) - assertEquals("episodes", state.row) - } - - @Test fun aFailedOlderVersionWriteCannotUndoTheNewerChoice() { - render() - press(Key.DirectionRight); press(Key.DirectionCenter); press(Key.DirectionDown); press(Key.DirectionCenter) - val first = effects.last() as TitleAction.Version - press(Key.DirectionCenter); press(Key.DirectionUp); press(Key.DirectionCenter) - val second = effects.last() as TitleAction.Version - rule.runOnIdle { state.rejectPlayback(first); state.confirmPlayback(second) } - assertEquals("v1", state.versionId) - press(Key.DirectionCenter); press(Key.DirectionDown); press(Key.DirectionCenter) - val failed = effects.last() as TitleAction.Version - rule.runOnIdle { state.rejectPlayback(failed) } - assertEquals("v1", state.versionId) - } - - @Test fun aNewRowUsesTheNearestHorizontalCardBeforeItsOwnRememberedCard() { - render() - repeat(4) { press(Key.DirectionRight) } - val x = rule.onNodeWithTag("title-option-trailer").fetchSemanticsNode().boundsInRoot.center.x - val cast = state.data.cast.take(WolfskinTokens.Card.personSlots) - val nearest = cast.indices.minBy { index -> - kotlin.math.abs(rule.onNodeWithTag("title-cast-${cast[index].id}").fetchSemanticsNode().boundsInRoot.center.x - x) - } - press(Key.DirectionDown) - assertEquals(nearest, state.column) - press(Key.DirectionRight) - val remembered = state.column - press(Key.DirectionUp); press(Key.DirectionDown) - assertEquals(remembered, state.column) - } - - @Test fun wheelThenRemoteLandsOnTheFirstVisibleFocusableRow() { - render() - rule.onNodeWithTag("title-page").performMouseInput { scroll(800f) } - val expected = rule.runOnIdle { - shell.view.listState.layoutInfo.visibleItemsInfo.first { - it.key in state.rows && it.offset + it.size > WolfskinTokens.Layout.pinnedLine.value * 2 - }.key as String - } - assertNotEquals("header", expected) - press(Key.DirectionDown) - assertEquals(expected, state.row) - } - @Test fun seriesOptionsKeepTheCompleteHouseholdActionLabel() { - render("Andor") - val layouts = mutableListOf() - rule.onNodeWithText("Add for someone", useUnmergedTree = true) - .performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> action(layouts) } - val layout = layouts.single() - assertFalse("The final pill must not shrink to its icon or a clipped label", layout.didOverflowWidth) - assertEquals(1, layout.lineCount) - } - - @Test fun anInitialLoadNeverExposesStaleCastBelowItsSkeleton() { - render(transform = { it.copy(status = TitleReadStatus.Loading) }) - rule.onNodeWithText("Cast").assertDoesNotExist() - press(Key.DirectionDown) - assertTrue(shell.navFocused) - assertEquals("header", state.row) - rule.runOnIdle { rule.activity.onBackPressedDispatcher.onBackPressed() } - assertEquals(listOf(TitleAction.Pop), effects) - } - - - @Test fun unratedFactsDoNotReserveAnEmptyRatingsSlot() { - render("The Amazing Spider-Man", transform = { - it.copy(item = it.item.copy(ratings = emptyList(), genres = listOf("Drama"), runtimeSeconds = 0), - cast = emptyList(), related = emptyList()) - }) - val year = rule.onNodeWithText("2012", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot - val genre = rule.onNodeWithText("Drama", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot - assertEquals(WolfskinTokens.Title.factsGap.value * 2, genre.left - year.right, 1f) - } - - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Test fun mediaInfoRowsPreserveTheirDeclaredLineBoxes() { - render() - rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "more" }) } - press(Key.DirectionCenter) - val container = rule.onNodeWithText("Container", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot - val size = rule.onNodeWithText("Size", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot - val expectedPitch = (WolfskinTokens.Title.infoKey.lineHeight.value + WolfskinTokens.Title.infoRowGap.value) * 2 - assertEquals("Stacked facts retain the CSS line-box rhythm", expectedPitch, size.top - container.top, 1f) - val lastSubtitle = rule.onNodeWithText("Dutch", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot - assertEquals("Fourteen stacked lines do not accumulate native pixel rounding", 526.2f, lastSubtitle.top - container.top, 1f) - } - - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Test fun filmHeaderKeepsItsFixedCssStack() { - render() - assertEquals(358f, rule.onNodeWithTag("title-facts").fetchSemanticsNode().boundsInRoot.top, 1f) - assertEquals(410f, rule.onNodeWithTag("title-overview").fetchSemanticsNode().boundsInRoot.top, 1f) - assertEquals(553f, rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot.top, 1f) - } - - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Test fun episodeProgressReclaimsItsNegativeMarginBeforeTheActionRow() { - render("Andor") - assertEquals(603f, rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot.top, 1f) - } - - @Test fun everyLongTrackChoiceAndDoneRemainInsideTheVisibleSheet() { - render(transform = { data -> - val source = data.versions.first().copy( - audio = List(3) { org.justinvisser.wolfskin.design.TitleTrack(it + 3, "Audio ${it + 1}") }, - subtitles = List(15) { org.justinvisser.wolfskin.design.TitleTrack(it + 20, "Subtitle ${it + 1}") }, - ) - data.copy(versions = listOf(source)) - }) - rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "tracks" }) } - press(Key.DirectionCenter) - val bottom = rule.onRoot().fetchSemanticsNode().boundsInRoot.bottom - val labels = (1..3).map { "Audio $it" } + "Off" + (1..15).map { "Subtitle $it" } + "Done" - labels.forEachIndexed { index, label -> - if (index > 0) press(Key.DirectionDown) - rule.waitForIdle() - val bounds = rule.onNodeWithText(label, useUnmergedTree = true).fetchSemanticsNode().boundsInRoot - assertTrue("$label must have visible height: $bounds", bounds.height > 20) - assertTrue("$label stays above the viewport bottom: $bounds / $bottom", bounds.bottom <= bottom) - assertTrue("$label stays below the viewport top: $bounds", bounds.top >= 0) - } - press(Key.DirectionCenter) - assertNull(state.sheet) - assertFalse(shell.navFocused) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleLaunchKeysTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleLaunchKeysTest.kt deleted file mode 100644 index 07636e5a..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleLaunchKeysTest.kt +++ /dev/null @@ -1,132 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import org.json.JSONArray -import org.json.JSONObject -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.* -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** Full launch paths render the real picker, Home hero/shelves, title and person surfaces. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -class TitleLaunchKeysTest { - @get:Rule val rule = createAndroidComposeRule() - private lateinit var fixture: TitleFixture - private lateinit var home: HomeFixture - private lateinit var shell: ShellState - private var title by mutableStateOf(null) - private var person by mutableStateOf(null) - private val actions = mutableListOf() - private val shellActions = mutableListOf() - private lateinit var paths: List - - @Before fun launch() { - fixture = TitleFixture(rule.activity.assets.open("wolfskin-titles.json").bufferedReader().use { JSONObject(it.readText()) }) - home = HomeFixture(rule.activity) - val source = javaClass.getResourceAsStream("/title-press-paths.json")!!.bufferedReader().use { JSONArray(it.readText()) } - paths = List(source.length()) { source.getJSONObject(it) } - shell = ShellState(fixture.people, "justin") { action -> - shellActions.add(action) - if (action is ShellAction.Person) { title = null; person = null } - } - rule.setContent { WolfskinTheme { - val performer = person - val page = title - val snapshot = remember(shell.person.key) { home.snapshot(shell.person.key) } - when { - performer != null -> PersonSurface(performer, shell) - page != null -> TitleSurface(page, shell) - else -> ShellSurface(shell) { modifier -> - HomeScreen(shell, snapshot, modifier) { action -> - if (action is HomeAction.Details) openTitle(action.title.id) - } - } - } - } } - } - - private fun openTitle(id: String) { - val data = fixture.title(id, shell.person.key) - title = TitleState(data, shell.person, ::act) - person = null - shell.enterDestination(titleRoute(id)) - } - - private fun act(action: TitleAction) { - actions.add(action) - when (action) { - is TitleAction.Person -> { - val (actor, credits) = fixture.person(action.person.id, shell.person.key) - person = PersonState(actor, credits, ::act) - shell.enterDestination(personRoute(actor.id)) - } - is TitleAction.OpenTitle -> openTitle(action.item.id) - else -> Unit - } - } - - private fun replay(end: String, expectedPresses: Int) { - val path = paths.single { it.getString("end") == end } - val keys = path.getJSONArray("keys") - assertEquals(expectedPresses, path.getInt("presses")) - assertEquals(expectedPresses, keys.length()) - for (index in 0 until keys.length()) { - val key = when (keys.getString(index)) { - "UP" -> Key.DirectionUp; "DOWN" -> Key.DirectionDown - "LEFT" -> Key.DirectionLeft; "RIGHT" -> Key.DirectionRight - "OK" -> Key.DirectionCenter - else -> error("Unknown launch replay key") - } - rule.onRoot().performKeyInput { pressKey(key) } - } - } - - @Test fun titleToNavTakesFourPressesFromTheRealHomeHero() { - replay("nav", 4) - assertNotNull(title) - assertTrue(shell.navFocused) - assertEquals(shell.currentTab, shell.navIndex) - } - - @Test fun switchingPersonFromATitleTakesEightLaunchPresses() { - replay("person-mindel", 8) - assertEquals("mindel", shell.person.key) - assertFalse(shell.pickerVisible) - assertEquals("home", shell.route) - assertTrue(shellActions.last() is ShellAction.Person) - } - - @Test fun lockeVersionTwoAndPlayTakesFifteenLaunchPresses() { - replay("play-version-2", 15) - val page = requireNotNull(title) - assertEquals("Locke", page.data.item.name) - assertEquals("v2", page.versionId) - assertNull(page.sheet) - assertEquals("play", page.options[page.column].id) - assertEquals("Locke", (actions.last() as TitleAction.Play).item.name) - } - - @Test fun lockeToTomHardyTakesElevenLaunchPresses() { - replay("person-Tom Hardy", 11) - assertEquals("Tom Hardy", requireNotNull(person).person.name) - assertEquals(0, person!!.index) - assertFalse(shell.navFocused) - assertTrue(actions.last() is TitleAction.Person) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleLocaleTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleLocaleTest.kt deleted file mode 100644 index e9462bff..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleLocaleTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.compose.ui.semantics.SemanticsActions -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onLast -import androidx.compose.ui.test.performSemanticsAction -import androidx.compose.ui.text.TextLayoutResult -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitleItem -import org.justinvisser.wolfskin.design.TitlePageData -import org.justinvisser.wolfskin.design.LibraryRating -import org.justinvisser.wolfskin.design.TitleState -import org.justinvisser.wolfskin.design.TitleSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode -import java.util.Locale - -/** Native text layout proves locale formatting still fits the focused poster's one-line plate. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class TitleLocaleTest { - @get:Rule val rule = createAndroidComposeRule() - - private fun render(locale: Locale, expected: String) { - val previous = Locale.getDefault() - try { - Locale.setDefault(locale) - val person = ShellPerson("justin", "Justin", "J", false) - val item = TitleItem("movie:1", "Title", "movie", ratings = listOf(LibraryRating("tmdb", 7.8))) - val shell = ShellState(listOf(person), person.key).apply { pickerVisible = false; enterDestination("title/movie:1") } - val state = TitleState(TitlePageData(item, related = listOf(item.copy(id = "movie:2")), connection = TitleConnection.SignedIn), person) {} - state.focus("related", 0) - rule.setContent { WolfskinTheme { TitleSurface(state, shell) } } - rule.mainClock.advanceTimeBy(1000) - val layouts = mutableListOf() - rule.onAllNodesWithText(expected, useUnmergedTree = true).onLast().performSemanticsAction(SemanticsActions.GetTextLayoutResult) { get -> get(layouts) } - assertEquals(1, layouts.single().lineCount) - assertFalse(layouts.single().didOverflowWidth) - } finally { Locale.setDefault(previous) } - } - - @Test fun dutchScoreUsesCommaOnTheNativePlate() = render(Locale.forLanguageTag("nl-NL"), "7,8") - @Test fun englishScoreUsesPointOnTheNativePlate() = render(Locale.US, "7.8") -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleReadFacadeTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleReadFacadeTest.kt deleted file mode 100644 index 1a672f67..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleReadFacadeTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.content.Context -import com.github.damontecres.wholphin.WholphinApplication -import com.github.damontecres.wholphin.data.ItemPlaybackRepository -import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.FavoriteWatchManager -import com.github.damontecres.wholphin.services.ImageUrlService -import com.github.damontecres.wholphin.ui.successQueryResult -import com.github.damontecres.wholphin.ui.successResponse -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkObject -import kotlinx.coroutines.test.runTest -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.itemsApi -import org.jellyfin.sdk.api.client.extensions.tvShowsApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.api.operations.ItemsApi -import org.jellyfin.sdk.api.operations.TvShowsApi -import org.jellyfin.sdk.api.operations.UserLibraryApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import com.github.damontecres.wholphin.test.TestTracks -import org.jellyfin.sdk.model.api.MediaStreamType -import org.jellyfin.sdk.model.api.request.GetEpisodesRequest -import org.jellyfin.sdk.model.api.request.GetItemsRequest -import org.junit.Assert.* -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitleState -import org.justinvisser.wolfskin.tv.read.TitleReadFacade -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.RuntimeEnvironment -import java.util.UUID - -/** SDK replies stay in-process; disk reload and identity isolation exercise the real facade. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class) -class TitleReadFacadeTest { - @get:Rule val directory = TemporaryFolder() - private val library = mockk() - private val items = mockk() - private val shows = mockk() - private val api = mockk() - private val playback = mockk() - private val repository = mockk() - private val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null) - private lateinit var facade: TitleReadFacade - private val firstId = UUID.fromString("00000000-0000-0000-0000-000000000001") - private val secondId = UUID.fromString("00000000-0000-0000-0000-000000000002") - private val seriesId = UUID.fromString("00000000-0000-0000-0000-000000000003") - private val seasonId = UUID.fromString("00000000-0000-0000-0000-000000000004") - private val sourceId = UUID.fromString("00000000-0000-0000-0000-000000000005").toString().replace("-", "") - - @Before fun setup() { - // BaseItem formats runtime using application resources; no Hilt/network application is started. - val app = mockk { - every { resources } returns RuntimeEnvironment.getApplication().resources - } - mockkObject(WholphinApplication.Companion) - every { WholphinApplication.instance } returns app - every { api.userLibraryApi } returns library - every { api.itemsApi } returns items - every { api.tvShowsApi } returns shows - every { repository.currentServer } returns null - every { repository.currentUser } returns null - coEvery { playback.getSelectedTracks(any(), any(), any()) } returns null - val context = mockk { every { filesDir } returns directory.root } - facade = TitleReadFacade(api, mockk(relaxed = true), playback, repository, mockk(), context) - } - - @After fun teardown() { - unmockkObject(WholphinApplication.Companion) - } - - private fun file(id: UUID, kind: BaseItemKind = BaseItemKind.MOVIE): BaseItemDto { - val source = TestTracks.Builder().addVideo().addAudio().addSubtitle().buildMediaSourceInfo() - val streams = source.mediaStreams.orEmpty().map { stream -> - when (stream.type) { - MediaStreamType.VIDEO -> stream.copy(index = 0, displayTitle = "1080p") - MediaStreamType.AUDIO -> stream.copy(index = 3, displayTitle = "English", isDefault = true) - MediaStreamType.SUBTITLE -> stream.copy(index = 8, displayTitle = "Dutch") - else -> error("Unexpected track in the SDK fixture builder") - } - } - return BaseItemDto(id = id, name = "A real file", type = kind, runTimeTicks = 6_000_000_000, - mediaSources = listOf(source.copy(id = sourceId, name = "Original", container = "mkv", mediaStreams = streams))) - } - - @Test fun diskCopyPreservesFileAndNonContiguousStreamIndexesWithoutRatingsBeforeSignIn() = runTest { - coEvery { library.getItem(firstId) } returns successResponse(file(firstId)) - facade.read(firstId, null, preferences) - coEvery { library.getItem(any()) } throws java.io.IOException("offline") - val saved = facade.readSaved(firstId, preferences)!! - val screen = TitleState(saved, ShellPerson("justin", "Justin", "J", false)) {} - assertEquals("MKV", saved.versions.single().container) - assertEquals(3, screen.audio?.index) - assertEquals(listOf(8), saved.versions.single().subtitles.map { it.index }) - assertEquals(TitleConnection.SignedOut, saved.connection) - assertTrue(saved.item.ratings.isEmpty()) - assertEquals(firstId.toString(), saved.watchTarget.id) - } - - @Test fun diskCopiesNeverCrossViewerIdentity() = runTest { - val userA = mockk { every { id } returns firstId } - val userB = mockk { every { id } returns secondId } - every { repository.currentUser } returns userA - coEvery { library.getItem(firstId) } returns successResponse(file(firstId)) - facade.read(firstId, null, preferences) - every { repository.currentUser } returns userB - assertNull(facade.readSaved(firstId, preferences)) - every { repository.currentUser } returns userA - assertEquals("A real file", facade.readSaved(firstId, preferences)?.item?.name) - } - - @Test fun personBiographyAndContinuousCreditsRemainReadableFromDiskWithoutAService() = runTest { - val actor = BaseItemDto(id = firstId, type = BaseItemKind.PERSON, name = "Actor", overview = "Actual biography") - coEvery { library.getItem(firstId) } returns successResponse(actor) - coEvery { items.getItems(any()) } returns successQueryResult(listOf(file(secondId))) - facade.readPerson(firstId, 0) - coEvery { library.getItem(any()) } throws java.io.IOException("offline") - coEvery { items.getItems(any()) } throws java.io.IOException("offline") - val saved = facade.readPersonSaved(firstId)!! - assertEquals("Actual biography", saved.person.biography) - assertEquals(secondId.toString(), saved.credits.single().id) - assertEquals("A real file", saved.credits.single().name) - } - - @Test fun personSavedCopiesNeverCrossServerOrViewerIdentity() = runTest { - val userA = mockk { every { id } returns firstId } - val userB = mockk { every { id } returns secondId } - val serverA = mockk { every { id } returns seriesId } - val serverB = mockk { every { id } returns seasonId } - every { repository.currentUser } returns userA - every { repository.currentServer } returns serverA - coEvery { library.getItem(firstId) } returns successResponse( - BaseItemDto(id = firstId, type = BaseItemKind.PERSON, name = "Saved actor", overview = "Private biography")) - coEvery { items.getItems(any()) } returns successQueryResult(listOf(file(secondId))) - facade.readPerson(firstId, 0) - every { repository.currentUser } returns userB - assertNull(facade.readPersonSaved(firstId)) - every { repository.currentUser } returns userA - every { repository.currentServer } returns serverB - assertNull(facade.readPersonSaved(firstId)) - every { repository.currentServer } returns serverA - val saved = requireNotNull(facade.readPersonSaved(firstId)) - assertEquals("Private biography", saved.person.biography) - assertEquals(secondId.toString(), saved.credits.single().id) - } - - @Test fun explicitEpisodeStaysThePlayAndWatchTargetAcrossDifferentSeasonSelection() = runTest { - val episode = file(firstId, BaseItemKind.EPISODE).copy(seriesId = seriesId, parentIndexNumber = 4, indexNumber = 9) - val other = file(secondId, BaseItemKind.EPISODE).copy(seriesId = seriesId, parentIndexNumber = 2, indexNumber = 1) - val series = BaseItemDto(id = seriesId, name = "Series", type = BaseItemKind.SERIES) - val secondSeason = BaseItemDto(id = seasonId, type = BaseItemKind.SEASON, indexNumber = 2) - coEvery { library.getItem(firstId) } returns successResponse(episode) - coEvery { library.getItem(seriesId) } returns successResponse(series) - coEvery { items.getItems(any()) } returns successQueryResult(listOf(secondSeason)) - coEvery { shows.getEpisodes(any()) } returns successQueryResult(listOf(other)) - val data = facade.read(firstId, 2, preferences) - assertEquals(seriesId.toString(), data.item.id) - assertEquals(firstId.toString(), data.playTarget?.id) - assertEquals(firstId.toString(), data.watchTarget.id) - assertEquals(listOf(secondId.toString()), data.episodes.map { it.id }) - assertEquals(listOf(2), data.seasonNumbers) - assertEquals(2, TitleState(data, ShellPerson("justin", "Justin", "J", false)) {}.season) - } - - @Test fun compactJellyfinSourceIdUsesTheSameVersionPersistenceKey() = runTest { - coEvery { library.getItem(firstId) } returns successResponse(file(firstId)) - coEvery { playback.savePlayVersion(any(), any()) } returns null - val data = facade.read(firstId, null, preferences) - facade.apply(org.justinvisser.wolfskin.design.TitleAction.Version(data.watchTarget, data.versions.single())) - coVerify(exactly = 1) { playback.savePlayVersion(firstId, UUID.fromString("00000000-0000-0000-0000-000000000005")) } - } - @Test fun subtitleOffDisablesPlaybackRatherThanRestoringThePreferredLanguage() = runTest { - coEvery { library.getItem(firstId) } returns successResponse(file(firstId)) - coEvery { playback.saveTrackSelection(any(), any(), any(), MediaStreamType.SUBTITLE) } returns - com.github.damontecres.wholphin.data.model.ItemPlayback(userId = 1, itemId = firstId) - val data = facade.read(firstId, null, preferences) - facade.apply(org.justinvisser.wolfskin.design.TitleAction.Track(data.watchTarget, data.versions.single(), audio = null, subtitle = -1)) - coVerify(exactly = 1) { playback.saveTrackSelection(any(), any(), com.github.damontecres.wholphin.data.model.TrackIndex.DISABLED, MediaStreamType.SUBTITLE) } - coVerify(exactly = 0) { playback.saveTrackSelection(any(), any(), com.github.damontecres.wholphin.data.model.TrackIndex.UNSPECIFIED, MediaStreamType.SUBTITLE) } - facade.apply(org.justinvisser.wolfskin.design.TitleAction.Track(data.watchTarget, data.versions.single(), audio = null, subtitle = 8)) - coVerify(exactly = 1) { playback.saveTrackSelection(any(), any(), 8, MediaStreamType.SUBTITLE) } - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleTrailerTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleTrailerTest.kt deleted file mode 100644 index afa137ee..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleTrailerTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.content.Context -import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.ResolveInfo -import android.net.Uri -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import org.robolectric.annotation.Config - -/** Browser handlers must never become a fallback for a missing YouTube app. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class) -class TitleTrailerTest { - private val context = ApplicationProvider.getApplicationContext() - private val url = "https://www.youtube.com/watch?v=abcdefghijk" - - @Test fun missingYoutubeHandlerStaysOnTheTitleEvenWhenABrowserExists() { - register(Intent(Intent.ACTION_VIEW, Uri.parse(url)), "test.browser") - assertFalse(TitleTrailer(context).open(url)) - assertNull(shadowOf(context as Application).nextStartedActivity) - } - - @Test fun installedYoutubeHandlerReceivesAnExplicitPackageAndNoBrowserUrl() { - register(Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:abcdefghijk")), "test.youtube") - assertTrue(TitleTrailer(context).open(url)) - val launched = shadowOf(context as Application).nextStartedActivity - assertEquals("test.youtube", launched.`package`) - assertEquals("vnd.youtube", launched.data?.scheme) - assertEquals("abcdefghijk", launched.data?.schemeSpecificPart) - } - - @Test fun nonYoutubeTrailerNeverOpensAGeneralViewHandler() { - register(Intent(Intent.ACTION_VIEW, Uri.parse("https://trailers.example/video")), "test.browser") - assertFalse(TitleTrailer(context).open("https://trailers.example/video")) - assertNull(shadowOf(context as Application).nextStartedActivity) - } - - private fun register(intent: Intent, name: String) { - val info = ResolveInfo().apply { - activityInfo = ActivityInfo().apply { packageName = name; this.name = "$name.Player"; exported = true } - } - shadowOf(context.packageManager).addResolveInfoForIntent(intent, info) - } -} diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/UpstreamContentFocusTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/UpstreamContentFocusTest.kt deleted file mode 100644 index d9e88778..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/UpstreamContentFocusTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.view.View -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toPixelMap -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsFocused -import androidx.compose.ui.test.assertIsNotFocused -import androidx.compose.ui.test.captureToImage -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import androidx.compose.ui.unit.dp -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.BackdropStyle -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.BackdropResult -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.takahirom.roborazzi.captureRoboImage -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import java.io.File -import java.util.UUID -import kotlinx.coroutines.flow.MutableStateFlow -import org.junit.After -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.ShellSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -/** The rollout-off path must preserve native content focus and its backdrop beneath the shell. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class UpstreamContentFocusTest { - @get:Rule val rule = createAndroidComposeRule() - - @Before fun prepare() { - WolfskinScreenPreference.initialize(rule.activity) - WolfskinScreenPreference.enabled = false - rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) - @Suppress("DEPRECATION") - rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - } - - @After fun restore() { WolfskinScreenPreference.enabled = true } - - private fun shellState() = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply { - pickerVisible = false - navFocused = false - } - - @Test fun switchOffUpMovesBetweenContentRowsBeforeEnteringNav() { - val state = shellState() - val first = FocusRequester() - val second = FocusRequester() - rule.setContent { - val upstream = resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, emptyMap()) - WolfskinTheme { - ShellSurface(state, handlesContentKeys = upstream == null) { contentModifier -> - Column(contentModifier.padding(start = 100.dp, top = 150.dp), verticalArrangement = Arrangement.spacedBy(32.dp)) { - Box(Modifier.size(200.dp, 60.dp).testTag("upstream-first").focusRequester(first).focusable()) - Box(Modifier.size(200.dp, 60.dp).testTag("upstream-second").focusRequester(second).focusable()) - } - } - } - } - rule.runOnIdle { second.requestFocus() } - rule.onNodeWithTag("upstream-second").assertIsFocused() - rule.onNodeWithTag("upstream-second").performKeyInput { pressKey(Key.DirectionUp) } - rule.onNodeWithTag("upstream-first").assertIsFocused() - rule.runOnIdle { assertFalse("Up from the second row must not enter nav", state.navFocused) } - rule.onNodeWithTag("upstream-first").performKeyInput { pressKey(Key.DirectionUp) } - rule.runOnIdle { assertTrue("Only the first row exits upward to nav", state.navFocused) } - rule.onNodeWithTag("upstream-first").assertIsNotFocused() - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionDown) } - rule.onNodeWithTag("upstream-first").assertIsFocused() - } - - @Test fun switchOffShowsRealUpstreamBackdropAndSwitchOnRestoresOwnedBackground() { - val state = shellState() - val model = mockk(relaxed = true) - every { model.session } returns WolfskinSession().also { it.state = state } - every { model.libraryDestinations } returns emptyMap() - // An unavailable saved identity uses the real sign-in placeholder, avoiding any server reads. - every { model.signedInPerson } returns "unavailable" - every { model.backdropFlow } returns MutableStateFlow(BackdropResult("fixture", null, Color.Red, Color.Red, Color.Red)) - coEvery { model.load(any(), any()) } returns Unit - val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null) - val user = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null) - val defaults = AppPreferences.getDefaultInstance() - val preferences = UserPreferences(defaults.toBuilder().setInterfacePreferences(defaults.interfacePreferences.toBuilder().setBackdropStyle(BackdropStyle.BACKDROP_DYNAMIC_COLOR)).build(), null) - val navigation = NavigationManager() - rule.setContent { WolfskinApp(server, user, navigation, preferences, Modifier.fillMaxSize(), model) } - rule.mainClock.advanceTimeBy(1500) - rule.waitForIdle() - val off = rule.onRoot().captureToImage().toPixelMap()[1700, 900] - assertTrue("The real dynamic backdrop must remain visible through native content", off.red > .4f && off.green < .1f && off.blue < .1f) - val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))) - directory.mkdirs() - rule.onRoot().captureRoboImage(File(directory, "review-upstream-off.png").absolutePath) - rule.runOnIdle { WolfskinScreenPreference.enabled = true } - rule.mainClock.advanceTimeBy(1500) - rule.waitForIdle() - rule.onNodeWithText("Sign in as Justin to open their Jellyfin library").assertIsDisplayed() - val on = rule.onRoot().captureToImage().toPixelMap()[1700, 900] - val background = org.justinvisser.wolfskin.design.WolfskinTokens.Color.bg - org.junit.Assert.assertEquals("Owned background red", background.red, on.red, 1f / 255f) - org.junit.Assert.assertEquals("Owned background green", background.green, on.green, 1f / 255f) - org.junit.Assert.assertEquals("Owned background blue", background.blue, on.blue, 1f / 255f) - rule.onRoot().captureRoboImage(File(directory, "review-upstream-on.png").absolutePath) - } -} diff --git a/app/src/test/resources/title-press-paths.json b/app/src/test/resources/title-press-paths.json deleted file mode 100644 index 14b28147..00000000 --- a/app/src/test/resources/title-press-paths.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "goal": "Round 3 ยท title page โ†’ nav", - "keys": [ - "OK", - "RIGHT", - "OK", - "UP" - ], - "presses": 4, - "note": "1 from the title page", - "start": "app launch: Who's watching with Justin focused (index.html:357)", - "replayed": "PASS", - "source": "index.html:2152-2174 PRESSES, replayed 2026-11-05", - "entryPresses": 3, - "title": "The Girl Who Leapt Through Time", - "end": "nav" - }, - { - "goal": "Round 3 ยท play Locke with version 2 (sample files)", - "keys": [ - "OK", - "DOWN", - "DOWN", - "DOWN", - "RIGHT", - "RIGHT", - "RIGHT", - "RIGHT", - "OK", - "RIGHT", - "OK", - "DOWN", - "OK", - "LEFT", - "OK" - ], - "presses": 15, - "note": "6 on the title page", - "start": "app launch: Who's watching with Justin focused (index.html:357)", - "replayed": "PASS", - "source": "index.html:2152-2174 PRESSES, replayed 2026-11-05", - "entryPresses": 9, - "title": "Locke", - "end": "play-version-2" - }, - { - "goal": "Round 3 ยท Locke โ†’ its first cast memberโ€™s page", - "keys": [ - "OK", - "DOWN", - "DOWN", - "DOWN", - "RIGHT", - "RIGHT", - "RIGHT", - "RIGHT", - "OK", - "DOWN", - "OK" - ], - "presses": 11, - "note": "2 on the title page: โ†“ to Cast, OK", - "start": "app launch: Who's watching with Justin focused (index.html:357)", - "replayed": "PASS", - "source": "index.html:2152-2174 PRESSES, replayed 2026-11-05", - "entryPresses": 9, - "title": "Locke", - "end": "person-Tom Hardy" - }, - { - "goal": "Round 3 ยท switch person from a title page", - "keys": [ - "OK", - "RIGHT", - "OK", - "UP", - "LEFT", - "OK", - "RIGHT", - "OK" - ], - "presses": 8, - "note": "5 from the title page", - "start": "app launch: Who's watching with Justin focused (index.html:357)", - "replayed": "PASS", - "source": "index.html:2152-2174 PRESSES, replayed 2026-11-05", - "entryPresses": 3, - "title": "The Girl Who Leapt Through Time", - "end": "person-mindel" - } -] diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ConductarrDiscoverOperations.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ConductarrDiscoverOperations.kt deleted file mode 100644 index 0c8a7067..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ConductarrDiscoverOperations.kt +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import kotlinx.coroutines.flow.last -import org.justinvisser.wolfskin.conductarr.ClientError -import org.justinvisser.wolfskin.conductarr.ConductarrClient -import org.justinvisser.wolfskin.conductarr.MediaType -import org.justinvisser.wolfskin.conductarr.MutationResult -import org.justinvisser.wolfskin.conductarr.ReadResult -import org.justinvisser.wolfskin.conductarr.TvRoutes -import org.justinvisser.wolfskin.conductarr.model.RecommendationHideInputBody -import org.justinvisser.wolfskin.conductarr.model.RequestIntentBody -import org.justinvisser.wolfskin.conductarr.model.RequestReceipt -import org.justinvisser.wolfskin.design.DiscoverOperations -import org.justinvisser.wolfskin.design.DiscoverRequest -import org.justinvisser.wolfskin.design.DiscoverRequestResult -import org.justinvisser.wolfskin.design.DiscoverSnapshot -import org.justinvisser.wolfskin.design.DiscoverTitle -import org.justinvisser.wolfskin.design.RequestPhase - -/** An injected signed-in client is the only real mutation path; the fixture host never constructs one. */ -class ConductarrDiscoverOperations( - private val client: ConductarrClient, - private val refreshSnapshot: suspend () -> DiscoverSnapshot, -) : DiscoverOperations { - override suspend fun request(intent: DiscoverRequest): DiscoverRequestResult { - val title = intent.title - require(title.type != "tv" || !title.seasons.isNullOrEmpty()) { "Whole-series season metadata is required" } - val body = RequestIntentBody( - mediaType = if (title.type == "movie") RequestIntentBody.MediaType.MOVIE else RequestIntentBody.MediaType.TV, - recommendationProfile = intent.profile.takeIf { intent.fromRecommendation }, requester = intent.requester, - seasons = title.seasons?.map(Int::toLong), - source = if (intent.fromRecommendation) RequestIntentBody.Source.RECOMMENDATION else RequestIntentBody.Source.TITLE, - tmdbId = title.tmdb, - ) - return when (val result = client.mutate(TvRoutes.request(body))) { - is MutationResult.Success -> result.value.outcome() - is MutationResult.Failure -> DiscoverRequestResult(if (result.error == ClientError.Http(403)) RequestPhase.Refused else RequestPhase.NotConfirmed) - } - } - - override suspend fun check(intent: DiscoverRequest): DiscoverRequestResult { - return when (val result = client.read(TvRoutes.requests(intent.title.tmdb, intent.title.mediaType()), org.justinvisser.wolfskin.conductarr.ReadTrigger.FORCE).last()) { - is ReadResult.Value -> result.saved.value?.requests?.firstOrNull { it.requester == intent.requester }?.outcome() ?: DiscoverRequestResult(RequestPhase.NotConfirmed) - is ReadResult.Failure -> DiscoverRequestResult(RequestPhase.NotConfirmed) - } - } - - override suspend fun hide(profile: String, title: DiscoverTitle): Boolean { - val result = client.mutate(TvRoutes.hide(profile, - RecommendationHideInputBody(if (title.type == "movie") RecommendationHideInputBody.MediaType.MOVIE else RecommendationHideInputBody.MediaType.TV, - title = title.title, tmdbId = title.tmdb, year = title.year?.toLong()))) - return result is MutationResult.Success || observedHidden(profile, title) == true - } - - override suspend fun restore(profile: String, title: DiscoverTitle): Boolean { - val result = client.mutate(TvRoutes.restore(profile, title.tmdb, title.mediaType())) - return result is MutationResult.Success || observedHidden(profile, title) == false - } - - private suspend fun observedHidden(profile: String, title: DiscoverTitle): Boolean? { - return when (val result = client.read(TvRoutes.dismissals(profile), org.justinvisser.wolfskin.conductarr.ReadTrigger.FORCE).last()) { - is ReadResult.Value -> result.saved.value?.dismissals?.any { - it.tmdbId == title.tmdb && it.mediaType.name == title.mediaType().name - } - is ReadResult.Failure -> null - } - } - override suspend fun refresh(): DiscoverSnapshot = refreshSnapshot() -} - -private fun DiscoverTitle.mediaType(): MediaType = if (type == "movie") MediaType.MOVIE else MediaType.TV -private fun RequestReceipt.outcome(): DiscoverRequestResult = DiscoverRequestResult( - phase = if (state == RequestReceipt.State.PENDING) RequestPhase.Pending else if (state == RequestReceipt.State.RETIRED) RequestPhase.Refused else RequestPhase.Receipt, - actor = actor.name, - requester = requester, -) diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/DiscoverHost.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/DiscoverHost.kt deleted file mode 100644 index b107bd97..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/DiscoverHost.kt +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import com.github.damontecres.wholphin.BuildConfig -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.justinvisser.wolfskin.design.DiscoverFixture -import org.justinvisser.wolfskin.design.DiscoverOperations -import org.justinvisser.wolfskin.design.DiscoverRequest -import org.justinvisser.wolfskin.design.DiscoverRequestResult -import org.justinvisser.wolfskin.design.DiscoverSnapshot -import org.justinvisser.wolfskin.design.DiscoverState -import org.justinvisser.wolfskin.design.DiscoverTitle -import org.justinvisser.wolfskin.design.DiscoverStatus -import org.justinvisser.wolfskin.design.RequestPhase -import org.justinvisser.wolfskin.design.ShellState - -internal val LocalDiscoverState = compositionLocalOf { error("Discover requires the Wolfskin shell") } - -/** No client or endpoint is constructed before TV sign-in exists; release stays explicitly signed out. */ -@Composable -internal fun rememberDiscoverState( - shell: ShellState, - push: (String) -> Unit, - pop: () -> Unit, - loadFixture: suspend (Context) -> DiscoverFixture = { context -> withContext(Dispatchers.IO) { readDiscoverFixture(context) } }, -): DiscoverState { - val context = LocalContext.current - val scope = rememberCoroutineScope() - var state by remember(shell, shell.person.key) { mutableStateOf(DiscoverState(shell, DiscoverSnapshot(status = if (BuildConfig.DEBUG) DiscoverStatus.Loading else DiscoverStatus.NeedsSignIn), SignedOutDiscoverOperations, scope, push, pop)) } - LaunchedEffect(shell, shell.person.key) { - if (BuildConfig.DEBUG) { - val fixture = loadFixture(context) - val snapshot = fixture.snapshot(shell.person.key) - val resumeContent = shell.route == "discover" && (!shell.navFocused || shell.navIndex == shell.currentTab) - val loaded = DiscoverState(shell, snapshot, DebugDiscoverOperations(snapshot, shell.person.name), scope, push, pop, fixture::service) - state = loaded - if (resumeContent) loaded.focus(if (loaded.hasHero || !loaded.canRequest) 0 else 1) - } - } - LaunchedEffect(shell.route, state) { state.enterRoute(shell.route) } - return state -} - -private fun readDiscoverFixture(context: Context): DiscoverFixture { - val json = context.assets.open("wolfskin-discover/fixture.json").bufferedReader().use { it.readText() } - return DiscoverFixture(json) { "file:///android_asset/wolfskin-discover/$it" } -} - -/** Fixture writes are in-memory only and cannot reach a Conductarr client. */ -private class DebugDiscoverOperations(private val snapshot: DiscoverSnapshot, private val actor: String) : DiscoverOperations { - override suspend fun request(intent: DiscoverRequest) = DiscoverRequestResult(RequestPhase.Receipt, actor) - override suspend fun check(intent: DiscoverRequest) = DiscoverRequestResult(RequestPhase.NotConfirmed) - override suspend fun hide(profile: String, title: DiscoverTitle) = true - override suspend fun restore(profile: String, title: DiscoverTitle) = true - override suspend fun refresh() = snapshot -} - -private object SignedOutDiscoverOperations : DiscoverOperations { - override suspend fun request(intent: DiscoverRequest) = DiscoverRequestResult(RequestPhase.Refused) - override suspend fun check(intent: DiscoverRequest) = DiscoverRequestResult(RequestPhase.NotConfirmed) - override suspend fun hide(profile: String, title: DiscoverTitle) = false - override suspend fun restore(profile: String, title: DiscoverTitle) = false - override suspend fun refresh() = DiscoverSnapshot() -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/HomeDestination.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/HomeDestination.kt deleted file mode 100644 index e2ac7a91..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/HomeDestination.kt +++ /dev/null @@ -1,144 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import javax.inject.Inject -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.justinvisser.wolfskin.design.* -import org.justinvisser.wolfskin.tv.read.HomeLibraryCache -import org.justinvisser.wolfskin.tv.read.HomeReadFacade - -/** The Home entry owns its source reads and upstream navigation, never the design module. */ -@HiltViewModel -class WolfskinHomeViewModel @Inject constructor( - @param:ApplicationContext private val context: Context, - private val home: HomeReadFacade, - private val cache: HomeLibraryCache, - private val navigation: NavigationManager, - private val seeAll: WolfskinSeeAllLists, -) : ViewModel() { - var snapshot by mutableStateOf(HomeSnapshot(buildHomeRows(HomeLibrary(), emptyList(), loading = true), HomeAvailability.Loading)) - private set - var toast by mutableStateOf(null) - private set - private var sourceScope: String? = null - private var loadRevision = 0L - - suspend fun load(server: JellyfinServer, user: JellyfinUser, person: String, retry: Boolean = false) { - withContext(Dispatchers.IO) { debugHomeSnapshot(context, person) }?.let { snapshot = it; return } - loadLibrary(server, user, person, retry) - } - - /** The authenticated read path is shared by release and offline fixture-driven tests. */ - internal suspend fun loadLibrary(server: JellyfinServer, user: JellyfinUser, person: String, retry: Boolean = false) { - val revision = ++loadRevision - val continuation = listOf(homeDiscoverState(HomeAvailability.SignedOut)) - if (person != user.name.orEmpty().lowercase()) { - snapshot = HomeSnapshot(buildHomeRows(HomeLibrary(), continuation, libraryAvailable = false), jellyfinAvailable = false) - return - } - val scope = "${server.id}:${user.id}" - if (sourceScope != scope) { - sourceScope = scope - toast = null - snapshot = HomeSnapshot(buildHomeRows(HomeLibrary(), emptyList(), loading = true), HomeAvailability.Loading) - } - val saved = withContext(Dispatchers.IO) { try { cache.read(scope) } catch (_: java.io.IOException) { null } } - if (revision != loadRevision) return - if (saved != null) snapshot = HomeSnapshot(buildHomeRows(saved.first, continuation)) - try { - val library = withContext(Dispatchers.IO) { home.read(user.id) } - val savedAt = System.currentTimeMillis() - withContext(Dispatchers.IO) { try { cache.write(scope, library, savedAt) } catch (_: java.io.IOException) { /* Local storage failure must not turn a reachable library into an outage. */ } } - if (revision != loadRevision) return - snapshot = HomeSnapshot(buildHomeRows(library, continuation)) - } catch (exception: Exception) { - if (exception is CancellationException) throw exception - if (revision != loadRevision) return - snapshot = if (saved != null) HomeSnapshot(buildHomeRows(saved.first, continuation), jellyfinAvailable = false, savedAt = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(saved.second))) - else HomeSnapshot(buildHomeRows(HomeLibrary(), continuation, libraryAvailable = false), jellyfinAvailable = false) - if (retry) showToast(RecoveryMessage.StillUnreachable) - } - } - - fun act(action: HomeAction, shell: ShellState, server: JellyfinServer, user: JellyfinUser) { - when (action) { - HomeAction.Discover -> openRoot(shell, "discover") - HomeAction.Retry -> viewModelScope.launch { load(server, user, shell.person.key, retry = true) } - is HomeAction.SeeAll -> if (action.row.target in shell.tabs) openRoot(shell, action.row.target!!) else seeAll.open(shell.person.key, action.row.id, action.row.title, action.row.allCards.map { it.libraryTitle() }, navigation) - is HomeAction.Details -> navigation.navigateTo(Destination.Wolfskin(titleRoute(action.title.id))) - is HomeAction.Play -> viewModelScope.launch { - if (!snapshot.jellyfinAvailable || shell.person.key != user.name.orEmpty().lowercase()) { - showToast(RecoveryMessage.PlaybackUnavailable) - return@launch - } - try { - val id = withContext(Dispatchers.IO) { home.playableId(user.id, action.title) } - if (id == null) showToast(RecoveryMessage.PlaybackUnavailable) - else navigation.navigateTo(Destination.Playback(id, if (action.resume) (action.title.resumeSeconds * 1000).toLong() else 0L)) - } catch (exception: Exception) { - if (exception is CancellationException) throw exception - showToast(RecoveryMessage.PlaybackUnavailable) - } - } - } - } - - private fun openRoot(shell: ShellState, route: String) { - shell.enterDestination(route) - shell.focusNav(shell.tabs.indexOf(route)) - shell.key(ShellKey.Ok) - } - - private fun showToast(message: String) { - toast = message - viewModelScope.launch { delay(WolfskinTokens.Motion.toast.toLong()); if (toast == message) toast = null } - } -} - -/** Complete catalogue projections feed the shared See all grid without another source read. */ -internal fun HomeTitle.libraryTitle(): LibraryTitle = LibraryTitle( - id = id, title = title, type = type, year = year, runtimeSeconds = runtimeSeconds?.toLong(), seasons = seasons, - poster = poster, played = played, progress = runtimeSeconds?.let { if (it > 0) (resumeSeconds / it).toFloat() else 0f } ?: 0f, - addedAt = addedAt, tmdbId = tmdbId?.toString(), - availability = if (held) "In your library" else otherOwner?.let { "In $itโ€™s library" }.orEmpty(), - badge = if (held) "held" else null, - ratings = ratings.map { rating -> - when (rating.mark) { - "fresh" -> LibraryRating("rtCritics", rating.score, "Fresh") - "rotten" -> LibraryRating("rtCritics", rating.score, "Rotten") - "tomato" -> LibraryRating("rtCritics", rating.score) - "upright" -> LibraryRating("rtAudience", rating.score, "Upright") - "spilled" -> LibraryRating("rtAudience", rating.score, "Spilled") - else -> LibraryRating(rating.mark, rating.score) - } - }, -) - -/** The shell keeps its focus memory while playback or later-slice screens are pushed above Home. */ -@Composable -fun HomeDestination(shell: ShellState, server: JellyfinServer, user: JellyfinUser, modifier: Modifier = Modifier, model: WolfskinHomeViewModel = hiltViewModel()) { - LaunchedEffect(shell.person.key, server.id, user.id) { model.load(server, user, shell.person.key) } - Box(modifier) { - HomeScreen(shell, model.snapshot, Modifier.fillMaxSize()) { model.act(it, shell, server, user) } - model.toast?.let { RecoveryToast(it) } - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/RequestsRefresh.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/RequestsRefresh.kt deleted file mode 100644 index 2c88cc9b..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/RequestsRefresh.kt +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.WolfskinTokens - -/** Foreground refreshes once; only an open sheet or an unsettled request keeps polling. */ -@Composable -internal fun RequestsRefresh(state: ShellState, refresh: () -> Unit) { - val owner = LocalLifecycleOwner.current - LaunchedEffect(state, state.person.key, owner) { - owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - refreshRequestsWhileStarted(state, refresh) - } - } -} - -/** Cancellation on leaving STARTED stops polling; reentry performs a fresh foreground read. */ -internal suspend fun refreshRequestsWhileStarted(state: ShellState, refresh: () -> Unit) { - refresh() - while (currentCoroutineContext().isActive) { - delay(WolfskinTokens.Behaviour.requestsPollSeconds.toLong() * 1000L) - if (state.requestsVisible || state.requests.snapshot.openCount > 0) refresh() - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellPeople.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellPeople.kt deleted file mode 100644 index 2b7562df..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellPeople.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import com.github.damontecres.wholphin.BuildConfig -import com.github.damontecres.wholphin.data.model.JellyfinUser -import org.json.JSONObject -import org.justinvisser.wolfskin.design.ShellPerson - -/** Prototype identities are packaged exclusively in debug assets, never release resources. */ -fun readShellPeople(context: Context, saved: List): List { - if (BuildConfig.DEBUG) { - val json = context.assets.open("wolfskin-shell.json").bufferedReader().use { JSONObject(it.readText()) } - val profiles = json.getJSONArray("profiles") - return List(profiles.length()) { index -> - val person = profiles.getJSONObject(index) - ShellPerson(person.getString("key"), person.getString("name"), person.getString("initial"), person.getJSONObject("destinations").has("Anime")) - } - } - return saved.map { user -> - val name = user.name.orEmpty() - ShellPerson(name.lowercase(), name, name.take(1).uppercase(), false) - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellRequests.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellRequests.kt deleted file mode 100644 index a5b32953..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellRequests.kt +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv - -import android.content.Context -import com.github.damontecres.wholphin.BuildConfig -import org.justinvisser.wolfskin.design.ComingInSnapshot -import org.justinvisser.wolfskin.design.ComingInStatus -import org.justinvisser.wolfskin.design.readComingInFixture - -/** Release stays unavailable until Conductarr sign-in supplies an authenticated generated client. */ -fun readShellRequests(context: Context): ComingInSnapshot { - if (!BuildConfig.DEBUG) return ComingInSnapshot() - val json = context.assets.open("wolfskin-requests.json").bufferedReader().use { it.readText() } - val items = readComingInFixture(json) { name -> - context.assets.open("wolfskin-requests/$name").use { it.readBytes() } - } - return ComingInSnapshot(items.take(6), ComingInStatus.Ready, signedIn = true) -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellRouting.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellRouting.kt deleted file mode 100644 index dffa611b..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/ShellRouting.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import com.github.damontecres.wholphin.ui.nav.Destination -import org.jellyfin.sdk.model.api.BaseItemKind - -/** The rollout switch changes only intercepted screens; unrelated destinations always pass through. */ -fun resolveUpstreamDestination(destination: Destination, wolfskinEnabled: Boolean, libraries: Map): Destination? = - when (destination) { - is Destination.Home -> if (wolfskinEnabled) null else destination - is Destination.MediaItem -> if (wolfskinEnabled && destination.type in TITLE_KINDS) null else destination - is Destination.Wolfskin -> if (!wolfskinEnabled && destination.route in LIBRARY_ROUTES) libraries[destination.route] else null - else -> destination - } - -private val LIBRARY_ROUTES = setOf("movies", "shows", "anime") -private val TITLE_KINDS = setOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE, BaseItemKind.PERSON) diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleDestination.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleDestination.kt deleted file mode 100644 index 913f51e7..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleDestination.kt +++ /dev/null @@ -1,250 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import android.net.Uri -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.BuildConfig -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.justinvisser.wolfskin.design.PersonState -import org.justinvisser.wolfskin.design.PersonSurface -import org.justinvisser.wolfskin.design.RecoveryMessage -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.TitleAction -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitleItem -import org.justinvisser.wolfskin.design.TitlePageData -import org.justinvisser.wolfskin.design.TitleReadStatus -import org.justinvisser.wolfskin.design.TitleState -import org.justinvisser.wolfskin.design.TitleSurface -import org.justinvisser.wolfskin.tv.read.TitleReadFacade -import java.util.UUID -import javax.inject.Inject - -/** Title and performer routes remain within the single existing Wolfskin destination subtype. */ -fun titleRoute(id: String): String = "title/${Uri.encode(id)}" -fun personRoute(id: String): String = "person/${Uri.encode(id)}" - -/** Each navigation entry owns its title focus and selections across nested person/title pages. */ -@HiltViewModel -class WolfskinTitleViewModel @Inject constructor( - @param:ApplicationContext private val context: Context, - private val facade: TitleReadFacade, - private val fixtureProvider: TitleFixtureProvider, - private val navigation: NavigationManager, -) : ViewModel() { - var title by mutableStateOf(null) - private set - var person by mutableStateOf(null) - private set - private var route = "" - private lateinit var viewer: ShellPerson - private lateinit var preferences: UserPreferences - private var fixture: TitleFixture? = null - private var personReadPending = false - private var personReadFinished = false - private var loadJob: Job? = null - private val saveMutex = Mutex() - private val fixtureSelections by lazy { context.getSharedPreferences("wolfskin-title-fixture", Context.MODE_PRIVATE) } - - /** Fixture reads are exclusively debug assets; real ids always use the authenticated Jellyfin facade. */ - fun load(destination: String, selected: ShellPerson, userPreferences: UserPreferences) { - if (route == destination && ::viewer.isInitialized && viewer.key == selected.key) return - route = destination - loadJob?.cancel() - person = null - fixture = null - personReadPending = false - personReadFinished = false - viewer = selected - preferences = userPreferences - val id = Uri.decode(destination.substringAfter('/')) - title = TitleState(TitlePageData(TitleItem(id, "", if (destination.startsWith("person/")) "person" else "movie"), status = TitleReadStatus.Loading), viewer, ::handle) - loadJob = viewModelScope.launch { - if (BuildConfig.DEBUG && id.toUuidOrNull() == null) { - val source = fixtureProvider.read() - fixture = source - if (destination.startsWith("person/")) { - val (actor, credits) = withContext(Dispatchers.IO) { source.person(id, selected.key) } - person = PersonState(actor, credits, ::handle) - } else { - val data = withContext(Dispatchers.IO) { source.title(id, selected.key) } - val key = "${viewer.key}:${data.item.id}" - val watchKey = "${viewer.key}:${data.watchTarget.id}" - title = TitleState(data.copy( - selectedVersion = fixtureSelections.getString("$key:version", null), - selectedAudio = if (fixtureSelections.contains("$key:audio")) fixtureSelections.getInt("$key:audio", -1) else null, - selectedSubtitle = fixtureSelections.getInt("$key:subtitle", -1), - watchTarget = data.watchTarget.copy(watched = fixtureSelections.getBoolean("$watchKey:watched", data.watchTarget.watched)), - hidden = fixtureSelections.getBoolean("$key:hidden", data.hidden), - ), viewer, ::handle) - } - } else if (destination.startsWith("person/")) readPerson(id) else readTitle(id) - } - } - - private suspend fun readTitle(id: String, retry: Boolean = false) { - val uuid = id.toUuidOrNull() ?: return - val current = title ?: return - try { - if (current.data.status != TitleReadStatus.Ready) facade.readSaved(uuid, preferences)?.let { current.update(it) } - val data = facade.read(uuid, current.takeIf { it.data.status == TitleReadStatus.Ready }?.season, preferences) - current.update(data) - } catch (exception: Exception) { - if (exception is CancellationException) throw exception - if (current.data.status == TitleReadStatus.Ready) { - if (current.data.episodesStatus == TitleReadStatus.Loading) current.restoreSeason() - } else current.update(current.data.copy(status = TitleReadStatus.Unavailable)) - if (current.data.status == TitleReadStatus.Ready || retry) current.toast = RecoveryMessage.StillUnreachable - } - } - - private suspend fun readPerson(id: String, retry: Boolean = false) { - if (personReadPending || personReadFinished) return - val uuid = id.toUuidOrNull() ?: return - val owner = viewer.key - personReadPending = true - try { - if (person == null) facade.readPersonSaved(uuid)?.let { saved -> - if (owner == viewer.key) person = newPerson(saved.person, saved.credits).apply { savedAt = saved.savedAt } - } - val start = if (person?.savedAt != null) 0 else person?.credits?.size ?: 0 - val result = facade.readPerson(uuid, start) - if (owner != viewer.key) return - val current = person - val credits = if (start == 0) result.credits else (current?.credits.orEmpty() + result.credits).distinctBy { it.id } - if (current == null) person = newPerson(result.person, credits) else current.update(result.person, credits) - personReadFinished = result.credits.size < 200 - } catch (exception: Exception) { - if (exception is CancellationException) throw exception - if (owner == viewer.key) { - person?.toast = RecoveryMessage.StillUnreachable - if (person == null) { - title?.update(title!!.data.copy(status = TitleReadStatus.Unavailable)) - if (retry) title?.toast = RecoveryMessage.StillUnreachable - } - } - } finally { if (owner == viewer.key) personReadPending = false } - } - - private fun newPerson(actor: org.justinvisser.wolfskin.design.TitlePerson, credits: List) = - PersonState(actor, credits, ::handle) { viewModelScope.launch { readPerson(actor.id) } } - - private fun handle(action: TitleAction) { - when (action) { - TitleAction.Pop -> navigation.goBack() - is TitleAction.Person -> navigation.navigateTo(action.person.id.toUuidOrNull()?.let { Destination.MediaItem(it, org.jellyfin.sdk.model.api.BaseItemKind.PERSON) } ?: Destination.Wolfskin(personRoute(action.person.id))) - is TitleAction.OpenTitle -> navigation.navigateTo(action.item.id.toUuidOrNull()?.let { Destination.MediaItem(it, when (action.item.kind) { - "movie" -> org.jellyfin.sdk.model.api.BaseItemKind.MOVIE - "episode" -> org.jellyfin.sdk.model.api.BaseItemKind.EPISODE - else -> org.jellyfin.sdk.model.api.BaseItemKind.SERIES - }) } ?: Destination.Wolfskin(titleRoute(action.item.id))) - is TitleAction.Play -> play(action.item.id.toUuidOrNull(), if (action.restart) 0 else action.item.resumeSeconds * 1000) - is TitleAction.Trailer -> if (action.url.startsWith("jellyfin:")) play(action.url.removePrefix("jellyfin:").toUuidOrNull(), 0) - else if (!TitleTrailer(context).open(action.url)) title?.toast = "No YouTube app is available to play this trailer." - is TitleAction.Version, is TitleAction.Track, is TitleAction.Watched -> savePlayback(action) - is TitleAction.Season -> { - loadJob?.cancel() - loadJob = viewModelScope.launch { - val current = title ?: return@launch - current.showSeasonLoading() - val source = fixture - if (source == null) readTitle(Uri.decode(route.substringAfter('/'))) - else { - val data = withContext(Dispatchers.IO) { source.title(Uri.decode(route.substringAfter('/')), viewer.key, action.number) } - current.update(data.copy(selectedVersion = current.versionId, selectedAudio = current.audioIndex, - selectedSubtitle = current.subtitleIndex, watchTarget = current.data.watchTarget.copy(watched = current.watched), hidden = current.data.hidden)) - } - } - } - TitleAction.Retry -> viewModelScope.launch { - if (fixture == null) { - if (route.startsWith("person/")) readPerson(Uri.decode(route.substringAfter('/')), retry = true) else readTitle(Uri.decode(route.substringAfter('/')), retry = true) - } - else title?.toast = RecoveryMessage.StillUnreachable - } - is TitleAction.Request, is TitleAction.Requests -> title?.toast = "Requests need this TV to be signed in to Conductarr." - is TitleAction.Hide -> if (fixture != null) fixtureSelections.edit().putBoolean("${viewer.key}:${action.item.id}:hidden", action.hidden).apply() - } - } - - private fun play(id: UUID?, position: Long) { - viewModelScope.launch { - try { - if (id == null) throw IllegalArgumentException("No Jellyfin playback identity") - facade.preparePlayback(id) - navigation.navigateTo(Destination.Playback(id, position)) - } catch (exception: Exception) { - if (exception is CancellationException) throw exception - title?.toast = RecoveryMessage.PlaybackUnavailable - } - } - } - - private fun savePlayback(action: TitleAction) { - val current = title ?: return - if (fixture != null) { - val edits = fixtureSelections.edit() - when (action) { - is TitleAction.Version -> edits.putString("${viewer.key}:${action.item.id}:version", action.version.id) - is TitleAction.Track -> { - action.audio?.let { edits.putInt("${viewer.key}:${action.item.id}:audio", it) } - edits.putInt("${viewer.key}:${action.item.id}:subtitle", action.subtitle) - } - is TitleAction.Watched -> edits.putBoolean("${viewer.key}:${action.item.id}:watched", action.watched) - else -> Unit - } - edits.apply() - current.confirmPlayback(action) - return - } - viewModelScope.launch { - saveMutex.withLock { - try { facade.apply(action); current.confirmPlayback(action) } - catch (exception: Exception) { - if (exception is CancellationException) throw exception - current.rejectPlayback(action) - current.toast = "Couldnโ€™t save that change. Try again." - } - } - } - } -} - -/** The screen's own Back handler closes its sheet before delegating a navigation pop. */ -@Composable -fun TitleDestination(route: String, shell: ShellState, preferences: UserPreferences, modifier: Modifier = Modifier, viewModel: WolfskinTitleViewModel = hiltViewModel()) { - LaunchedEffect(route, shell.person.key) { viewModel.load(route, shell.person, preferences) } - val title = viewModel.title?.takeIf { it.person.key == shell.person.key } - LaunchedEffect(title?.data?.status) { - if (title?.data?.status == TitleReadStatus.Ready) shell.navFocused = false - } - val person = viewModel.person.takeIf { title != null } - BackHandler(enabled = title != null) { - title?.back(shell) - } - if (person != null && route.startsWith("person/")) PersonSurface(person, shell, modifier) - else if (title != null) TitleSurface(title, shell, modifier) -} - -private fun String.toUuidOrNull(): UUID? = try { UUID.fromString(this) } catch (_: IllegalArgumentException) { null } diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleFixture.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleFixture.kt deleted file mode 100644 index 3e9709aa..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleFixture.kt +++ /dev/null @@ -1,144 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import com.github.damontecres.wholphin.BuildConfig -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton -import org.json.JSONArray -import org.json.JSONObject -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitleItem -import org.justinvisser.wolfskin.design.TitlePageData -import org.justinvisser.wolfskin.design.TitlePerson -import org.justinvisser.wolfskin.design.LibraryRating -import org.justinvisser.wolfskin.design.TitleTrack -import org.justinvisser.wolfskin.design.TitleVersion -import java.util.Locale - -/** The locked prototype is projected only by debug callers and offline JVM captures. */ -class TitleFixture(private val fixture: JSONObject) { - private val profiles = fixture.getJSONArray("profiles").objects() - private val cast = fixture.getJSONObject("cast") - private val availableArt = fixture.getJSONArray("availableArt").let { array -> (0 until array.length()).map { array.getString(it) }.toSet() } - val people: List = profiles.map { ShellPerson(it.getString("key"), it.getString("name"), it.getString("initial"), it.getJSONObject("destinations").has("Anime")) } - private val all = profiles.flatMap { profile -> listOf("movies", "shows", "anime", "discover").flatMap { profile.getJSONArray(it).objects() } }.distinctBy { key(it) } - private val pool = profiles.flatMap { it.getJSONArray("discover").objects() }.distinctBy { key(it) } - - /** Each viewer gets their own ownership, resume target and episode watch identity. */ - fun title(id: String, person: String, season: Int? = null): TitlePageData { - val profile = profiles.first { it.getString("key") == person } - val library = listOf("movies", "shows", "anime").flatMap { profile.getJSONArray(it).objects() } - val episodes = listOf("resume", "nextUp").flatMap { profile.getJSONArray(it).objects() } - val source = (all + episodes).first { it.getString("id") == id || key(it) == id || it.getString("title") == id } - val selectedEpisode = source.takeIf { it.getString("type") == "episode" } - val container = if (selectedEpisode != null) library.first { it.getString("title") == selectedEpisode.getString("seriesTitle") } else source - val own = library.firstOrNull { key(it) == key(container) } - val item = item(own ?: container, person) - val play = selectedEpisode ?: episodes.firstOrNull { it.optString("seriesTitle") == container.getString("title") } - ?: own?.takeIf { it.optDouble("progress") > 0 } - val target = play?.let { item(it, person) } ?: item.takeIf { it.held && it.kind == "movie" } - val selectedSeason = season ?: play?.optInt("seasonNumber")?.takeIf { it > 0 } ?: 1 - val related = (pool + library).filter { key(it) != key(container) && overlap(it, container) > 0 } - .sortedByDescending { overlap(it, container) }.distinctBy { key(it) }.take(16).map { item(it, person) } - val castMembers = cast.optJSONArray(key(container))?.objects().orEmpty().map { member -> - TitlePerson(member.getString("name"), member.getString("name"), member.optString("character"), art(member.optString("img").takeIf { it.isNotEmpty() }?.let { "assets/cast/$it" })) - } - return TitlePageData(item, play?.let { item(it, person) }, selectedEpisode?.let { item(it, person) } ?: item, - versions = target?.let { versions(it) }.orEmpty(), - trailerUrl = if (item.held && key(container) in TRAILERS) "fixture:trailer" else null, - cast = castMembers, related = related, episodes = episodes.filter { it.optString("seriesTitle") == container.getString("title") && it.optInt("seasonNumber") == selectedSeason }.map { item(it, person) }, - connection = TitleConnection.SignedIn, pick = container.optString("source") == "materialized_personal_recommendation", selectedSeason = selectedSeason) - } - - /** Only real household credits are retained; prototype filler is never promoted to filmography. */ - fun person(name: String, viewer: String): Pair> { - val credits = all.mapNotNull { title -> - val actor = cast.optJSONArray(key(title))?.objects()?.firstOrNull { it.getString("name") == name } ?: return@mapNotNull null - item(title, viewer).copy(character = actor.optString("character")) - }.sortedWith(compareByDescending { it.year ?: 0 }.thenBy { it.name.replace(Regex("^(The|A|An) "), "") }) - val actor = cast.keys().asSequence().flatMap { cast.getJSONArray(it).objects() }.first { it.getString("name") == name } - return TitlePerson(name, name, portrait = art(actor.optString("img").takeIf { it.isNotEmpty() }?.let { "assets/cast/$it" })) to credits - } - - private fun item(source: JSONObject, person: String): TitleItem { - val owners = profiles.filter { profile -> listOf("movies", "shows", "anime").any { bucket -> profile.getJSONArray(bucket).objects().any { key(it) == key(source) } } } - val held = source.optString("type") == "episode" || owners.any { it.getString("key") == person } - return TitleItem( - source.getString("id"), source.getString("title"), source.getString("type"), source.optInt("year").takeIf { it > 0 }, - source.optLong("runtimeSeconds"), source.optInt("seasonCount"), source.optString("overview"), - source.optJSONArray("genres")?.let { values -> (0 until values.length()).map { values.getString(it) } }.orEmpty(), - art(source.optString("poster")), art(source.optString("backdrop")), held, source.optBoolean("played"), source.optDouble("resumeSeconds").toLong(), - seriesName = source.optString("seriesTitle").takeIf { it.isNotEmpty() }, season = source.optInt("seasonNumber"), episode = source.optInt("episodeNumber"), - ratings = ratings(source), otherOwners = owners.filter { it.getString("key") != person }.map { it.getString("name") }, - isPosterBackdrop = source.optJSONObject("artwork")?.optString("backdropKind") == "poster_crop", - ) - } - - private fun art(path: String?): String? = path?.takeIf { it in availableArt }?.let { "file:///android_asset/wolfskin-titles/$it" } - - private fun ratings(item: JSONObject): List { - val episode = item.optString("type") == "episode" - if (episode) { - val series = all.firstOrNull { it.optString("title") == item.optString("seriesTitle") } - val base = series?.let { ratings(it) }.orEmpty() - val score = item.optDouble("communityRating", Double.NaN) - return if (score in 0.0..10.0) base.filter { it.source != "tmdb" } + LibraryRating("tmdb", score, episode = true) else base - } - val ratings = item.optJSONObject("ratings") ?: JSONObject().apply { - if (!item.isNull("criticRating")) put("rtCritics", item.getDouble("criticRating")) - if (!item.isNull("communityRating")) put("tmdb", item.getDouble("communityRating")) - } - return buildList { - if (!ratings.isNull("rtCritics")) add(LibraryRating("rtCritics", ratings.getDouble("rtCritics"), ratings.optString("rtCriticsLabel").takeIf { it.isNotEmpty() })) - if (!ratings.isNull("rtAudience")) add(LibraryRating("rtAudience", ratings.getDouble("rtAudience"), ratings.optString("rtAudienceLabel").takeIf { it.isNotEmpty() })) - if (item.optString("type") == "movie" && (!ratings.has("imdbVotes") || ratings.optInt("imdbVotes") > 0) && !ratings.isNull("imdb")) add(LibraryRating("imdb", ratings.getDouble("imdb"))) - if (isEmpty() && (!ratings.has("tmdbVotes") || ratings.optInt("tmdbVotes") > 0) && !ratings.isNull("tmdb")) add(LibraryRating("tmdb", ratings.getDouble("tmdb"))) - }.filter { it.score in 0.0..if (it.source.startsWith("rt")) 100.0 else 10.0 } - } - - private fun versions(item: TitleItem): List { - val subtitles = listOf(TitleTrack(2, "English"), TitleTrack(3, "English SDH"), TitleTrack(4, "Dutch")) - fun version(id: String, name: String, video: String, bitrate: Double, container: String, audio: List) = - TitleVersion(id, name, video, "$bitrate Mbps", String.format(Locale.US, "%.1f GB", bitrate * item.runtimeSeconds / 8000), container, audio, subtitles) - val standardAudio = listOf(TitleTrack(0, "English ยท AC3 5.1", true), TitleTrack(1, "English ยท AAC 2.0")) - return if (item.name == "Locke") listOf( - version("v1", "2160p ยท HDR10 ยท Remux", "2160p HEVC Main 10 ยท HDR10", 58.4, "MKV", listOf(TitleTrack(0, "English ยท TrueHD 7.1", true), TitleTrack(1, "English ยท AC3 5.1"))), - version("v2", "1080p ยท SDR", "1080p H.264 High ยท SDR", 9.8, "MP4", standardAudio), - ) else listOf(version("v1", "1080p ยท SDR", "1080p H.264 High ยท SDR", 8.2, "MKV", standardAudio)) - } - - private fun key(item: JSONObject): String = "${item.getString("type")}:${item.optLong("tmdbId")}" - private fun overlap(first: JSONObject, second: JSONObject): Int { - fun genres(item: JSONObject): Set = item.optJSONArray("genres")?.let { array -> (0 until array.length()).flatMap { - when (val genre = array.getString(it)) { - "Sci-Fi & Fantasy" -> listOf("Science Fiction", "Fantasy") - "Action & Adventure" -> listOf("Action", "Adventure") - "Sci-Fi" -> listOf("Science Fiction") - else -> listOf(genre) - } - }.toSet() }.orEmpty() - return genres(first).intersect(genres(second)).size - } - - companion object { - private val TRAILERS = setOf("movie:14069", "movie:210479", "movie:318917", "movie:42994", "movie:123431", "tv:83867", "tv:76479", "tv:1215", "tv:1437", "tv:67070", "movie:1930", "movie:21469", "movie:76203", "tv:60948") - } -} - -/** Every title entry shares one parsed, read-only debug fixture; release callers cannot load it. */ -@Singleton -class TitleFixtureProvider @Inject constructor(@param:ApplicationContext private val context: Context) { - private val fixture by lazy { - TitleFixture(context.assets.open("wolfskin-titles.json").bufferedReader().use { JSONObject(it.readText()) }) - } - suspend fun read(): TitleFixture = withContext(Dispatchers.IO) { - check(BuildConfig.DEBUG) { "Title fixtures are debug-only" } - fixture - } -} - -private fun JSONArray.objects(): List = List(length()) { getJSONObject(it) } diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleTrailer.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleTrailer.kt deleted file mode 100644 index 0dce1f9f..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/TitleTrailer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri - -/** Only an installed app that handles the YouTube scheme may receive a remote trailer. */ -class TitleTrailer(private val context: Context) { - /** A missing handler leaves the title in place rather than falling back to a browser. */ - fun open(url: String): Boolean { - val source = Uri.parse(url) - val video = when (source.host?.lowercase()) { - "youtu.be" -> source.pathSegments.firstOrNull() - "youtube.com", "www.youtube.com", "m.youtube.com" -> source.getQueryParameter("v") - ?: source.pathSegments.takeIf { it.firstOrNull() in setOf("shorts", "embed") }?.getOrNull(1) - else -> null - } ?: return false - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:$video")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val handler = context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) - .firstOrNull { it.activityInfo.exported } ?: return false - intent.setPackage(handler.activityInfo.packageName) - return try { - context.startActivity(intent) - true - } catch (_: ActivityNotFoundException) { - false - } catch (_: SecurityException) { - false - } - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt deleted file mode 100644 index 63e29f9b..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt +++ /dev/null @@ -1,251 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.ui.NavDisplay -import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.BackdropResult -import com.github.damontecres.wholphin.services.BackdropService -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.services.SetupDestination -import com.github.damontecres.wholphin.services.SetupNavigationManager -import com.github.damontecres.wholphin.ui.nav.Backdrop -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.DestinationContent -import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.serializer.toUUIDOrNull -import kotlinx.coroutines.withContext -import org.justinvisser.wolfskin.design.PeopleStatus -import org.justinvisser.wolfskin.design.SettingsList -import org.justinvisser.wolfskin.design.ShellAction -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellPlaceholder -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.ShellSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.justinvisser.wolfskin.tv.read.LibraryReadFacade -import org.justinvisser.wolfskin.design.DiscoverOverlay -import org.justinvisser.wolfskin.design.DiscoverScreen - -/** View memory must outlive the upstream setup entry replaced by a user switch. */ -@Singleton -class WolfskinSession @Inject constructor() { - var state by mutableStateOf(null) - var serverId: String? = null - var onAction: (ShellAction) -> Unit = {} -} - -/** The shell routes only through the existing NavigationManager and real settings pages. */ -@HiltViewModel -class WolfskinShellViewModel @Inject constructor( - @param:ApplicationContext private val context: Context, - val session: WolfskinSession, - private val repository: ServerRepository, - private val navigation: NavigationManager, - private val setupNavigation: SetupNavigationManager, - private val backdrop: BackdropService, - val libraries: LibraryReadFacade, - val seeAll: WolfskinSeeAllLists, -) : ViewModel() { - val backdropFlow: StateFlow = backdrop.backdropFlow - private val requestsSnapshot by lazy { readShellRequests(context) } - var libraryDestinations by mutableStateOf>(emptyMap()) - private set - var signedInPerson by mutableStateOf("") - private set - - suspend fun load(server: JellyfinServer, user: JellyfinUser) { - WolfskinScreenPreference.initialize(context) - signedInPerson = user.name.orEmpty().lowercase() - if (session.state == null || session.serverId != server.id.toString()) { - val saved = withContext(Dispatchers.IO) { repository.serverDao.getServer(server.id)?.users.orEmpty() } - val people = readShellPeople(context, saved.ifEmpty { listOf(user) }) - session.serverId = server.id.toString() - session.state = ShellState(people, signedInPerson) { session.onAction(it) } - } - session.onAction = { action -> handleAction(action, server, user) } - try { - libraryDestinations = libraries.readDestinations(user.id) - } catch (exception: Exception) { - if (exception is kotlinx.coroutines.CancellationException) throw exception - libraryDestinations = emptyMap() - } - } - - fun clearBackdrop() { viewModelScope.launch { backdrop.clearBackdrop() } } - - /** The debug fixture is the only Requests supplier until TV sign-in is enabled. */ - fun refreshRequests() = refreshRequests(retry = false) - - private fun refreshRequests(retry: Boolean) { - val shell = session.state ?: return - val target = shell.requests - viewModelScope.launch { - val snapshot = withContext(Dispatchers.IO) { requestsSnapshot } - target.update(snapshot) - if (retry && snapshot.status == org.justinvisser.wolfskin.design.ComingInStatus.Unavailable && shell.requests === target) { - shell.toast = org.justinvisser.wolfskin.design.RecoveryMessage.StillUnreachable - } - } - } - - private fun handleAction(action: ShellAction, server: JellyfinServer, user: JellyfinUser) { - when (action) { - is ShellAction.Destination -> if (action.route == "settings") navigation.navigateTo(Destination.Wolfskin("settings")) else navigation.replace(if (action.route == "home") Destination.Home() else Destination.Wolfskin(action.route)) - ShellAction.Pop -> navigation.goBack() - is ShellAction.HandOff -> navigation.navigateTo(handOffDestination(action.group)) - is ShellAction.Person -> selectPerson(action.person, server, user) - ShellAction.RetryPeople -> viewModelScope.launch { load(server, user) } - ShellAction.RetryRequests -> refreshRequests(retry = true) - is ShellAction.RequestTitle -> navigation.navigateTo(Destination.Wolfskin(titleRoute("${action.request.mediaType}:${action.request.tmdbId}"))) - } - } - - private fun selectPerson(person: ShellPerson, server: JellyfinServer, user: JellyfinUser) { - navigation.replace(Destination.Home()) - if (person.key == user.name.orEmpty().lowercase()) return - viewModelScope.launch { - val saved = withContext(Dispatchers.IO) { - repository.serverDao.getServer(server.id)?.users?.firstOrNull { it.name.equals(person.name, ignoreCase = true) } - } - if (saved == null || saved.isProtected) { - signedInPerson = "" - return@launch - } - try { - val current = repository.changeUser(server, saved) - setupNavigation.navigateTo(SetupDestination.AppContent(current)) - } catch (exception: Exception) { - if (exception is kotlinx.coroutines.CancellationException) throw exception - signedInPerson = "" - session.state?.apply { peopleStatus = PeopleStatus.Unavailable; pickerVisible = true } - } - } - } -} - -/** Settings groups open the fork's own pages; the prototype hand-off panel is never shipped. */ -fun handOffDestination(group: String): Destination = when (group) { - "advanced" -> Destination.Settings(PreferenceScreenOption.ADVANCED) - "profile" -> Destination.HomeSettings - else -> Destination.Settings(PreferenceScreenOption.BASIC) -} - -private val LocalShellState = compositionLocalOf { error("Wolfskin destination requires the shell") } - -/** The one upstream hook preserves destination decorators and full-screen hand-offs. */ -@Composable -fun WolfskinApp( - server: JellyfinServer, - user: JellyfinUser, - navigationManager: NavigationManager, - preferences: UserPreferences, - modifier: Modifier = Modifier, - viewModel: WolfskinShellViewModel = hiltViewModel(), -) { - LaunchedEffect(server.id, user.id) { viewModel.load(server, user) } - val loading = remember(user.id) { - ShellState(listOf(ShellPerson(user.name.orEmpty().lowercase(), user.name.orEmpty(), user.name.orEmpty().take(1), false)), user.name.orEmpty().lowercase()).apply { peopleStatus = PeopleStatus.Loading } - } - val state = viewModel.session.state ?: loading - val discover = rememberDiscoverState( - state, - { route -> navigationManager.navigateTo(Destination.Wolfskin(route)) }, - navigationManager::goBack, - ) - RequestsRefresh(state, viewModel::refreshRequests) - WolfskinTheme { - CompositionLocalProvider(LocalShellState provides state, LocalDiscoverState provides discover) { - Box(modifier) { - val current = navigationManager.backStack.last() - if (!state.pickerVisible && resolveUpstreamDestination(current, WolfskinScreenPreference.enabled, viewModel.libraryDestinations) != null) { - val backdrop by viewModel.backdropFlow.collectAsStateWithLifecycle() - Backdrop( - backdrop = backdrop, - drawerIsOpen = false, - backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle, - ) - } - NavDisplay( - backStack = navigationManager.backStack, - onBack = { if (navigationManager.backStack.last().fullScreen) navigationManager.goBack() else state.key(org.justinvisser.wolfskin.design.ShellKey.Back) }, - entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()), - entryProvider = { destination -> - NavEntry(destination, contentKey = "${destination}_${server.id}_${user.id}") { - val route = when (destination) { - is Destination.Home -> "home" - is Destination.Wolfskin -> destination.route - is Destination.MediaItem -> if (destination.type == org.jellyfin.sdk.model.api.BaseItemKind.PERSON) personRoute(destination.itemId.toString()) else titleRoute(destination.itemId.toString()) - else -> "pushed" - } - LaunchedEffect(destination) { if (!destination.fullScreen) state.enterDestination(route) } - if (destination.fullScreen) { - DestinationContent(destination, preferences, viewModel::clearBackdrop, Modifier.fillMaxSize()) - } else { - BackHandler { state.key(org.justinvisser.wolfskin.design.ShellKey.Back) } - val upstream = resolveUpstreamDestination(destination, WolfskinScreenPreference.enabled, viewModel.libraryDestinations) - if (upstream == null && (route.startsWith("title/") || route.startsWith("person/"))) { - val jellyfinIdentity = destination is Destination.MediaItem || android.net.Uri.decode(route.substringAfter('/')).toUUIDOrNull() != null - if (jellyfinIdentity && state.person.key != viewModel.signedInPerson) { - ShellSurface(state, handlesContentKeys = false) { ShellPlaceholder("Sign in as ${state.person.name} to open their Jellyfin library", it) } - } else TitleDestination(route, state, preferences) - } else { - ShellSurface(state, handlesContentKeys = upstream == null, - overlay = { if (route == "discover" || route.startsWith("discover-")) DiscoverOverlay(discover) }) { contentModifier -> - when { - (upstream != null || destination is Destination.Home || route in LIBRARY_DESTINATIONS) && state.person.key != viewModel.signedInPerson -> ShellPlaceholder("Sign in as ${state.person.name} to open their Jellyfin library", contentModifier) - upstream != null -> DestinationContent(upstream, preferences, viewModel::clearBackdrop, contentModifier) - destination is Destination.Home -> HomeDestination(state, server, user, contentModifier) - route in LIBRARY_DESTINATIONS || route.startsWith("seeall/") -> - WolfskinLibraryContent(androidx.compose.ui.platform.LocalContext.current, route, state, user, "${server.id}:${user.id}", viewModel.libraries, navigationManager, viewModel.seeAll, contentModifier) - destination is Destination.Wolfskin -> DestinationContent(destination, preferences, viewModel::clearBackdrop, contentModifier) - else -> ShellPlaceholder(route, contentModifier) - } - } - } - } - } - }, - ) - } - } - } -} - -/** The single registered destination branch delegates Wolfskin-only screens back to the shell. */ -@Composable -fun WolfskinDestinationContent(route: String, modifier: Modifier = Modifier) { - when { - route == "settings" -> SettingsList(LocalShellState.current, modifier) - route == "discover" || route.startsWith("discover-") -> DiscoverScreen(LocalDiscoverState.current, modifier) - else -> ShellPlaceholder(route, modifier) - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinLibrary.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinLibrary.kt deleted file mode 100644 index 0a9a56e5..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinLibrary.kt +++ /dev/null @@ -1,99 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import com.github.damontecres.wholphin.BuildConfig -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import java.io.File -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.justinvisser.wolfskin.design.* -import org.justinvisser.wolfskin.tv.read.LibraryReadFacade -import org.justinvisser.wolfskin.tv.read.LibrarySnapshotStore -import org.justinvisser.wolfskin.conductarr.FileCacheStore -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.serializer.toUUIDOrNull - -/** Other shelves register the complete per-type list, not the visible twenty-card window. */ -@Singleton -class WolfskinSeeAllLists @Inject constructor() { - private val lists = mutableMapOf() - - fun open(person: String, key: String, title: String, items: List, navigation: NavigationManager, loadNextPage: (suspend (Int) -> LibraryPage)? = null) { - val identity = "$person/$key" - val grid = LibraryGridState(title, items, catalogue = true, - onTitle = { navigation.navigateTo(libraryTitleDestination(it)) }) - if (loadNextPage != null) grid.pagination = LibraryPagination(grid, loadNextPage) - lists[identity] = grid - navigation.navigateTo(Destination.Wolfskin("seeall/$identity")) - } - - fun get(key: String): LibraryGridState? = lists[key] -} - -/** Debug fixture reads never become a fallback for an unavailable authenticated library. */ -@Composable -fun WolfskinLibraryContent( - context: Context, - route: String, - shell: ShellState, - user: JellyfinUser, - cacheScope: String, - facade: LibraryReadFacade, - navigation: NavigationManager, - seeAll: WolfskinSeeAllLists, - modifier: Modifier = Modifier, -) { - val view = shell.rememberedView(route) - val fixture by produceState(null, context) { - if (BuildConfig.DEBUG) value = withContext(Dispatchers.IO) { - context.assets.open("wolfskin-library.json").bufferedReader().use { LibraryFixture(it.readText(), "file:///android_asset/library/") } - } - } - val snapshots = remember(context) { LibrarySnapshotStore(FileCacheStore(File(context.filesDir, "wolfskin-library-cache"))) } - val grid = remember(shell.person.key, route, view) { - view.library ?: if (route.startsWith("seeall/")) seeAll.get(route.substringAfter("seeall/")) else LibraryGridState( - title = route.replaceFirstChar { it.uppercase() }, items = emptyList(), - onTitle = { navigation.navigateTo(libraryTitleDestination(it)) }, - onDiscover = { shell.enterDestination("discover"); navigation.replace(Destination.Wolfskin("discover")) }, - onRecover = { view.library?.let { it.refreshRevision++ } }, - ).also { it.status = LibraryStatus.Loading; view.library = it } - } - if (grid == null) { - ShellPlaceholder("See all list unavailable", modifier) - return - } - LaunchedEffect(grid, grid.refreshRevision, fixture) { - if (route in LIBRARY_DESTINATIONS && (!BuildConfig.DEBUG || fixture != null)) { - val wasLoading = grid.status == LibraryStatus.Loading - if (fixture != null) { - grid.source = withContext(Dispatchers.IO) { fixture!!.library(shell.person.key, route) } - grid.status = LibraryStatus.Ready - } else { - snapshots.read(cacheScope, route, force = grid.refreshRevision > 0) { facade.readLibrary(user.id, route) }.collect { snapshot -> - grid.source = snapshot.items - grid.status = if (snapshot.unreachable && snapshot.savedAtMillis == null) LibraryStatus.Unavailable else LibraryStatus.Ready - grid.savedAt = if (snapshot.unreachable) snapshot.savedAtMillis?.let { DateTimeFormatter.ofPattern("HH:mm").format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault())) } else null - grid.tail = if (snapshot.unreachable && snapshot.items.isNotEmpty()) GridTail.Retry else GridTail.None - if (snapshot.unreachable && grid.refreshRevision > 0) shell.toast = RecoveryMessage.StillUnreachable - } - } - if (wasLoading && shell.navFocused && shell.navIndex == shell.currentTab) shell.navFocused = false - } - } - LibraryScreen(shell, grid, modifier, ratingsSignedIn = fixture != null, view = view) -} - -val LIBRARY_DESTINATIONS = setOf("movies", "shows", "anime") - -private fun libraryTitleDestination(title: LibraryTitle): Destination = - title.id.toUUIDOrNull()?.let { Destination.MediaItem(it, if (title.type == "movie") BaseItemKind.MOVIE else BaseItemKind.SERIES) } - ?: Destination.Wolfskin(titleRoute(title.id)) diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinScreenPreference.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinScreenPreference.kt deleted file mode 100644 index b85b702f..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinScreenPreference.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.content.Context -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.content.edit -import androidx.preference.PreferenceManager - -/** The single rollout preference is exposed only in upstream Advanced settings. */ -object WolfskinScreenPreference { - private const val KEY = "wolfskin.screens.enabled" - private lateinit var preferences: SharedPreferences - private var current by mutableStateOf(true) - - fun initialize(context: Context) { - preferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) - current = preferences.getBoolean(KEY, true) - } - var enabled: Boolean - get() = current - set(value) { - current = value - preferences.edit { putBoolean(KEY, value) } - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/HomeLibraryCache.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/HomeLibraryCache.kt deleted file mode 100644 index 6e69ac54..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/HomeLibraryCache.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.justinvisser.wolfskin.tv.read - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import java.io.File -import javax.inject.Inject -import javax.inject.Singleton -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import org.justinvisser.wolfskin.conductarr.CacheEntry -import org.justinvisser.wolfskin.conductarr.FileCacheStore -import org.justinvisser.wolfskin.design.HomeLibrary -import org.justinvisser.wolfskin.design.HomeTitle - -/** Saved Jellyfin projections reuse the app-private atomic cache and are partitioned by server/user. */ -@Singleton -class HomeLibraryCache @Inject constructor(@param:ApplicationContext context: Context) { - private val store = FileCacheStore(File(context.filesDir, "wolfskin-home")) - - fun read(scope: String): Pair? { - val entry = store.read(scope, KEY) ?: return null - return try { - val json = JSONObject(entry.body) - val destinations = json.getJSONObject("destinations") - val totals = json.getJSONObject("totals") - HomeLibrary( - decode(json.getJSONArray("resume")), decode(json.getJSONArray("nextUp")), decode(json.getJSONArray("newest")), - destinations.keys().asSequence().associateWith { decode(destinations.getJSONArray(it)) }, - totals.keys().asSequence().associateWith { totals.getInt(it) }, - ) to entry.savedAtMillis - } catch (_: JSONException) { store.remove(scope, KEY); null } - } - - fun write(scope: String, library: HomeLibrary, savedAtMillis: Long) { - val json = JSONObject().put("resume", encode(library.resume)).put("nextUp", encode(library.nextUp)).put("newest", encode(library.newest)) - .put("destinations", JSONObject().apply { library.destinations.forEach { (key, titles) -> put(key, encode(titles)) } }) - .put("totals", JSONObject(library.destinationTotals)) - store.write(scope, KEY, CacheEntry(json.toString(), null, savedAtMillis, savedAtMillis, group = "home")) - } - - private fun encode(titles: List): JSONArray = JSONArray().apply { - titles.forEach { title -> put(JSONObject().apply { - put("id", title.id); put("title", title.title); put("type", title.type); put("year", title.year) - put("runtime", title.runtimeSeconds); put("seasons", title.seasons); put("overview", title.overview) - put("poster", title.poster); put("backdrop", title.backdrop); put("posterOnly", title.posterOnly) - put("seriesTitle", title.seriesTitle); put("season", title.season); put("episode", title.episode) - put("resume", title.resumeSeconds); put("played", title.played); put("addedAt", title.addedAt) - put("genres", JSONArray(title.genres)); put("tmdb", title.tmdbId) - }) } - } - - private fun decode(items: JSONArray): List = List(items.length()) { index -> - val item = items.getJSONObject(index) - fun string(key: String): String? = if (item.isNull(key)) null else item.getString(key) - fun number(key: String): Int? = if (item.isNull(key)) null else item.getInt(key) - val genres = item.getJSONArray("genres") - HomeTitle( - item.getString("id"), item.getString("title"), item.getString("type"), number("year"), number("runtime"), number("seasons"), item.getString("overview"), - string("poster"), string("backdrop"), item.getBoolean("posterOnly"), string("seriesTitle"), number("season"), number("episode"), item.getDouble("resume"), item.getBoolean("played"), item.getString("addedAt"), - List(genres.length()) { genres.getString(it) }, held = true, tmdbId = number("tmdb"), - ) - } - - companion object { private const val KEY = "home:jellyfin" } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/HomeReadFacade.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/HomeReadFacade.kt deleted file mode 100644 index 1696930a..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/HomeReadFacade.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.justinvisser.wolfskin.tv.read - -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.services.ImageUrlService -import java.util.UUID -import javax.inject.Inject -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.itemsApi -import org.jellyfin.sdk.api.client.extensions.tvShowsApi -import org.jellyfin.sdk.api.client.extensions.userViewsApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.ItemSortBy -import org.jellyfin.sdk.model.api.MediaType -import org.jellyfin.sdk.model.api.SortOrder -import org.justinvisser.wolfskin.design.HomeLibrary -import org.justinvisser.wolfskin.design.HomeTitle - -/** All Home SDK calls and image URLs stay behind this facade, with their own required field list. */ -class HomeReadFacade @Inject constructor(private val api: ApiClient, private val images: ImageUrlService) { - suspend fun read(userId: UUID): HomeLibrary { - val resume = api.itemsApi.getResumeItems(userId = userId, fields = FIELDS, mediaTypes = listOf(MediaType.VIDEO), limit = 20).content.items.map(::title) - val next = api.tvShowsApi.getNextUp(userId = userId, fields = FIELDS, limit = 20, enableUserData = true).content.items.map(::title) - val libraries = api.userViewsApi.getUserViews(userId = userId).content.items - val destinations = linkedMapOf>() - val newest = mutableListOf() - val totals = linkedMapOf() - for (library in libraries) { - val key = when { - "anime" in library.name.orEmpty().lowercase() -> "anime" - library.collectionType == CollectionType.MOVIES -> "movies" - library.collectionType == CollectionType.TVSHOWS -> "shows" - else -> continue - } - val response = api.itemsApi.getItems(userId = userId, parentId = library.id, recursive = true, includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES), fields = FIELDS, sortBy = listOf(ItemSortBy.IS_PLAYED, ItemSortBy.DATE_CREATED), sortOrder = listOf(SortOrder.ASCENDING, SortOrder.DESCENDING), limit = 20).content - destinations.getOrPut(key) { mutableListOf() }.addAll(response.items.map(::title)) - totals[key] = (totals[key] ?: 0) + response.totalRecordCount - newest.addAll(api.itemsApi.getItems(userId = userId, parentId = library.id, recursive = true, includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES), fields = FIELDS, sortBy = listOf(ItemSortBy.DATE_CREATED), sortOrder = listOf(SortOrder.DESCENDING), limit = 12).content.items.map(::title)) - } - return HomeLibrary(resume, next, newest.sortedByDescending { it.addedAt }.take(12), destinations, totals) - } - - suspend fun playableId(userId: UUID, title: HomeTitle): UUID? { - title.id.let { runCatching { UUID.fromString(it) }.getOrNull() }?.let { id -> - val item = api.userLibraryApi.getItem(userId = userId, itemId = id).content - if (item.type == BaseItemKind.SERIES) { - return (api.tvShowsApi.getNextUp(userId = userId, seriesId = id, limit = 1).content.items.firstOrNull() - ?: api.tvShowsApi.getEpisodes(seriesId = id, userId = userId, limit = 1).content.items.firstOrNull()) - ?.takeIf { it.type == BaseItemKind.EPISODE }?.id - } - return item.id.takeIf { item.type == BaseItemKind.MOVIE || item.type == BaseItemKind.EPISODE } - } - val kinds = if (title.isEpisode) listOf(BaseItemKind.EPISODE) else listOf(BaseItemKind.MOVIE) - return api.itemsApi.getItems(userId = userId, recursive = true, searchTerm = title.title, includeItemTypes = kinds, fields = FIELDS, limit = 20).content.items.firstOrNull { - it.name == title.title && (!title.isEpisode || (it.seriesName == title.seriesTitle && it.parentIndexNumber == title.season && it.indexNumber == title.episode)) - }?.id - } - - private fun title(item: BaseItemDto): HomeTitle { - val wrapped = BaseItem(item) - return HomeTitle( - id = item.id.toString(), title = item.name.orEmpty(), type = when (item.type) { BaseItemKind.EPISODE -> "episode"; BaseItemKind.SERIES -> "tv"; else -> "movie" }, - year = item.productionYear, runtimeSeconds = item.runTimeTicks?.div(TICKS_PER_SECOND)?.toInt(), seasons = item.childCount, - overview = item.overview.orEmpty(), poster = images.getItemImageUrl(wrapped, ImageType.PRIMARY, fillWidth = 256, fillHeight = 384), - backdrop = images.getItemImageUrl(wrapped, ImageType.BACKDROP, fillWidth = 1280, fillHeight = 720), - seriesTitle = item.seriesName, season = item.parentIndexNumber, episode = item.indexNumber, - resumeSeconds = (item.userData?.playbackPositionTicks ?: 0) / TICKS_PER_SECOND.toDouble(), played = item.userData?.played == true, - addedAt = item.dateCreated?.toString().orEmpty(), genres = item.genres.orEmpty(), held = true, tmdbId = item.providerIds?.get("Tmdb")?.toIntOrNull(), - ) - } - - companion object { - private const val TICKS_PER_SECOND = 10_000_000L - private val FIELDS = listOf(ItemFields.PROVIDER_IDS, ItemFields.OVERVIEW, ItemFields.DATE_CREATED, ItemFields.GENRES, ItemFields.CHILD_COUNT) - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/LibraryReadFacade.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/LibraryReadFacade.kt deleted file mode 100644 index fbb037b9..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/LibraryReadFacade.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.justinvisser.wolfskin.tv.read - -import com.github.damontecres.wholphin.ui.nav.Destination -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.userViewsApi -import org.jellyfin.sdk.api.client.extensions.itemsApi -import org.jellyfin.sdk.api.client.extensions.imageApi -import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.ItemSortBy -import org.jellyfin.sdk.model.api.ImageType -import org.justinvisser.wolfskin.design.LibraryTitle -import org.justinvisser.wolfskin.design.WolfskinTokens -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import java.util.UUID -import javax.inject.Inject - -/** SDK reads are confined here so shell and screen state never depend on SDK responses. */ -class LibraryReadFacade @Inject constructor(private val api: ApiClient) { - suspend fun readDestinations(userId: UUID): Map { - val libraries = api.userViewsApi.getUserViews(userId = userId).content.items.orEmpty() - return buildMap { - libraries.forEach { library -> - val name = library.name.orEmpty().lowercase() - val key = when { - "anime" in name -> "anime" - library.collectionType == CollectionType.MOVIES -> "movies" - library.collectionType == CollectionType.TVSHOWS -> "shows" - else -> null - } - if (key != null) put(key, Destination.MediaItem(library.id, BaseItemKind.COLLECTION_FOLDER, library.collectionType)) - } - } - } - - /** A destination spans every matching accessible library; containers never become titles. */ - suspend fun readLibrary(userId: UUID, destination: String): List { - val libraries = api.userViewsApi.getUserViews(userId = userId).content.items.orEmpty().filter { library -> - val anime = library.name.orEmpty().contains("anime", ignoreCase = true) - when (destination) { - "anime" -> anime - "movies" -> !anime && library.collectionType == CollectionType.MOVIES - "shows" -> !anime && library.collectionType == CollectionType.TVSHOWS - else -> false - } - } - val titles = linkedMapOf() - for (library in libraries) { - var offset = 0 - do { - val response = api.itemsApi.getItems( - userId = userId, parentId = library.id, recursive = true, - includeItemTypes = listOf(if (destination == "movies") BaseItemKind.MOVIE else BaseItemKind.SERIES), - fields = listOf(ItemFields.PROVIDER_IDS, ItemFields.SORT_NAME, ItemFields.DATE_CREATED), - sortBy = listOf(ItemSortBy.SORT_NAME), startIndex = offset, - limit = WolfskinTokens.Behaviour.libraryChunk.toInt(), - ).content - val chunk = response.items.orEmpty() - for (item in chunk) { - val runtime = item.runTimeTicks?.div(10_000_000) - val position = item.userData?.playbackPositionTicks ?: 0 - titles[item.id] = LibraryTitle( - id = item.id.toString(), title = item.name.orEmpty(), type = if (item.type == BaseItemKind.MOVIE) "movie" else "tv", - year = item.productionYear, runtimeSeconds = runtime, seasons = item.childCount, - poster = item.imageTags?.get(ImageType.PRIMARY)?.let { api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY, tag = it) }, - played = item.userData?.played == true, - progress = if ((item.runTimeTicks ?: 0) > 0) (position.toDouble() / item.runTimeTicks!!).toFloat() else 0f, - addedAt = item.dateCreated?.toString().orEmpty(), tmdbId = item.providerIds?.get("Tmdb"), - ) - } - offset += chunk.size - } while (chunk.isNotEmpty() && offset < response.totalRecordCount) - } - return titles.values.toList() - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/LibrarySnapshotStore.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/LibrarySnapshotStore.kt deleted file mode 100644 index 263922eb..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/LibrarySnapshotStore.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.justinvisser.wolfskin.tv.read - -import java.io.IOException -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import org.justinvisser.wolfskin.conductarr.CacheEntry -import org.justinvisser.wolfskin.conductarr.CacheStore -import org.justinvisser.wolfskin.design.LibraryTitle -import org.justinvisser.wolfskin.design.WolfskinTokens - -/** Saved data is published before refresh; an unreachable server never replaces it with an error. */ -data class LibrarySnapshot(val items: List, val savedAtMillis: Long?, val unreachable: Boolean) - -/** Library snapshots share the existing atomic disk store, partitioned by server and person. */ -class LibrarySnapshotStore(private val cache: CacheStore, private val now: () -> Long = System::currentTimeMillis) { - fun read(scope: String, destination: String, force: Boolean = false, load: suspend () -> List): Flow = flow { - val key = "library:$destination" - val saved = try { cache.read(scope, key) } catch (_: IOException) { null } - val titles = saved?.let { try { decode(it.body) } catch (_: JSONException) { null } } - if (saved != null && titles != null) emit(LibrarySnapshot(titles, saved.savedAtMillis, false)) - val lifetime = (WolfskinTokens.Behaviour.libraryRefreshMinutes * 60_000).toLong() - if (!force && saved != null && titles != null && now() - saved.validatedAtMillis < lifetime) return@flow - try { - val current = load() - val time = now() - try { cache.write(scope, key, CacheEntry(encode(current), null, time, time, group = "library")) } catch (_: IOException) { /* A readable server remains usable without writable local storage. */ } - emit(LibrarySnapshot(current, time, false)) - } catch (exception: Exception) { - if (exception is CancellationException) throw exception - emit(LibrarySnapshot(titles.orEmpty(), saved?.savedAtMillis.takeIf { titles != null }, true)) - } - }.flowOn(Dispatchers.IO) - - private fun encode(items: List): String = JSONArray().apply { - items.forEach { title -> put(JSONObject().apply { - put("id", title.id); put("title", title.title); put("type", title.type) - put("year", title.year); put("runtime", title.runtimeSeconds); put("seasons", title.seasons) - put("poster", title.poster); put("played", title.played); put("progress", title.progress) - put("added", title.addedAt); put("tmdb", title.tmdbId) - }) } - }.toString() - - private fun decode(body: String): List { - val array = JSONArray(body) - return List(array.length()) { index -> array.getJSONObject(index).let { item -> - LibraryTitle(id = item.getString("id"), title = item.getString("title"), type = item.getString("type"), - year = item.optInt("year").takeIf { it > 0 }, runtimeSeconds = item.optLong("runtime").takeIf { it > 0 }, - seasons = item.optInt("seasons").takeIf { it > 0 }, poster = item.optString("poster").takeIf { it.isNotEmpty() }, - played = item.optBoolean("played"), progress = item.optDouble("progress", 0.0).toFloat(), addedAt = item.optString("added"), - tmdbId = item.optString("tmdb").takeIf { it.isNotEmpty() }) - } } - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/TitleReadFacade.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/TitleReadFacade.kt deleted file mode 100644 index bdad22a5..00000000 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/read/TitleReadFacade.kt +++ /dev/null @@ -1,186 +0,0 @@ -package org.justinvisser.wolfskin.tv.read - -import android.content.Context -import com.github.damontecres.wholphin.data.ItemPlaybackRepository -import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.TrackIndex -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.FavoriteWatchManager -import com.github.damontecres.wholphin.services.ImageUrlService -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.json.Json -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.itemsApi -import org.jellyfin.sdk.api.client.extensions.tvShowsApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.ItemSortBy -import org.jellyfin.sdk.model.api.MediaStreamType -import org.jellyfin.sdk.model.api.request.GetEpisodesRequest -import org.jellyfin.sdk.model.api.request.GetItemsRequest -import org.jellyfin.sdk.model.api.request.GetNextUpRequest -import org.jellyfin.sdk.model.serializer.toUUIDOrNull -import org.json.JSONObject -import org.justinvisser.wolfskin.design.TitleAction -import org.justinvisser.wolfskin.design.TitleConnection -import org.justinvisser.wolfskin.design.TitleItem -import org.justinvisser.wolfskin.design.TitlePageData -import org.justinvisser.wolfskin.design.TitlePerson -import org.justinvisser.wolfskin.design.TitleTrack -import org.justinvisser.wolfskin.design.TitleVersion -import java.io.File -import java.util.Locale -import java.util.UUID -import javax.inject.Inject - -data class PersonRead(val person: TitlePerson, val credits: List, val savedAt: String? = null) - -/** Jellyfin SDK changes are confined to this boundary; title UI sees immutable screen data only. */ -class TitleReadFacade @Inject constructor( - private val api: ApiClient, - private val images: ImageUrlService, - private val playback: ItemPlaybackRepository, - private val repository: ServerRepository, - private val watched: FavoriteWatchManager, - @param:ApplicationContext private val context: Context, -) { - private val json = Json { ignoreUnknownKeys = true } - private val fields = listOf(ItemFields.PROVIDER_IDS, ItemFields.MEDIA_SOURCES, ItemFields.MEDIA_STREAMS, ItemFields.OVERVIEW, ItemFields.PEOPLE, ItemFields.PARENT_ID) - - /** A disk copy is decoded before any network read; ownership is isolated by server and user. */ - suspend fun readSaved(id: UUID, preferences: UserPreferences): TitlePageData? = withContext(Dispatchers.IO) { - val file = cacheFile(id) - if (!file.exists()) return@withContext null - try { - val saved = JSONObject(file.readText()) - val rows = json.decodeFromString(ListSerializer(BaseItemDto.serializer()), saved.getString("items")) - val numbers = saved.getJSONArray("seasonNumbers").let { array -> List(array.length()) { array.getInt(it) } } - build(rows, numbers, saved.optInt("selectedSeason"), saved.getString("watch"), saved.optString("target").takeIf { it.isNotEmpty() }, preferences) - .copy(savedAt = savedTime(file), trailerUrl = saved.optString("trailer").takeIf { it.isNotEmpty() }) - } catch (_: java.io.IOException) { null } catch (_: IllegalArgumentException) { null } catch (_: org.json.JSONException) { null } - } - - /** An episode opens its series with that exact episode as both play and watched target. */ - suspend fun read(id: UUID, season: Int?, preferences: UserPreferences): TitlePageData = withContext(Dispatchers.IO) { - val file = cacheFile(id) - val opened = api.userLibraryApi.getItem(id).content - val series = if (opened.type == BaseItemKind.EPISODE && opened.seriesId != null) api.userLibraryApi.getItem(opened.seriesId!!).content else opened - val seasons = if (series.type == BaseItemKind.SERIES) api.itemsApi.getItems(GetItemsRequest(parentId = series.id, includeItemTypes = listOf(BaseItemKind.SEASON), recursive = false, sortBy = listOf(ItemSortBy.INDEX_NUMBER))).content.items.orEmpty() else emptyList() - val next = if (opened.type == BaseItemKind.EPISODE || opened.type == BaseItemKind.MOVIE) opened else { - api.tvShowsApi.getNextUp(GetNextUpRequest(seriesId = series.id, userId = repository.currentUser?.id, limit = 1, fields = fields, enableResumable = true)).content.items.firstOrNull()?.let { api.userLibraryApi.getItem(it.id).content } - } - val selectedNumber = season ?: next?.parentIndexNumber ?: seasons.firstOrNull()?.indexNumber - val selected = seasons.firstOrNull { it.indexNumber == selectedNumber } ?: seasons.firstOrNull() - val episodes = selected?.let { api.tvShowsApi.getEpisodes(GetEpisodesRequest(seriesId = series.id, seasonId = it.id, fields = fields)).content.items.orEmpty() }.orEmpty() - val target = next ?: episodes.firstOrNull { it.type == BaseItemKind.EPISODE } - val rows = (listOf(series, opened) + listOfNotNull(target) + episodes).distinctBy { it.id } - val numbers = seasons.mapNotNull { it.indexNumber } - val result = build(rows, numbers, selected?.indexNumber, opened.id.toString(), target?.id?.toString(), preferences) - val localTrailer = if (result.trailerUrl == null && (series.localTrailerCount ?: 0) > 0) api.userLibraryApi.getLocalTrailers(series.id).content.firstOrNull()?.id?.toString() else null - val trailer = result.trailerUrl ?: localTrailer?.let { "jellyfin:$it" } - val saved = JSONObject().put("items", json.encodeToString(ListSerializer(BaseItemDto.serializer()), rows)) - .put("seasonNumbers", org.json.JSONArray(numbers)).put("selectedSeason", selected?.indexNumber).put("watch", opened.id.toString()) - .put("target", target?.id?.toString().orEmpty()).put("trailer", trailer.orEmpty()) - file.parentFile?.mkdirs() - try { file.writeText(saved.toString()) } catch (_: java.io.IOException) { /* Playback remains available when the disk cannot save a copy. */ } - result.copy(trailerUrl = trailer) - } - - /** A failed reachability read leaves navigation on the title instead of opening a dead player. */ - suspend fun preparePlayback(id: UUID) { api.userLibraryApi.getItem(id) } - - /** Track selection uses the same repository consumed by the upstream player. */ - suspend fun apply(action: TitleAction) = withContext(Dispatchers.IO) { - when (action) { - is TitleAction.Version -> playback.savePlayVersion( - UUID.fromString(action.item.id), - requireNotNull(action.version.id.toUUIDOrNull()) { "Invalid Jellyfin media source identifier" }, - ) - is TitleAction.Track -> { - val id = UUID.fromString(action.item.id) - val item = BaseItem(api.userLibraryApi.getItem(id).content) - var choice = repository.currentUser?.let { playback.itemPlaybackDao.getItem(it, id) } - action.audio?.let { choice = playback.saveTrackSelection(item, choice, it, MediaStreamType.AUDIO) } - playback.saveTrackSelection(item, choice, if (action.subtitle == -1) TrackIndex.DISABLED else action.subtitle, MediaStreamType.SUBTITLE) - } - is TitleAction.Watched -> watched.setWatched(UUID.fromString(action.item.id), action.watched) - else -> Unit - } - Unit - } - - /** A person keeps the same server/user-scoped last good copy as a title. */ - suspend fun readPersonSaved(id: UUID): PersonRead? = withContext(Dispatchers.IO) { - val file = personCacheFile(id) - if (!file.exists()) return@withContext null - try { - val rows = json.decodeFromString(ListSerializer(BaseItemDto.serializer()), file.readText()) - personData(id, rows.first(), rows.drop(1)).copy(savedAt = savedTime(file)) - } catch (_: java.io.IOException) { null } catch (_: IllegalArgumentException) { null } - } - - /** Held performer credits are read continuously; a signed-out TV never falls back to Seerr. */ - suspend fun readPerson(id: UUID, start: Int): PersonRead = withContext(Dispatchers.IO) { - val file = personCacheFile(id) - val previous = if (start > 0 && file.exists()) { - try { json.decodeFromString(ListSerializer(BaseItemDto.serializer()), file.readText()) } - catch (_: java.io.IOException) { emptyList() } catch (_: IllegalArgumentException) { emptyList() } - } else emptyList() - val person = previous.firstOrNull() ?: api.userLibraryApi.getItem(id).content - val credits = api.itemsApi.getItems(GetItemsRequest(personIds = listOf(id), recursive = true, - includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES), fields = fields, startIndex = start, limit = 200, - sortBy = listOf(ItemSortBy.PRODUCTION_YEAR, ItemSortBy.SORT_NAME))).content.items.orEmpty() - val rows = listOf(person) + (previous.drop(1) + credits).distinctBy { it.id } - file.parentFile?.mkdirs() - try { file.writeText(json.encodeToString(ListSerializer(BaseItemDto.serializer()), rows)) } - catch (_: java.io.IOException) { /* The readable grid does not disappear when saving fails. */ } - personData(id, person, credits) - } - - private fun personData(id: UUID, person: BaseItemDto, credits: List): PersonRead = - PersonRead(TitlePerson(id.toString(), person.name.orEmpty(), portrait = images.getItemImageUrl(id, ImageType.PRIMARY, maxWidth = 240, maxHeight = 240), biography = person.overview.orEmpty()), - credits.map { credit -> mapItem(credit).copy(character = credit.people.orEmpty().firstOrNull { it.id == id }?.role) }) - - private suspend fun build(rows: List, seasonNumbers: List, selectedSeason: Int?, watchId: String, targetId: String?, preferences: UserPreferences): TitlePageData { - val item = rows.first() - val target = rows.firstOrNull { it.id.toString() == targetId } - val chosen = target?.let { playback.getSelectedTracks(it.id, BaseItem(it), preferences) } - val versions = target?.mediaSources.orEmpty().map { source -> - val streams = source.mediaStreams.orEmpty() - TitleVersion(source.id.orEmpty(), source.name ?: source.path ?: source.id.orEmpty(), streams.firstOrNull { it.type == MediaStreamType.VIDEO }?.displayTitle.orEmpty(), - source.bitrate?.let { String.format(Locale.getDefault(), "%.1f Mbps", it / 1_000_000.0) }.orEmpty(), - source.size?.let { String.format(Locale.getDefault(), "%.1f GB", it / 1_000_000_000.0) }.orEmpty(), source.container.orEmpty().uppercase(), - streams.filter { it.type == MediaStreamType.AUDIO }.map { TitleTrack(it.index, it.displayTitle ?: it.language ?: it.codec.orEmpty(), it.isDefault) }, - streams.filter { it.type == MediaStreamType.SUBTITLE }.map { TitleTrack(it.index, it.displayTitle ?: it.language ?: it.codec.orEmpty(), it.isDefault) }) - } - return TitlePageData(mapItem(item).copy(seasons = seasonNumbers.size), target?.takeIf { it.type == BaseItemKind.EPISODE || (it.userData?.playbackPositionTicks ?: 0) > 0 }?.let { mapItem(it) }, - rows.first { it.id.toString() == watchId }.let { mapItem(it) }, versions, chosen?.source?.id, chosen?.audioStream?.stream?.index, - if (chosen?.subtitlesDisabled == true) -1 else chosen?.subtitleStream?.stream?.index ?: -1, - item.remoteTrailers.orEmpty().firstNotNullOfOrNull { it.url }, - cast = item.people.orEmpty().filter { it.type.toString().equals("Actor", true) }.map { person -> - TitlePerson(person.id.toString(), person.name.orEmpty(), person.role.orEmpty(), person.primaryImageTag?.let { images.getItemImageUrl(person.id, ImageType.PRIMARY, maxWidth = 200, maxHeight = 200, tag = it) }) - }, - episodes = rows.filter { it.type == BaseItemKind.EPISODE && it.parentIndexNumber == selectedSeason }.map { mapItem(it) }, - connection = TitleConnection.SignedOut, seasonNumbers = seasonNumbers, selectedSeason = selectedSeason) - } - - private fun mapItem(item: BaseItemDto): TitleItem = TitleItem( - item.id.toString(), item.name.orEmpty(), when (item.type) { BaseItemKind.MOVIE -> "movie"; BaseItemKind.EPISODE -> "episode"; else -> "tv" }, - item.productionYear, (item.runTimeTicks ?: 0) / 10_000_000, overview = item.overview.orEmpty(), genres = item.genres.orEmpty(), - poster = item.imageTags?.get(ImageType.PRIMARY)?.let { images.getItemImageUrl(item.id, ImageType.PRIMARY, maxWidth = 256, maxHeight = 384, tag = it) }, - backdrop = item.backdropImageTags?.firstOrNull()?.let { images.getItemImageUrl(item.id, ImageType.BACKDROP, maxWidth = 1280, maxHeight = 720, tag = it) }, - held = true, watched = item.userData?.played == true, resumeSeconds = (item.userData?.playbackPositionTicks ?: 0) / 10_000_000, - seriesId = item.seriesId?.toString(), seriesName = item.seriesName, season = item.parentIndexNumber ?: 0, episode = item.indexNumber ?: 0, - ) - - private fun cacheFile(id: UUID): File = File(context.filesDir, "wolfskin-title/${repository.currentServer?.id}/${repository.currentUser?.id}/$id.json") - private fun personCacheFile(id: UUID): File = File(cacheFile(id).parentFile, "person-$id.json") - private fun savedTime(file: File): String = java.text.SimpleDateFormat("HH:mm", Locale.getDefault()).format(java.util.Date(file.lastModified())) -} diff --git a/conductarr-client/.gitignore b/conductarr-client/.gitignore deleted file mode 100644 index 84c048a7..00000000 --- a/conductarr-client/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build/ diff --git a/conductarr-client/README.md b/conductarr-client/README.md deleted file mode 100644 index 573929a2..00000000 --- a/conductarr-client/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Conductarr TV client - -A UI-free JVM module for the TV operations in the [approved TV spec](https://pages.justinvisser.org/wolfskin/tv-spec), sections 3 and 8. The Android app depends on this module; no screen, navigation, login flow or production service is changed. - -## Pinned contract and generator decision - -`openapi/openapi.json` is the unmodified OpenAPI 3.1.0 document from Conductarr commit `481492d3a8a0651a89901de548cd511218c0562e`. `openapi/commit.sha` records the source. It was read with `git show :openapi.json` from the Conductarr clone, not downloaded from a running service. - -The approved fallback is used: checked-in, maintained kotlinx.serialization data classes and a thin typed TV API, rather than build-time generation. - -The OpenAPI Generator **7.26.0** probe used the `kotlin` generator with `serializationLibrary=kotlinx_serialization`, `useCoroutines=true`, `modelMutable=false` and `enumPropertyNaming=UPPERCASE`. Generation completed with its OpenAPI 3.1 beta warning. Nullable arrays were retained. However, the unconstrained JSON schema at `ErrorDetail.value` became `@Contextual Any?`. The generated serializer module has no `Any` adapter. Compiling the generated `ErrorDetail` and decoding `{"value":{"nested":[1,null,true]}}` with kotlinx.serialization produced: - -```text -Serializer for class 'Any' is not found. -``` - -Thus the generator does **not** handle every construct in this pinned document. The fallback uses `JsonElement?` for arbitrary error values. Its regression test covers objects, arrays, numbers, booleans and null. - -`ContractTest` checks: - -- All 20 TV method/path pairs against the pinned paths, including response and mutation body schemas. -- All 68 maintained schemas through their actual serialization descriptors: fields, references, primitive widths, collections, maps, enum wire values and requiredness. Read-only `$schema` metadata is intentionally ignored. Optional scalar properties use nullable Kotlin fields to represent absence; encoding omits them. -- A separately reviewed schema snapshot, `src/test/resources/model-contracts.json`, so changes to formats, limits, defaults or other schema constraints also require explicit review. Models preserve those wire shapes; Conductarr remains responsible for business validation and authority. - -When updating the pin, update the models and reviewed schema snapshot together. Do not regenerate the Seerr client into this module or substitute a running server's schema. - -## API and identity - -Create a `ConductarrClient(baseUrl, jellyfinToken, cache)` for the selected person's Jellyfin session. The caller supplies the token; the client sends exactly `Authorization: MediaBrowser Token=""`. The token must match the pinned contract's 32-hex Jellyfin access-token shape. HTTPS is required except for loopback fake servers. Base URL credentials, query strings and fragments are rejected. A path prefix is preserved. - -`TvRoutes` provides typed calls for operator, profiles, feed, dismissals, catalogue options/discovery/title/cast/related/enrichment/seasons, performers/filmography, activity/readiness, requests and library audiences. Request actors cannot be supplied: only the authenticated operator establishes the actor. The request body's requester selects the beneficiary. - -`client.read(call, trigger)` is a cold `Flow>`: collect it once per screen-level read. It emits saved data before starting network work, then a revalidation result when due. `ReadResult.Failure.saved` retains LKG on transport, HTTP and decoding failure. A 401 returns `NeedsSignIn` **without** fallback, invalidates that client's identity-scoped saved data, and prevents further calls on that instance. Create a new instance after sign-in/person switch. No Seerr fallback exists. - -`client.mutate(call)` returns a typed success or error. It never uses cached receipts, retries or follows redirects. Unknown request outcomes must be checked using `TvRoutes.requests(tmdb, media)`; do not blindly resend. - -The transport owns its OkHttp client rather than accepting arbitrary credential-logging interceptors. Diagnostics expose only `ClientEvent` enum values. Returned errors exclude bodies and exception text. Default timeouts: connection 10 s, read 20 s, whole call 30 s. At most two catalogue reads run concurrently. Close the client when its identity/session is discarded. - -## Saved data and refresh events - -Pass an **app-private, backup-excluded directory** to `FileCacheStore`; one store instance owns a directory in a process. Writes use a synced temporary file and atomic same-filesystem replacement. Corrupt records become misses. `MemoryCacheStore` is the deterministic fake. Data is not encrypted: cached household content belongs in private app storage, never shared/external storage. - -Cache keys partition by normalized server URL, credential digest, endpoint, path parameters and sorted query. Raw credentials are neither stored nor logged. A stale lifetime means refresh eligibility, **not deletion** of LKG. `Saved.savedAtMillis` supplies the Saved HH:MM timestamp; 304 preserves that timestamp while advancing validation time. - -| Operation family | Refresh rule | -|---|---| -| Operator and recommendation profiles | Retain indefinitely; `LAUNCH` refreshes | -| Library audiences | Daily | -| Feed | Server `expires_at`; conditional ETag revalidation | -| Catalogue options | `FOREGROUND` refreshes | -| Discovery pages | 30 minutes per parameterized lane/page | -| Enrichment ratings | 6 hours; 300 entries per movie/TV group; 404 means saved absence, failures never overwrite LKG | -| Title, cast, related, performer, filmography, dismissals, readiness | `OPEN` refreshes | -| Seasons | `OPEN` on season focus; retained per season, 300 entries | -| Activity | 30 seconds for caller-driven `POLL`, or `FOREGROUND` | -| Title requests and request mutations | No cache | - -`IF_STALE` observes lifetimes; `FORCE` explicitly revalidates; `CACHE_ONLY` paints without network. The caller schedules launch, foreground, screen-open and activity polling events; the module does not run a poller or own a UI spinner. Title-related calls should be hoisted, not issued by individual cards. ETags are sent when present on any endpoint, regardless of `Cache-Control: no-store`; this is the TV-owned application cache, not an HTTP response cache. - -## Verification - -The root `make test` selects `testDefaultDebugUnitTest` in this JVM module as well as the app. That module alias runs the JUnit contract/MockWebServer suite and the `ClientSmokeKt` executable. The smoke exercises a real loopback socket, disk reopen, ETag 304, and 503 recovery. All tests are local; none contact Conductarr or Jellyfin. - -The build gate is `make test` followed by `make apk-debug`, using the repository's serialized Docker Gradle wrapper. No signed release, installation, ADB, version change or production activation is part of this module. diff --git a/conductarr-client/build.gradle.kts b/conductarr-client/build.gradle.kts deleted file mode 100644 index d4e32aa0..00000000 --- a/conductarr-client/build.gradle.kts +++ /dev/null @@ -1,36 +0,0 @@ -plugins { - `java-library` - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.plugin.serialization) -} - -kotlin { - compilerOptions.jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) -} -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -dependencies { - api(libs.kotlinx.serialization.json) - api(platform(libs.okhttp.bom)) - implementation(libs.okhttp) - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${libs.versions.kotlinxCoroutinesTest.get()}") - testImplementation(libs.junit) - testImplementation("com.squareup.okhttp3:mockwebserver3") -} - -sourceSets.test { resources.srcDir("openapi") } - -val smoke by tasks.registering(JavaExec::class) { - dependsOn(tasks.testClasses) - classpath = sourceSets.test.get().runtimeClasspath - mainClass.set("org.justinvisser.wolfskin.conductarr.ClientSmokeKt") - mustRunAfter(tasks.test) -} - -// The root gate selects this name across all projects, including this JVM module. -tasks.register("testDefaultDebugUnitTest") { - dependsOn(tasks.test, smoke) -} diff --git a/conductarr-client/openapi/commit.sha b/conductarr-client/openapi/commit.sha deleted file mode 100644 index 67a3f347..00000000 --- a/conductarr-client/openapi/commit.sha +++ /dev/null @@ -1 +0,0 @@ -481492d3a8a0651a89901de548cd511218c0562e diff --git a/conductarr-client/openapi/openapi.json b/conductarr-client/openapi/openapi.json deleted file mode 100644 index 6a467051..00000000 --- a/conductarr-client/openapi/openapi.json +++ /dev/null @@ -1,15923 +0,0 @@ -{ - "components": { - "schemas": { - "AbandonInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/AbandonInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "reason": { - "description": "required non-empty audited reason for abandoning the candidate", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "ActingCredit": { - "additionalProperties": false, - "properties": { - "character": { - "type": "string" - }, - "credit_id": { - "type": "string" - }, - "episode_count": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "credit_id", - "character" - ], - "type": "object" - }, - "ActionResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ActionResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "cleared": { - "description": "queue records cleared by a reviewed closeout", - "format": "int64", - "type": "integer" - }, - "detail": { - "type": "string" - }, - "health_state": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "status": { - "description": "ok | blocked | busy | not_eligible | missing | invalid | arr_error", - "type": "string" - } - }, - "required": [ - "status" - ], - "type": "object" - }, - "ActivityFeed": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ActivityFeed.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/RequestProgress" - }, - "type": [ - "array", - "null" - ] - }, - "requester": { - "description": "Household key the feed is attributed to, or all for the household", - "type": "string" - }, - "sources": { - "items": { - "$ref": "#/components/schemas/EvidenceSource" - }, - "type": [ - "array", - "null" - ] - }, - "truncated": { - "description": "More recent requests exist than the window returned", - "type": "boolean" - } - }, - "required": [ - "requester", - "items", - "truncated", - "sources" - ], - "type": "object" - }, - "Actor": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "requester": { - "description": "The acting operator's own household requester key", - "type": "string" - }, - "role": { - "enum": [ - "admin", - "household" - ], - "type": "string" - } - }, - "required": [ - "name", - "role", - "requester" - ], - "type": "object" - }, - "ArrGateDrop": { - "additionalProperties": false, - "properties": { - "indexer": { - "type": "string" - }, - "rejections": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "release": { - "type": "string" - } - }, - "required": [ - "indexer", - "release", - "rejections" - ], - "type": "object" - }, - "Candidate": { - "additionalProperties": false, - "properties": { - "backdrop_path": { - "description": "TMDB-relative landscape artwork path; absent when none is observed", - "type": "string" - }, - "contributions": { - "items": { - "$ref": "#/components/schemas/SeedContribution" - }, - "type": [ - "array", - "null" - ] - }, - "genre_ids": { - "description": "Observed candidate genre IDs; absence does not prove no genres", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "overview": { - "type": "string" - }, - "poster_path": { - "type": "string" - }, - "score": { - "format": "double", - "type": "number" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "tvdb_id": { - "format": "int64", - "type": "integer" - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tvdb_id", - "title", - "overview", - "poster_path", - "year", - "score", - "contributions", - "media_type", - "tmdb_id" - ], - "type": "object" - }, - "CandidateReviewMapping": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/CandidateReviewMapping.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "available": { - "type": "boolean" - }, - "episode": { - "format": "int64", - "type": "integer" - }, - "episode_id": { - "format": "int64", - "type": "integer" - }, - "key": { - "description": "SxxEyy", - "type": "string" - }, - "season": { - "format": "int64", - "type": "integer" - }, - "title": { - "type": "string" - } - }, - "required": [ - "available" - ], - "type": "object" - }, - "CastMember": { - "additionalProperties": false, - "properties": { - "character": { - "type": "string" - }, - "credit_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "order": { - "format": "int64", - "type": "integer" - }, - "performer_id": { - "format": "int64", - "type": "integer" - }, - "profile_url": { - "type": "string" - } - }, - "required": [ - "performer_id", - "name", - "character", - "credit_id", - "order" - ], - "type": "object" - }, - "CreateReplacementHoldInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/CreateReplacementHoldInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "arr": { - "description": "primary Arr instance name", - "minLength": 1, - "type": "string" - }, - "episode_ids": { - "description": "one or more Sonarr episode ids; mutually exclusive with movie_id", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "movie_id": { - "description": "one Radarr movie id; mutually exclusive with episode_ids", - "format": "int64", - "type": "integer" - }, - "reason": { - "description": "why this replacement is being held", - "minLength": 1, - "type": "string" - }, - "ttl_hours": { - "description": "hold duration in hours; defaults to 168", - "format": "int64", - "maximum": 720, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "arr", - "reason" - ], - "type": "object" - }, - "DeleteBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/DeleteBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "result": { - "$ref": "#/components/schemas/DeleteResult", - "description": "the delete outcome (ok | partial | conflict | inconclusive | missing_in_arr | arr_error | not_wired | error) with pruned + mirror counts" - } - }, - "required": [ - "result" - ], - "type": "object" - }, - "DeleteRequest": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/DeleteRequest.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "scope_id": { - "description": "The scope_id of the deletion impact the operator confirmed; a different or unobtainable current scope deletes nothing", - "pattern": "^[0-9a-f]{64}$", - "type": "string" - } - }, - "required": [ - "scope_id" - ], - "type": "object" - }, - "DeleteResult": { - "additionalProperties": false, - "properties": { - "detail": { - "type": "string" - }, - "media_type": { - "type": "string" - }, - "mirrors": { - "format": "int64", - "type": "integer" - }, - "orphans": { - "format": "int64", - "type": "integer" - }, - "pruned": { - "format": "int64", - "type": "integer" - }, - "status": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "status" - ], - "type": "object" - }, - "DeletionAudience": { - "additionalProperties": false, - "properties": { - "bucket": { - "description": "Present only for a layer source", - "type": "string" - }, - "libraries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "owner": { - "description": "Membership owner label; absent for direct import sources", - "type": "string" - }, - "source": { - "description": "Configured layer:bucket or import:root-folder-basename reference", - "type": "string" - }, - "users": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "source", - "libraries", - "users" - ], - "type": "object" - }, - "DeletionCopy": { - "additionalProperties": false, - "properties": { - "arr_kind": { - "type": "string" - }, - "arr_name": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - }, - "path": { - "type": "string" - }, - "present": { - "type": "boolean" - }, - "title": { - "type": "string" - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "arr_kind", - "arr_name", - "present", - "id", - "title", - "year", - "path" - ], - "type": "object" - }, - "DeletionImpact": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/DeletionImpact.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "media_type": { - "type": "string" - }, - "scope": { - "$ref": "#/components/schemas/DeletionScope" - }, - "scope_id": { - "description": "SHA-256 comparison fingerprint, not authorization; fresh observation required before execution", - "type": "string" - }, - "status": { - "enum": [ - "ready", - "missing", - "inconclusive" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "status" - ], - "type": "object" - }, - "DeletionScope": { - "additionalProperties": false, - "properties": { - "audiences": { - "description": "Configured libraries/viewers affected through memberships, layer links or present primary/mirror import paths", - "items": { - "$ref": "#/components/schemas/DeletionAudience" - }, - "type": [ - "array", - "null" - ] - }, - "links": { - "description": "Managed layer symlinks to unlink, including separately labelled orphan collateral; emptied directories are pruned", - "items": { - "$ref": "#/components/schemas/RemovalLink" - }, - "type": [ - "array", - "null" - ] - }, - "memberships": { - "description": "Exact membership buckets to remove; no watched-state erasure", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "mirrors": { - "description": "Every configured rule-derived mirror, including verified absences", - "items": { - "$ref": "#/components/schemas/DeletionCopy" - }, - "type": [ - "array", - "null" - ] - }, - "primary": { - "$ref": "#/components/schemas/DeletionCopy" - }, - "retained_links": { - "description": "Existing layer links retained by the executor but losing their present primary/mirror source content; not unlink effects", - "items": { - "$ref": "#/components/schemas/RemovalLink" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "primary", - "mirrors", - "memberships", - "links", - "retained_links", - "audiences" - ], - "type": "object" - }, - "Detail": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/Detail.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "genres": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "held_title": { - "$ref": "#/components/schemas/HeldTitle" - }, - "metadata": { - "$ref": "#/components/schemas/TitleMetadata" - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "ratings": { - "items": { - "$ref": "#/components/schemas/Rating" - }, - "type": [ - "array", - "null" - ] - }, - "runtime": { - "format": "int64", - "type": "integer" - }, - "seasons": { - "items": { - "$ref": "#/components/schemas/Season" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - }, - "title": { - "$ref": "#/components/schemas/Title" - } - }, - "required": [ - "state", - "title", - "metadata_state", - "genres", - "seasons", - "metadata", - "observations" - ], - "type": "object" - }, - "Discovery": { - "additionalProperties": false, - "properties": { - "genre_id": { - "format": "int64", - "type": "integer" - }, - "next": { - "description": "Same selected query's next public page, bounded at page 500; absence alone does not prove catalogue exhaustion", - "type": "string" - }, - "region": { - "description": "Comma-separated selected service regions; NL for unfiltered discovery", - "type": "string" - }, - "service_id": { - "format": "int64", - "type": "integer" - }, - "sort": { - "enum": [ - "trending", - "popular", - "new", - "top", - "upcoming" - ], - "type": "string" - }, - "state": { - "description": "Empty is an answered zero-match lane; exhausted is an empty page beyond upstream total_pages; partial cannot prove absence", - "enum": [ - "observed", - "empty", - "partial", - "exhausted" - ], - "type": "string" - } - }, - "required": [ - "genre_id", - "service_id", - "sort", - "region", - "state" - ], - "type": "object" - }, - "DispatchLogEntry": { - "additionalProperties": false, - "properties": { - "arr_kind": { - "type": "string" - }, - "arr_name": { - "type": "string" - }, - "bucket": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - }, - "media_type": { - "type": "string" - }, - "mirrored_to": { - "type": "string" - }, - "outcome": { - "type": "string" - }, - "requested_by": { - "type": "string" - }, - "rule": { - "type": "string" - }, - "source": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "ts": { - "type": "string" - } - }, - "required": [ - "id", - "ts", - "tmdb_id", - "media_type", - "outcome" - ], - "type": "object" - }, - "Display": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/Display.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "poster_url": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "metadata_state" - ], - "type": "object" - }, - "DownloadsBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/DownloadsBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "provenance": { - "description": "current primary-Arr library items classified by the acquisition source of their current files: torrent, spot, manual, or usenet-streamed. torrent means at least one record still holds the file a torrent handoff imported; once managed usenet imports have replaced every such file, the item reads as that usenet lane and its torrent history stays in the torrent lane's handoff log", - "items": { - "$ref": "#/components/schemas/ProvenanceItem" - }, - "type": [ - "array", - "null" - ] - }, - "rows": { - "description": "one row per physical download, grouped by Arr target and download id", - "items": { - "$ref": "#/components/schemas/Row" - }, - "type": [ - "array", - "null" - ] - }, - "usage": { - "description": "library item counts and cached actual local-dataset bytes by provenance source", - "items": { - "$ref": "#/components/schemas/SourceUsage" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "rows", - "provenance", - "usage" - ], - "type": "object" - }, - "DryRunPlan": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/DryRunPlan.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "is_anime": { - "description": "bucket-routing anime-ness (from the matched rule)", - "type": "boolean" - }, - "matched": { - "description": "false when no arr had a lookup candidate or no rule matched", - "type": "boolean" - }, - "mirrors": { - "description": "rule-derived mirror instances the add fans out to", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "rule": { - "type": "string" - }, - "target": { - "description": "routed primary arr instance name", - "type": "string" - }, - "target_kind": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": [ - "matched", - "mirrors", - "is_anime" - ], - "type": "object" - }, - "Enrichment": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/Enrichment.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "genres": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "metadata": { - "$ref": "#/components/schemas/TitleMetadata" - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "ratings": { - "items": { - "$ref": "#/components/schemas/Rating" - }, - "type": [ - "array", - "null" - ] - }, - "runtime": { - "format": "int64", - "type": "integer" - }, - "seasons": { - "items": { - "$ref": "#/components/schemas/Season" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "observed", - "unavailable" - ], - "type": "string" - }, - "title": { - "$ref": "#/components/schemas/Title" - } - }, - "required": [ - "state", - "title", - "metadata_state", - "genres", - "seasons", - "metadata", - "observations", - "ratings" - ], - "type": "object" - }, - "Episode": { - "additionalProperties": false, - "properties": { - "air_date": { - "type": "string" - }, - "name": { - "type": "string" - }, - "number": { - "format": "int64", - "type": "integer" - }, - "overview": { - "type": "string" - }, - "recorded": { - "enum": [ - "recorded", - "missing", - "unknown" - ], - "type": "string" - } - }, - "required": [ - "number", - "name", - "overview", - "recorded" - ], - "type": "object" - }, - "EpisodeFileChip": { - "additionalProperties": false, - "properties": { - "quality": { - "type": "string" - }, - "release_name": { - "type": "string" - }, - "resolution": { - "type": "string" - }, - "size_bytes": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "size_bytes" - ], - "type": "object" - }, - "EpisodeOption": { - "additionalProperties": false, - "properties": { - "absolute_episode": { - "format": "int64", - "type": "integer" - }, - "air_date": { - "type": "string" - }, - "episode": { - "format": "int64", - "type": "integer" - }, - "episode_id": { - "format": "int64", - "type": "integer" - }, - "has_file": { - "type": "boolean" - }, - "in_payload": { - "type": "boolean" - }, - "key": { - "description": "SxxEyy", - "type": "string" - }, - "monitored": { - "type": "boolean" - }, - "reason": { - "type": "string" - }, - "season": { - "format": "int64", - "type": "integer" - }, - "selectable": { - "type": "boolean" - }, - "title": { - "type": "string" - } - }, - "required": [ - "episode_id", - "season", - "episode", - "key", - "monitored", - "has_file", - "in_payload", - "selectable" - ], - "type": "object" - }, - "EpisodeRow": { - "additionalProperties": false, - "properties": { - "air_date": { - "type": "string" - }, - "episode": { - "format": "int64", - "type": "integer" - }, - "file": { - "$ref": "#/components/schemas/EpisodeFileChip" - }, - "has_file": { - "type": "boolean" - }, - "key": { - "description": "SxxEyy", - "type": "string" - }, - "monitored": { - "type": "boolean" - }, - "queue_state": { - "description": "Arr queue status for this exact episode; omitted when no matching queue row was observed", - "type": "string" - }, - "runtime_minutes": { - "description": "Sonarr episode runtime in minutes; omitted when Sonarr reports none", - "format": "int64", - "type": "integer" - }, - "season": { - "format": "int64", - "type": "integer" - }, - "title": { - "type": "string" - }, - "watched": { - "additionalProperties": { - "type": "boolean" - }, - "type": "object" - } - }, - "required": [ - "season", - "episode", - "key", - "monitored", - "has_file", - "watched" - ], - "type": "object" - }, - "EpisodeView": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/EpisodeView.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "owners": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "seasons": { - "items": { - "$ref": "#/components/schemas/SeasonGroup" - }, - "type": [ - "array", - "null" - ] - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "users": { - "description": "jellyfin usernames whose sees: matrix covers this item (watch-chip columns)", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "tmdb_id", - "title", - "owners", - "users", - "seasons" - ], - "type": "object" - }, - "ErrorDetail": { - "additionalProperties": false, - "properties": { - "location": { - "description": "Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'", - "type": "string" - }, - "message": { - "description": "Error message text", - "type": "string" - }, - "value": { - "description": "The value at the given location" - } - }, - "type": "object" - }, - "ErrorModel": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ErrorModel.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "description": "A human-readable explanation specific to this occurrence of the problem.", - "examples": [ - "Property foo is required but is missing." - ], - "type": "string" - }, - "errors": { - "description": "Optional list of individual error details", - "items": { - "$ref": "#/components/schemas/ErrorDetail" - }, - "type": [ - "array", - "null" - ] - }, - "instance": { - "description": "A URI reference that identifies the specific occurrence of the problem.", - "examples": [ - "https://example.com/error-log/abc123" - ], - "format": "uri", - "type": "string" - }, - "status": { - "description": "HTTP status code", - "examples": [ - 400 - ], - "format": "int64", - "type": "integer" - }, - "title": { - "description": "A short, human-readable summary of the problem type. This value should not change between occurrences of the error.", - "examples": [ - "Bad Request" - ], - "type": "string" - }, - "type": { - "default": "about:blank", - "description": "A URI reference to human-readable documentation for the error.", - "examples": [ - "https://example.com/errors/example" - ], - "format": "uri", - "type": "string" - } - }, - "type": "object" - }, - "EvidenceSource": { - "additionalProperties": false, - "properties": { - "detail": { - "type": "string" - }, - "name": { - "type": "string" - }, - "state": { - "enum": [ - "observed", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "name", - "state" - ], - "type": "object" - }, - "FileDetail": { - "additionalProperties": false, - "properties": { - "audio": { - "type": "string" - }, - "container": { - "type": "string" - }, - "quality": { - "type": "string" - }, - "release_name": { - "type": "string" - }, - "resolution": { - "type": "string" - }, - "size_bytes": { - "format": "int64", - "type": "integer" - }, - "video": { - "type": "string" - } - }, - "required": [ - "size_bytes" - ], - "type": "object" - }, - "FillGapsResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/FillGapsResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "searched_count": { - "format": "int64", - "type": "integer" - }, - "status": { - "description": "ok | busy | missing_in_arr | arr_error", - "type": "string" - } - }, - "required": [ - "status", - "searched_count" - ], - "type": "object" - }, - "FilmographyPage": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/FilmographyPage.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "completeness": { - "description": "Seerr filters adult credits and excludes crew here; not an exhaustive career", - "enum": [ - "returned-non-adult-cast" - ], - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/FilmographyTitle" - }, - "type": [ - "array", - "null" - ] - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "page": { - "format": "int64", - "type": "integer" - }, - "page_size": { - "format": "int64", - "type": "integer" - }, - "performer_id": { - "format": "int64", - "type": "integer" - }, - "source": { - "enum": [ - "seerr" - ], - "type": "string" - }, - "source_paging": { - "enum": [ - "unpaged" - ], - "type": "string" - }, - "total_pages": { - "description": "Native pages of grouped typed titles; absent when source is unanswered", - "format": "int64", - "type": "integer" - }, - "total_results": { - "description": "Unique typed titles in this returned source set; absent when source is unanswered", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "performer_id", - "metadata_state", - "source", - "source_paging", - "completeness", - "page", - "page_size", - "items", - "observations" - ], - "type": "object" - }, - "FilmographyTitle": { - "additionalProperties": false, - "properties": { - "credits": { - "items": { - "$ref": "#/components/schemas/ActingCredit" - }, - "type": [ - "array", - "null" - ] - }, - "title": { - "$ref": "#/components/schemas/Title" - }, - "users": { - "description": "Configured viewers when visibility_state is configured; otherwise unknown", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "visibility_state": { - "description": "Configured library audience only, never observed Jellyfin visibility or playback", - "enum": [ - "configured", - "unknown" - ], - "type": "string" - } - }, - "required": [ - "title", - "credits", - "visibility_state", - "users" - ], - "type": "object" - }, - "Finding": { - "additionalProperties": false, - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "message": { - "type": "string" - }, - "severity": { - "description": "Raw Arr HealthCheckResult; informational update warnings remain warning", - "enum": [ - "ok", - "notice", - "warning", - "error" - ], - "type": "string" - }, - "source": { - "type": "string" - }, - "state": { - "description": "Health impact, independent of the raw severity", - "enum": [ - "healthy", - "degraded", - "unhealthy" - ], - "type": "string" - }, - "wiki_url": { - "type": "string" - } - }, - "required": [ - "source", - "severity", - "message", - "state" - ], - "type": "object" - }, - "Genre": { - "additionalProperties": false, - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "name" - ], - "type": "object" - }, - "HealthDetail": { - "additionalProperties": false, - "properties": { - "failed": { - "type": "boolean" - }, - "has_file": { - "type": "boolean" - }, - "in_queue": { - "type": "boolean" - }, - "missing": { - "type": "boolean" - } - }, - "required": [ - "has_file", - "in_queue", - "failed", - "missing" - ], - "type": "object" - }, - "HealthzBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/HealthzBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "config": { - "$ref": "#/components/schemas/HealthzConfig", - "description": "Provenance of the config bytes this process parsed" - }, - "features": { - "$ref": "#/components/schemas/HealthzFeatures", - "description": "Config gates the cockpit shows or hides surfaces on" - }, - "modules": { - "additionalProperties": { - "$ref": "#/components/schemas/HealthzModule" - }, - "description": "Per-module status from the registry", - "type": "object" - }, - "state": { - "description": "Aggregate state; a disabled module is excluded from it", - "enum": [ - "starting", - "healthy", - "degraded", - "unhealthy", - "stopped" - ], - "type": "string" - } - }, - "required": [ - "state", - "modules", - "features" - ], - "type": "object" - }, - "HealthzConfig": { - "additionalProperties": false, - "properties": { - "path": { - "description": "Config file the process loaded", - "type": "string" - }, - "sha256": { - "description": "Hex sha256 of the raw config bytes; compare with a host-side sha256sum", - "type": "string" - }, - "size": { - "description": "Byte length of those config bytes", - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, - "HealthzFeatures": { - "additionalProperties": false, - "properties": { - "torrent_fallback": { - "description": "features.torrent_fallback: the torrent lane and its Torrent tab", - "type": "boolean" - } - }, - "required": [ - "torrent_fallback" - ], - "type": "object" - }, - "HealthzModule": { - "additionalProperties": false, - "properties": { - "checked_at": { - "description": "Cached check time; for Arr health this is the last valid sample, omitted before the first valid sample", - "format": "date-time", - "type": "string" - }, - "detail": { - "description": "Human-readable context for non-healthy states", - "type": "string" - }, - "findings": { - "description": "Cached application diagnostics, preserving raw severity separately from health impact", - "items": { - "$ref": "#/components/schemas/Finding" - }, - "type": [ - "array", - "null" - ] - }, - "kind": { - "description": "process | glue; empty when the restart surface is not wired (spec emission)", - "type": "string" - }, - "observation": { - "$ref": "#/components/schemas/Observation", - "description": "Application observation freshness and latest attempt failure; omitted for modules without this contract" - }, - "state": { - "description": "Module state; disabled means a config gate turned this subsystem off", - "enum": [ - "starting", - "healthy", - "degraded", - "unhealthy", - "stopped", - "disabled" - ], - "type": "string" - } - }, - "required": [ - "state" - ], - "type": "object" - }, - "HeldTitle": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/HeldTitle.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "$ref": "#/components/schemas/ItemDetail" - }, - "episodes": { - "$ref": "#/components/schemas/EpisodeView" - }, - "libraries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "observed", - "partial" - ], - "type": "string" - }, - "versions": { - "items": { - "$ref": "#/components/schemas/TitleVersion" - }, - "type": [ - "array", - "null" - ] - }, - "watched": { - "additionalProperties": { - "type": "boolean" - }, - "type": "object" - } - }, - "required": [ - "state", - "detail", - "libraries", - "versions", - "observations", - "watched" - ], - "type": "object" - }, - "HistoryReport": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/HistoryReport.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "from": { - "format": "date-time", - "type": "string" - }, - "modules": { - "items": { - "$ref": "#/components/schemas/ModuleHistory" - }, - "type": [ - "array", - "null" - ] - }, - "samples": { - "format": "int64", - "type": "integer" - }, - "to": { - "format": "date-time", - "type": "string" - }, - "window": { - "type": "string" - } - }, - "required": [ - "window", - "from", - "to", - "samples", - "modules" - ], - "type": "object" - }, - "HoldingStats": { - "additionalProperties": false, - "properties": { - "missing": { - "format": "int64", - "type": "integer" - }, - "size_bytes": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "size_bytes", - "missing" - ], - "type": "object" - }, - "Item": { - "additionalProperties": false, - "properties": { - "arr_kind": { - "type": "string" - }, - "arr_name": { - "type": "string" - }, - "is_anime": { - "type": "boolean" - }, - "media_type": { - "type": "string" - }, - "owners": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "poster_ref": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "title", - "owners", - "is_anime", - "arr_kind", - "arr_name" - ], - "type": "object" - }, - "ItemDetail": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ItemDetail.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "backdrop_ref": { - "description": "Public provider landscape artwork from the holding Arr", - "type": "string" - }, - "file": { - "$ref": "#/components/schemas/FileDetail" - }, - "health": { - "$ref": "#/components/schemas/HealthDetail" - }, - "is_anime": { - "type": "boolean" - }, - "media_type": { - "type": "string" - }, - "owners": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "poster_ref": { - "description": "Public provider poster artwork from the holding Arr", - "type": "string" - }, - "runtime": { - "description": "minutes", - "format": "int64", - "type": "integer" - }, - "source": { - "$ref": "#/components/schemas/SourceDetail" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "users": { - "description": "jellyfin usernames whose sees: matrix covers this item (movie watch-chip columns)", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "title", - "owners", - "is_anime", - "source", - "health", - "users" - ], - "type": "object" - }, - "ItemsBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ItemsBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "counts": { - "description": "owner-rail counts including Unassigned", - "items": { - "$ref": "#/components/schemas/OwnerCount" - }, - "type": [ - "array", - "null" - ] - }, - "items": { - "description": "poster-grid entries: title/year/poster ref/owners/anime badge", - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": [ - "array", - "null" - ] - }, - "library": { - "$ref": "#/components/schemas/RailStats", - "description": "owner-rail Library stats: size/last import/queue/missing/failed" - }, - "media_type": { - "type": "string" - } - }, - "required": [ - "media_type", - "items", - "counts", - "library" - ], - "type": "object" - }, - "LayerVisibilityActionOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LayerVisibilityActionOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "action": { - "$ref": "#/components/schemas/LayerVisibilityActionView" - }, - "events": { - "items": { - "$ref": "#/components/schemas/LayerVisibilityEventView" - }, - "type": [ - "array", - "null" - ] - }, - "observation": { - "$ref": "#/components/schemas/LayerVisibilityObservationView" - } - }, - "required": [ - "action", - "events" - ], - "type": "object" - }, - "LayerVisibilityActionView": { - "additionalProperties": false, - "properties": { - "action_id": { - "description": "durable layer action identity; the approval names this", - "type": "string" - }, - "bucket": { - "type": "string" - }, - "created_at": { - "format": "date-time", - "type": "string" - }, - "detail": { - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "operation": { - "enum": [ - "MaterializeAndRefresh" - ], - "type": "string" - }, - "stage": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "updated_at": { - "format": "date-time", - "type": "string" - }, - "version": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "action_id", - "tmdb_id", - "media_type", - "bucket", - "operation", - "stage", - "detail", - "version", - "created_at", - "updated_at" - ], - "type": "object" - }, - "LayerVisibilityActionsOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LayerVisibilityActionsOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/LayerVisibilityActionView" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "LayerVisibilityApprovalView": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LayerVisibilityApprovalView.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "action_id": { - "type": "string" - }, - "bucket": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "stage": { - "type": "string" - }, - "updated_at": { - "format": "date-time", - "type": "string" - }, - "version": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "action_id", - "bucket", - "stage", - "detail", - "version", - "updated_at" - ], - "type": "object" - }, - "LayerVisibilityEventView": { - "additionalProperties": false, - "properties": { - "created_at": { - "format": "date-time", - "type": "string" - }, - "from_stage": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "to_stage": { - "type": "string" - } - }, - "required": [ - "from_stage", - "to_stage", - "reason", - "created_at" - ], - "type": "object" - }, - "LayerVisibilityExpectedItemView": { - "additionalProperties": false, - "properties": { - "key": { - "description": "movie sentinel or SxxEyy logical identity", - "type": "string" - }, - "path": { - "description": "expected owned-layer media-source path", - "type": "string" - }, - "provider": { - "description": "provider episode identity when available", - "type": "string" - } - }, - "required": [ - "key", - "path" - ], - "type": "object" - }, - "LayerVisibilityObservationView": { - "additionalProperties": false, - "properties": { - "bucket": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "expected": { - "items": { - "$ref": "#/components/schemas/LayerVisibilityExpectedItemView" - }, - "type": [ - "array", - "null" - ] - }, - "first_drift_at": { - "format": "date-time", - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "observed_at": { - "format": "date-time", - "type": "string" - }, - "status": { - "enum": [ - "converged", - "drift", - "scanner_pending", - "unknown", - "no_files", - "unreleased", - "needs_operator" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "bucket", - "status", - "detail", - "observed_at", - "expected" - ], - "type": "object" - }, - "LayerVisibilityObservationsOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LayerVisibilityObservationsOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/LayerVisibilityObservationView" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "LegacyMovieEntry": { - "additionalProperties": false, - "properties": { - "bucket": { - "type": "string" - }, - "entry_path": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "bucket", - "entry_path" - ], - "type": "object" - }, - "LibraryActivity": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LibraryActivity.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "failed": { - "description": "Titles with a recent failed grab across sources whose history answered", - "format": "int64", - "type": "integer" - }, - "history_state": { - "description": "Whether every source answered its import history; partial means failed and last_import are lower bounds", - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - }, - "last_import": { - "description": "Newest import timestamp across sources whose history answered", - "type": "string" - }, - "media_type": { - "type": "string" - }, - "queue": { - "description": "Active downloads across sources whose queue answered; never a stand-in for an unanswered queue", - "format": "int64", - "type": "integer" - }, - "queue_state": { - "description": "Whether every source answered its download queue; partial means queue is a lower bound", - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - }, - "sources": { - "description": "Per-source answers; detail names the unanswered read", - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "description": "Weakest of queue_state and history_state", - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "state", - "queue_state", - "history_state", - "media_type", - "queue", - "failed", - "sources" - ], - "type": "object" - }, - "LibraryAudience": { - "additionalProperties": false, - "properties": { - "is_anime": { - "type": "boolean" - }, - "libraries": { - "description": "Enabled configured household libraries covering this membership scope", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "owner": { - "description": "Named household membership used by the existing add, move and remove interface", - "type": "string" - }, - "users": { - "description": "Enabled Jellyfin usernames whose configured sees covers this scope, not watch-write authority", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "owner", - "media_type", - "is_anime", - "libraries", - "users" - ], - "type": "object" - }, - "LibraryAudiencesBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LibraryAudiencesBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "audiences": { - "items": { - "$ref": "#/components/schemas/LibraryAudience" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "audiences" - ], - "type": "object" - }, - "LibraryCoverage": { - "additionalProperties": false, - "properties": { - "complete": { - "description": "All active predicates classified and collection sources observed; not metadata freshness or playback proof", - "type": "boolean" - }, - "matched": { - "format": "int64", - "type": "integer" - }, - "metadata_observed": { - "description": "Titles with both latest genre and provider outcomes observed, not a freshness guarantee", - "format": "int64", - "type": "integer" - }, - "total": { - "format": "int64", - "type": "integer" - }, - "unresolved": { - "format": "int64", - "type": "integer" - }, - "watched": { - "description": "Watched source observation; absent/conflicting per-title flags remain unknown even after a successful collection read", - "enum": [ - "not_requested", - "observed", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "total", - "matched", - "unresolved", - "metadata_observed", - "complete", - "watched" - ], - "type": "object" - }, - "LibraryFact": { - "additionalProperties": false, - "properties": { - "metadata": { - "$ref": "#/components/schemas/TitleMetadata" - }, - "watched": { - "enum": [ - "watched", - "unwatched", - "unknown" - ], - "type": "string" - } - }, - "required": [ - "metadata", - "watched" - ], - "type": "object" - }, - "LibraryItem": { - "additionalProperties": false, - "properties": { - "added_at": { - "description": "When the title joined its holding Arr; not an import or playback timestamp", - "type": "string" - }, - "arr_kind": { - "type": "string" - }, - "arr_name": { - "type": "string" - }, - "backdrop_ref": { - "description": "Remote landscape artwork from the holding Arr", - "type": "string" - }, - "is_anime": { - "type": "boolean" - }, - "libraries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "type": "string" - }, - "missing": { - "type": "boolean" - }, - "owners": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "poster_ref": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "users": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "libraries", - "users", - "missing", - "tmdb_id", - "media_type", - "title", - "owners", - "is_anime", - "arr_kind", - "arr_name" - ], - "type": "object" - }, - "LibraryObservation": { - "additionalProperties": false, - "properties": { - "detail": { - "type": "string" - }, - "observed_at": { - "format": "date-time", - "type": "string" - }, - "source": { - "type": "string" - }, - "state": { - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "source", - "state" - ], - "type": "object" - }, - "LibraryResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/LibraryResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "counts": { - "items": { - "$ref": "#/components/schemas/OwnerCount" - }, - "type": [ - "array", - "null" - ] - }, - "coverage": { - "$ref": "#/components/schemas/LibraryCoverage" - }, - "facts": { - "items": { - "$ref": "#/components/schemas/LibraryFact" - }, - "type": [ - "array", - "null" - ] - }, - "items": { - "items": { - "$ref": "#/components/schemas/LibraryItem" - }, - "type": [ - "array", - "null" - ] - }, - "library": { - "type": "string" - }, - "media_type": { - "type": "string" - }, - "observation_age_ms": { - "description": "Age of observed_at when this response was built", - "format": "int64", - "type": "integer" - }, - "observed_at": { - "description": "Oldest answering source read behind this collection; may predate the request by the shared-acquisition window", - "format": "date-time", - "type": "string" - }, - "query": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "sources": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - }, - "stats": { - "$ref": "#/components/schemas/HoldingStats" - }, - "unresolved": { - "items": { - "$ref": "#/components/schemas/LibraryItem" - }, - "type": [ - "array", - "null" - ] - }, - "watch_checked_at": { - "description": "Successful watched title-collection observation time; omitted for unrequested or unavailable reads", - "format": "date-time", - "type": "string" - } - }, - "required": [ - "snapshot", - "observed_at", - "observation_age_ms", - "facts", - "unresolved", - "coverage", - "state", - "library", - "media_type", - "query", - "items", - "counts", - "sources", - "stats" - ], - "type": "object" - }, - "MediaHealthBreakerHoldInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthBreakerHoldInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "reason": { - "description": "REQUIRED non-empty reason for suspending detection", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "MediaHealthBreakerResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthBreakerResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "healthy_epochs": { - "description": "clean control epochs credited so far; two close the breaker", - "format": "int64", - "type": "integer" - }, - "held": { - "type": "boolean" - }, - "held_at": { - "type": "string" - }, - "held_by": { - "type": "string" - }, - "hold_reason": { - "type": "string" - }, - "open": { - "type": "boolean" - }, - "reason": { - "description": "the detector fault that opened the breaker, if any", - "type": "string" - } - }, - "required": [ - "open", - "held", - "healthy_epochs", - "detail" - ], - "type": "object" - }, - "MediaHealthConsumerView": { - "additionalProperties": false, - "properties": { - "arr": { - "description": "kind/name of the holding arr", - "type": "string" - }, - "episode_key": { - "description": "SxxEyy; \"-\" for a movie", - "type": "string" - }, - "import_path": { - "type": "string" - }, - "imported_at": { - "type": "string" - }, - "record_type": { - "description": "episode | movie", - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "arr", - "record_type", - "tmdb_id" - ], - "type": "object" - }, - "MediaHealthDecisionInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthDecisionInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "reason": { - "description": "audited operator note; REQUIRED to reject", - "type": "string" - } - }, - "type": "object" - }, - "MediaHealthDecisionResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthDecisionResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "state": { - "description": "the proposal status after the decision", - "type": "string" - }, - "status": { - "description": "ok | not_eligible | missing | invalid", - "type": "string" - } - }, - "required": [ - "status" - ], - "type": "object" - }, - "MediaHealthDetail": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthDetail.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "history": { - "items": { - "$ref": "#/components/schemas/MediaHealthRepairEvent" - }, - "type": [ - "array", - "null" - ] - }, - "proposal": { - "$ref": "#/components/schemas/MediaHealthProposalView" - } - }, - "required": [ - "proposal", - "history" - ], - "type": "object" - }, - "MediaHealthProbeInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthProbeInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "blob_id": { - "description": "blob id to probe", - "type": "string" - }, - "media_type": { - "description": "Required with tmdb_id for non-admin probes", - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "reason": { - "description": "why this blob needs a fresh probe", - "minLength": 1, - "type": "string" - }, - "tmdb_id": { - "description": "Required for non-admin probes: exact title with retained evidence for this blob", - "format": "int64", - "type": "integer" - }, - "webdav_path": { - "description": "WebDAV path to resolve to a blob", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "MediaHealthProbeResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthProbeResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "blob_id": { - "type": "string" - }, - "requested_at": { - "type": "string" - } - }, - "required": [ - "blob_id", - "requested_at" - ], - "type": "object" - }, - "MediaHealthProposalView": { - "additionalProperties": false, - "properties": { - "blob_id": { - "type": "string" - }, - "confirmed_at": { - "type": "string" - }, - "consumers": { - "items": { - "$ref": "#/components/schemas/MediaHealthConsumerView" - }, - "type": [ - "array", - "null" - ] - }, - "fault_kind": { - "description": "dead_blob", - "type": "string" - }, - "group": { - "description": "review | repairing | closed", - "type": "string" - }, - "media_type": { - "description": "tv | movie", - "type": "string" - }, - "proposal_id": { - "format": "int64", - "type": "integer" - }, - "repair_note": { - "type": "string" - }, - "repair_stage": { - "type": "string" - }, - "size_bytes": { - "format": "int64", - "type": "integer" - }, - "status": { - "description": "the proposal status grammar", - "type": "string" - }, - "summary": { - "description": "what approving this repair will do", - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "webdav_path": { - "type": "string" - } - }, - "required": [ - "proposal_id", - "group", - "status", - "fault_kind", - "blob_id", - "confirmed_at", - "media_type", - "summary", - "consumers" - ], - "type": "object" - }, - "MediaHealthRepairEvent": { - "additionalProperties": false, - "properties": { - "at": { - "type": "string" - }, - "from_stage": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "to_stage": { - "type": "string" - } - }, - "required": [ - "to_stage", - "at" - ], - "type": "object" - }, - "MediaHealthReviewResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MediaHealthReviewResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "breaker_open": { - "type": "boolean" - }, - "breaker_reason": { - "type": "string" - }, - "group": { - "type": "string" - }, - "proposals": { - "items": { - "$ref": "#/components/schemas/MediaHealthProposalView" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "breaker_open", - "proposals" - ], - "type": "object" - }, - "MembershipChange": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "move", - "add", - "remove" - ], - "type": "string" - }, - "actor": { - "description": "Server-authenticated subject that submitted this change", - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/MembershipItem" - }, - "type": [ - "array", - "null" - ] - }, - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "action", - "items" - ], - "type": "object" - }, - "MembershipInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MembershipInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "action": { - "enum": [ - "move", - "add", - "remove" - ], - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/MembershipItem" - }, - "type": [ - "array", - "null" - ] - }, - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "action", - "items" - ], - "type": "object" - }, - "MembershipItem": { - "additionalProperties": false, - "properties": { - "is_anime": { - "type": "boolean" - }, - "media_type": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "is_anime" - ], - "type": "object" - }, - "MembershipJob": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MembershipJob.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "change": { - "$ref": "#/components/schemas/MembershipChange" - }, - "error": { - "type": "string" - }, - "id": { - "type": "string" - }, - "results": { - "items": { - "$ref": "#/components/schemas/MembershipResult" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "pending", - "running", - "completed", - "failed" - ], - "type": "string" - } - }, - "required": [ - "id", - "state", - "change", - "results" - ], - "type": "object" - }, - "MembershipJobsBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MembershipJobsBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "jobs": { - "items": { - "$ref": "#/components/schemas/MembershipJob" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "jobs" - ], - "type": "object" - }, - "MembershipResult": { - "additionalProperties": false, - "properties": { - "action": { - "type": "string" - }, - "bucket": { - "type": "string" - }, - "media_type": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "source": { - "type": "string" - }, - "status": { - "type": "string" - }, - "target": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "watch_items": { - "description": "durable rows captured; may also be restored for destination-visible users", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "action", - "source", - "target", - "tmdb_id", - "media_type", - "status" - ], - "type": "object" - }, - "MissingItem": { - "additionalProperties": false, - "properties": { - "check_error": { - "description": "an arr lookup errored โ€” membership uncertain, not pruned", - "type": "boolean" - }, - "media_type": { - "type": "string" - }, - "missing_in": { - "description": "arr instance names the item is absent from", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "rule": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "missing_in" - ], - "type": "object" - }, - "MissingOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MissingOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/MissingItem" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "ModuleHistory": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "series": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "transitions": { - "format": "int64", - "type": "integer" - }, - "uptime": { - "format": "double", - "type": "number" - } - }, - "required": [ - "name", - "series", - "uptime", - "transitions" - ], - "type": "object" - }, - "MovieCutoverBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MovieCutoverBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "result": { - "$ref": "#/components/schemas/MovieCutoverResult", - "description": "the cutover outcome, recovery paths, snapshot count and primary path plan" - } - }, - "required": [ - "result" - ], - "type": "object" - }, - "MovieCutoverCandidate": { - "additionalProperties": false, - "properties": { - "buckets": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "entries": { - "items": { - "$ref": "#/components/schemas/LegacyMovieEntry" - }, - "type": [ - "array", - "null" - ] - }, - "recovery_cutover_id": { - "format": "int64", - "type": "integer" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "unresolved_paths": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "tmdb_id", - "buckets", - "entries" - ], - "type": "object" - }, - "MovieCutoverEvidence": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MovieCutoverEvidence.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "entries": { - "items": { - "$ref": "#/components/schemas/LegacyMovieEntry" - }, - "type": [ - "array", - "null" - ] - }, - "latest": { - "$ref": "#/components/schemas/MovieCutoverResult" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "entries" - ], - "type": "object" - }, - "MovieCutoverPendingBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/MovieCutoverPendingBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "candidates": { - "description": "legacy entries and unfinished recovery cutovers grouped by movie", - "items": { - "$ref": "#/components/schemas/MovieCutoverCandidate" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "candidates" - ], - "type": "object" - }, - "MovieCutoverResult": { - "additionalProperties": false, - "properties": { - "cutover_id": { - "format": "int64", - "type": "integer" - }, - "detail": { - "type": "string" - }, - "entries": { - "items": { - "$ref": "#/components/schemas/LegacyMovieEntry" - }, - "type": [ - "array", - "null" - ] - }, - "primary_moves": { - "items": { - "$ref": "#/components/schemas/PrimaryMove" - }, - "type": [ - "array", - "null" - ] - }, - "status": { - "type": "string" - }, - "step": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "unresolved_paths": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "watch_items": { - "format": "int64", - "type": "integer" - }, - "watch_snapshot": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "cutover_id", - "tmdb_id", - "status", - "step", - "watch_items", - "watch_snapshot" - ], - "type": "object" - }, - "NotifyLogEntry": { - "additionalProperties": false, - "properties": { - "event": { - "description": "media_arrived | media_stalled | media_regrab | media_import_issue", - "type": "string" - }, - "hint": { - "description": "context recorded at send time (stall hint, or issue state + sanitized reason folded with arr/episodes/age)", - "type": "string" - }, - "media_type": { - "type": "string" - }, - "owner": { - "description": "the bucket owner (requester) at send time", - "type": "string" - }, - "sent_at": { - "type": "string" - }, - "stage": { - "description": "0 arrived; 1 first-stall; 2 incomplete-followup; regrab attempt / issue step otherwise", - "format": "int64", - "type": "integer" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "event", - "stage", - "sent_at" - ], - "type": "object" - }, - "NotifyRecentOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/NotifyRecentOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "entries": { - "items": { - "$ref": "#/components/schemas/NotifyLogEntry" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "entries" - ], - "type": "object" - }, - "Observation": { - "additionalProperties": false, - "properties": { - "attempted_at": { - "description": "Completion time of the latest observation attempt, successful or failed", - "format": "date-time", - "type": "string" - }, - "error": { - "description": "Latest observation failure; retained findings are not a new successful observation", - "type": "string" - }, - "freshness": { - "description": "Unknown before a valid sample; stale only after the freshness threshold", - "enum": [ - "unknown", - "fresh", - "stale" - ], - "type": "string" - } - }, - "required": [ - "freshness" - ], - "type": "object" - }, - "OpenLink": { - "additionalProperties": false, - "properties": { - "detail": { - "type": "string" - }, - "state": { - "enum": [ - "observed", - "missing", - "unavailable", - "unconfigured", - "not_permitted" - ], - "type": "string" - }, - "url": { - "description": "The viewer's own Jellyfin item page; the person signs in to Jellyfin as themself", - "type": "string" - } - }, - "required": [ - "state" - ], - "type": "object" - }, - "Operator": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/Operator.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "capabilities": { - "$ref": "#/components/schemas/OperatorCapabilities" - }, - "name": { - "type": "string" - }, - "recommendation_profile": { - "type": "string" - }, - "requester": { - "type": "string" - }, - "role": { - "enum": [ - "admin", - "household" - ], - "type": "string" - }, - "subject": { - "type": "string" - }, - "watch_user": { - "type": "string" - } - }, - "required": [ - "subject", - "name", - "role", - "requester", - "watch_user", - "recommendation_profile", - "capabilities" - ], - "type": "object" - }, - "OperatorCapabilities": { - "additionalProperties": false, - "properties": { - "administer": { - "type": "boolean" - }, - "manage_library": { - "type": "boolean" - }, - "manage_memberships": { - "type": "boolean" - }, - "recommendation_profiles": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "request_media": { - "type": "boolean" - }, - "requesters": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "watch_users": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "administer", - "manage_library", - "manage_memberships", - "request_media", - "watch_users", - "recommendation_profiles", - "requesters" - ], - "type": "object" - }, - "Option": { - "additionalProperties": false, - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "logo_url": { - "description": "Validated TMDB CDN logo for a watch provider; absent for genres and when upstream has no safe logo", - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "name" - ], - "type": "object" - }, - "Options": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/Options.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "genres": { - "items": { - "$ref": "#/components/schemas/Option" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "region": { - "description": "Comma-separated configured catalogue regions", - "type": "string" - }, - "services": { - "description": "Curated streaming providers observed in their configured regions, not title availability or playback evidence", - "items": { - "$ref": "#/components/schemas/Option" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "observed", - "partial", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "state", - "media_type", - "region", - "genres", - "services", - "observations" - ], - "type": "object" - }, - "OwnerCount": { - "additionalProperties": false, - "properties": { - "count": { - "format": "int64", - "type": "integer" - }, - "owner": { - "type": "string" - } - }, - "required": [ - "owner", - "count" - ], - "type": "object" - }, - "Page": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/Page.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "discovery": { - "$ref": "#/components/schemas/Discovery", - "description": "General discovery context and upstream page observation, independent of local holding freshness" - }, - "items": { - "items": { - "$ref": "#/components/schemas/Title" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "all", - "movie", - "tv" - ], - "type": "string" - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "page": { - "format": "int64", - "type": "integer" - }, - "query": { - "type": "string" - }, - "source": { - "enum": [ - "search", - "general", - "related" - ], - "type": "string" - }, - "state": { - "enum": [ - "observed", - "partial" - ], - "type": "string" - }, - "total_pages": { - "description": "Known pages; merged regional service lanes expose only the materialized prefix until exhaustion", - "format": "int64", - "type": "integer" - }, - "total_results": { - "description": "Upstream matches before type filtering, or the known deduplicated prefix for merged regional service lanes", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "state", - "source", - "query", - "media_type", - "page", - "total_pages", - "total_results", - "items", - "observations" - ], - "type": "object" - }, - "PayloadSanitizeBlockView": { - "additionalProperties": false, - "properties": { - "dav_dir": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "from_name": { - "type": "string" - }, - "recorded_at": { - "type": "string" - }, - "to_name": { - "type": "string" - } - }, - "required": [ - "dav_dir", - "from_name", - "to_name", - "detail", - "recorded_at" - ], - "type": "object" - }, - "PerformerDetail": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/PerformerDetail.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "also_known_as": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "biography": { - "type": "string" - }, - "birthday": { - "type": "string" - }, - "deathday": { - "type": "string" - }, - "known_for_department": { - "type": "string" - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "name": { - "type": "string" - }, - "performer_id": { - "format": "int64", - "type": "integer" - }, - "place_of_birth": { - "type": "string" - }, - "profile_url": { - "type": "string" - }, - "source": { - "enum": [ - "seerr" - ], - "type": "string" - } - }, - "required": [ - "performer_id", - "metadata_state", - "source" - ], - "type": "object" - }, - "Person": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "name": { - "description": "Jellyfin username the household member is known by", - "type": "string" - } - }, - "required": [ - "key", - "name" - ], - "type": "object" - }, - "PrimaryMove": { - "additionalProperties": false, - "properties": { - "bucket": { - "type": "string" - }, - "new_path": { - "type": "string" - }, - "old_path": { - "type": "string" - } - }, - "required": [ - "bucket", - "old_path", - "new_path" - ], - "type": "object" - }, - "ProvenanceItem": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "media_type": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "source": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "title", - "owner", - "arr", - "source" - ], - "type": "object" - }, - "Provider": { - "additionalProperties": false, - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "logo_url": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "name" - ], - "type": "object" - }, - "RailStats": { - "additionalProperties": false, - "properties": { - "failed": { - "format": "int64", - "type": "integer" - }, - "last_import": { - "type": "string" - }, - "missing": { - "format": "int64", - "type": "integer" - }, - "queue": { - "format": "int64", - "type": "integer" - }, - "size_bytes": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "size_bytes", - "queue", - "missing", - "failed" - ], - "type": "object" - }, - "Rating": { - "additionalProperties": false, - "properties": { - "score": { - "description": "RT is 0โ€“100; IMDb and TMDB are 0โ€“10. Absent is never zero.", - "format": "double", - "type": "number" - }, - "source": { - "enum": [ - "rt_critics", - "rt_audience", - "imdb", - "tmdb" - ], - "type": "string" - }, - "state": { - "enum": [ - "observed", - "unrated", - "missing", - "unavailable" - ], - "type": "string" - }, - "status": { - "description": "Observed RT critics or audience label, never inferred from a percentage.", - "type": "string" - }, - "url": { - "type": "string" - }, - "votes": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "source", - "state" - ], - "type": "object" - }, - "ReadProfile": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "key", - "name" - ], - "type": "object" - }, - "ReadinessStage": { - "additionalProperties": false, - "properties": { - "name": { - "enum": [ - "request", - "acquisition", - "visibility", - "playback" - ], - "type": "string" - }, - "observed_at": { - "description": "When the evidence was last observed (RFC 3339 UTC), when the owner records it", - "type": "string" - }, - "source": { - "description": "The evidence owner this stage reads; it never stands in for another stage", - "type": "string" - }, - "state": { - "enum": [ - "done", - "partial", - "in_progress", - "none", - "unknown", - "stale", - "conflicting", - "failed" - ], - "type": "string" - }, - "summary": { - "description": "What this stage's own evidence says, including why it is unknown, stale, conflicting or failed", - "type": "string" - } - }, - "required": [ - "name", - "state", - "summary", - "source" - ], - "type": "object" - }, - "RecentOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecentOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "entries": { - "items": { - "$ref": "#/components/schemas/DispatchLogEntry" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "entries" - ], - "type": "object" - }, - "RecommendationCoverage": { - "additionalProperties": false, - "properties": { - "complete": { - "description": "Successful ranked snapshot and all active predicates observed for its scoped candidates; not freshness, catalogue completeness or playback proof", - "type": "boolean" - }, - "excluded": { - "format": "int64", - "type": "integer" - }, - "matched": { - "format": "int64", - "type": "integer" - }, - "total": { - "format": "int64", - "type": "integer" - }, - "unresolved": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "total", - "matched", - "excluded", - "unresolved", - "complete" - ], - "type": "object" - }, - "RecommendationDismissal": { - "additionalProperties": false, - "properties": { - "hidden_at": { - "format": "date-time", - "type": "string" - }, - "hidden_by": { - "description": "Household requester key of the operator who hid it", - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "poster_path": { - "type": "string" - }, - "title": { - "description": "Display name captured when hidden; empty when it was unknown", - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "media_type", - "tmdb_id", - "title", - "year", - "poster_path", - "hidden_by", - "hidden_at" - ], - "type": "object" - }, - "RecommendationDismissals": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationDismissals.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "dismissals": { - "items": { - "$ref": "#/components/schemas/RecommendationDismissal" - }, - "type": [ - "array", - "null" - ] - }, - "profile": { - "type": "string" - } - }, - "required": [ - "profile", - "dismissals" - ], - "type": "object" - }, - "RecommendationFact": { - "additionalProperties": false, - "properties": { - "genre_source": { - "description": "Ranked genre context takes precedence; metadata is a fallback only when ranked genres are unknown", - "enum": [ - "ranked", - "metadata", - "unknown" - ], - "type": "string" - }, - "match": { - "enum": [ - "matched", - "excluded", - "unresolved" - ], - "type": "string" - }, - "metadata": { - "$ref": "#/components/schemas/TitleMetadata" - } - }, - "required": [ - "metadata", - "match", - "genre_source" - ], - "type": "object" - }, - "RecommendationFeed": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationFeed.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "attempted_at": { - "format": "date-time", - "type": "string" - }, - "expires_at": { - "description": "observed_at plus the configured feed interval; expiry only, not a scheduled refresh or retry", - "format": "date-time", - "type": "string" - }, - "failure": { - "enum": [ - "profile_resolution", - "memberships", - "history", - "candidates", - "cycle" - ], - "type": "string" - }, - "materialization": { - "enum": [ - "unknown", - "pending", - "current", - "failed" - ], - "type": "string" - }, - "movies": { - "items": { - "$ref": "#/components/schemas/Candidate" - }, - "type": [ - "array", - "null" - ] - }, - "observed_at": { - "format": "date-time", - "type": "string" - }, - "outcome": { - "enum": [ - "unknown", - "ranked", - "no_seeds", - "empty" - ], - "type": "string" - }, - "profile": { - "$ref": "#/components/schemas/ReadProfile" - }, - "projection": { - "$ref": "#/components/schemas/RecommendationProjection" - }, - "shows": { - "items": { - "$ref": "#/components/schemas/Candidate" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "current", - "stale", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "projection", - "profile", - "state", - "outcome", - "materialization", - "movies", - "shows" - ], - "type": "object" - }, - "RecommendationHideInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationHideInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "poster_path": { - "maxLength": 200, - "type": "string" - }, - "title": { - "description": "Display hint, used only when the retained feed no longer carries the title", - "maxLength": 300, - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "minimum": 1, - "type": "integer" - }, - "year": { - "format": "int64", - "maximum": 9999, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "media_type", - "tmdb_id" - ], - "type": "object" - }, - "RecommendationHideResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationHideResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "created": { - "description": "False when the title was already hidden", - "type": "boolean" - }, - "dismissal": { - "$ref": "#/components/schemas/RecommendationDismissal" - }, - "materialization": { - "description": "Jellyfin recommendation folders after the hide; failed leaves the dismissal in force until the next cycle converges them", - "enum": [ - "current", - "failed", - "unchanged" - ], - "type": "string" - }, - "profile": { - "type": "string" - } - }, - "required": [ - "profile", - "dismissal", - "created", - "materialization" - ], - "type": "object" - }, - "RecommendationProfiles": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationProfiles.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "default_profile": { - "type": "string" - }, - "profiles": { - "items": { - "$ref": "#/components/schemas/ReadProfile" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "profiles", - "default_profile" - ], - "type": "object" - }, - "RecommendationProjection": { - "additionalProperties": false, - "properties": { - "coverage": { - "$ref": "#/components/schemas/RecommendationCoverage" - }, - "facts": { - "description": "Scoped candidates in original movie rank followed by original series rank; unknown observations never prove exclusion", - "items": { - "$ref": "#/components/schemas/RecommendationFact" - }, - "type": [ - "array", - "null" - ] - }, - "genre_id": { - "format": "int64", - "type": "integer" - }, - "media_type": { - "enum": [ - "all", - "movie", - "tv" - ], - "type": "string" - }, - "service_id": { - "format": "int64", - "type": "integer" - }, - "unresolved": { - "description": "Possible matches with unknown active predicates, in original per-media relative order", - "items": { - "$ref": "#/components/schemas/Candidate" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "media_type", - "genre_id", - "service_id", - "coverage", - "facts", - "unresolved" - ], - "type": "object" - }, - "RecommendationRestoreResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationRestoreResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "profile": { - "type": "string" - }, - "restored": { - "description": "False when the title was not hidden", - "type": "boolean" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "profile", - "media_type", - "tmdb_id", - "restored" - ], - "type": "object" - }, - "RecommendationSyncResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RecommendationSyncResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "state": { - "enum": [ - "healthy", - "degraded", - "unhealthy" - ], - "type": "string" - }, - "status": { - "description": "synced", - "type": "string" - } - }, - "required": [ - "status", - "state" - ], - "type": "object" - }, - "RedispatchInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RedispatchInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "items": { - "description": "items to re-dispatch", - "items": { - "$ref": "#/components/schemas/RedispatchItem" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "RedispatchItem": { - "additionalProperties": false, - "properties": { - "media_type": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type" - ], - "type": "object" - }, - "RedispatchOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RedispatchOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "results": { - "items": { - "$ref": "#/components/schemas/RedispatchResult" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "results" - ], - "type": "object" - }, - "RedispatchResult": { - "additionalProperties": false, - "properties": { - "media_type": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "status": { - "description": "ok | partial | failed | skipped", - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "status" - ], - "type": "object" - }, - "RegrabRequest": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RegrabRequest.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "episode_ids": { - "description": "arr episode ids to regrab (tv)", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "episode_keys": { - "description": "SxxEyy keys to regrab (tv)", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "season": { - "description": "regrab a whole season (tv)", - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, - "RegrabResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RegrabResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "blocklisted": { - "format": "int64", - "type": "integer" - }, - "cleared": { - "format": "int64", - "type": "integer" - }, - "detail": { - "type": "string" - }, - "searched": { - "format": "int64", - "type": "integer" - }, - "status": { - "description": "ok | busy | superseded | missing_in_arr | invalid | arr_error", - "type": "string" - } - }, - "required": [ - "status", - "blocklisted", - "cleared", - "searched" - ], - "type": "object" - }, - "RemovalLink": { - "additionalProperties": false, - "properties": { - "bucket": { - "type": "string" - }, - "orphan": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "bucket", - "path", - "target", - "orphan" - ], - "type": "object" - }, - "RemoveFromQueueInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RemoveFromQueueInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "reason": { - "description": "required non-empty audited queue-removal reason", - "type": "string" - }, - "then": { - "description": "reopen for a new grab, or resolve an already-imported/duplicate issue", - "enum": [ - "reopen", - "resolve" - ], - "type": "string" - } - }, - "required": [ - "reason", - "then" - ], - "type": "object" - }, - "RenameHistoryOutputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RenameHistoryOutputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "passes": { - "items": { - "$ref": "#/components/schemas/RenamePass" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "passes" - ], - "type": "object" - }, - "RenamePass": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "ended_at": { - "type": "string" - }, - "files": { - "format": "int64", - "type": "integer" - }, - "pass_id": { - "format": "int64", - "type": "integer" - }, - "plan_json": { - "type": "string" - }, - "repointed": { - "format": "int64", - "type": "integer" - }, - "request_id": { - "description": "Opaque originating request identity, retained in the durable plan; absent for legacy or uncorrelated callers. Correlation only, not idempotency.", - "type": "string" - }, - "series_id": { - "format": "int64", - "type": "integer" - }, - "started_at": { - "type": "string" - }, - "status": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "watch_items": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "pass_id", - "tmdb_id", - "arr", - "series_id", - "title", - "status", - "plan_json", - "files", - "watch_items", - "repointed", - "detail", - "started_at", - "ended_at" - ], - "type": "object" - }, - "RenamePlan": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "files": { - "items": { - "$ref": "#/components/schemas/RenamePlanFile" - }, - "type": [ - "array", - "null" - ] - }, - "series_id": { - "format": "int64", - "type": "integer" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "arr", - "series_id", - "title", - "files" - ], - "type": "object" - }, - "RenamePlanBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RenamePlanBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "plan": { - "$ref": "#/components/schemas/RenamePlan", - "description": "the holding Sonarr, the series, and each pending file's existing and new path" - } - }, - "required": [ - "plan" - ], - "type": "object" - }, - "RenamePlanFile": { - "additionalProperties": false, - "properties": { - "episode_file_id": { - "format": "int64", - "type": "integer" - }, - "episode_numbers": { - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "existing_path": { - "type": "string" - }, - "new_path": { - "type": "string" - }, - "season_number": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "episode_file_id", - "season_number", - "episode_numbers", - "existing_path", - "new_path" - ], - "type": "object" - }, - "RenameResult": { - "additionalProperties": false, - "properties": { - "converged": { - "type": "boolean" - }, - "detail": { - "type": "string" - }, - "pass_id": { - "format": "int64", - "type": "integer" - }, - "plan": { - "$ref": "#/components/schemas/RenamePlan" - }, - "renamed": { - "format": "int64", - "type": "integer" - }, - "repointed": { - "items": { - "$ref": "#/components/schemas/RepointedLink" - }, - "type": [ - "array", - "null" - ] - }, - "skipped": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "status": { - "type": "string" - }, - "watch_items": { - "format": "int64", - "type": "integer" - }, - "watch_snapshot": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "pass_id", - "plan", - "status", - "renamed", - "watch_items", - "watch_snapshot", - "converged" - ], - "type": "object" - }, - "RenameResultBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RenameResultBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "result": { - "$ref": "#/components/schemas/RenameResult", - "description": "the pass outcome with the plan it ran, the watch-state counts and any repointed layer links" - } - }, - "required": [ - "result" - ], - "type": "object" - }, - "ReplacementHoldReleaseResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ReplacementHoldReleaseResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "released": { - "type": "boolean" - } - }, - "required": [ - "released" - ], - "type": "object" - }, - "ReplacementHoldView": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "created_at": { - "format": "date-time", - "type": "string" - }, - "expires_at": { - "format": "date-time", - "type": "string" - }, - "reason": { - "type": "string" - }, - "record_id": { - "format": "int64", - "type": "integer" - }, - "record_type": { - "type": "string" - }, - "requested_by": { - "type": "string" - } - }, - "required": [ - "arr", - "record_type", - "record_id", - "requested_by", - "reason", - "created_at", - "expires_at" - ], - "type": "object" - }, - "ReplacementHoldsResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ReplacementHoldsResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "holds": { - "items": { - "$ref": "#/components/schemas/ReplacementHoldView" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "holds" - ], - "type": "object" - }, - "RepointedLink": { - "additionalProperties": false, - "properties": { - "bucket": { - "type": "string" - }, - "dir": { - "type": "string" - }, - "new_name": { - "type": "string" - }, - "new_target": { - "type": "string" - }, - "old_name": { - "type": "string" - }, - "old_target": { - "type": "string" - } - }, - "required": [ - "bucket", - "dir", - "old_name", - "new_name", - "old_target", - "new_target" - ], - "type": "object" - }, - "RequestIntentBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RequestIntentBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "recommendation_profile": { - "type": "string" - }, - "requester": { - "description": "Household requester key to attribute the request to; defaults to the signed-in operator's own requester and must be one of the operator's capability requesters", - "type": "string" - }, - "seasons": { - "description": "Television only: the explicit seasons wanted (0 is specials). An empty selection is refused, never read as all seasons. Omit for a movie", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "source": { - "default": "title", - "description": "Recommendation requires recommendation_profile and settles that profile's own acceptance only when it is also the requester", - "enum": [ - "title", - "recommendation" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "media_type", - "tmdb_id" - ], - "type": "object" - }, - "RequestProgress": { - "additionalProperties": false, - "properties": { - "actor": { - "$ref": "#/components/schemas/Actor", - "description": "Who filed it, as recorded; absent when the request was filed outside the cockpit" - }, - "awaiting_approval": { - "description": "The request is filed in Seerr and nothing has been dispatched, imported or observed yet, so no acquisition has started. Absent otherwise", - "type": "boolean" - }, - "blocking_stage": { - "description": "First stage without done evidence. Absent only when every stage has it. Not a readiness claim", - "enum": [ - "request", - "acquisition", - "visibility", - "playback" - ], - "type": "string" - }, - "filed_by_you": { - "description": "The signed-in operator filed this command", - "type": "boolean" - }, - "id": { - "description": "request-N for a cockpit command, dispatch-N for a request Seerr approved outside the cockpit", - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "on_behalf": { - "description": "The actor's own requester differs from the attributed requester", - "type": "boolean" - }, - "requested_at": { - "type": "string" - }, - "requester": { - "$ref": "#/components/schemas/Person", - "description": "The household member the request is attributed to; this decides which feed the item belongs to" - }, - "seasons": { - "description": "Television seasons explicitly requested; empty for a movie or when not recorded", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "stages": { - "items": { - "$ref": "#/components/schemas/ReadinessStage" - }, - "type": [ - "array", - "null" - ] - }, - "title": { - "description": "Known from the dispatch log or library membership; empty when no local record names the title yet", - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "id", - "tmdb_id", - "media_type", - "requester", - "on_behalf", - "filed_by_you", - "seasons", - "stages" - ], - "type": "object" - }, - "RequestReceipt": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RequestReceipt.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "actor": { - "$ref": "#/components/schemas/RequestReceiptActor", - "description": "Authenticated operator who issued the command; separate from the requester" - }, - "attempts": { - "format": "int64", - "type": "integer" - }, - "coverage": { - "description": "One outcome per selected season, never inferred from other seasons", - "items": { - "$ref": "#/components/schemas/RequestReceiptCoverage" - }, - "type": [ - "array", - "null" - ] - }, - "created_at": { - "type": "string" - }, - "detail": { - "description": "Why the command is pending or what was refused", - "type": "string" - }, - "id": { - "description": "Native request record; retrying the same command resumes this record", - "format": "int64", - "type": "integer" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "membership": { - "$ref": "#/components/schemas/RequestReceiptMembership" - }, - "on_behalf": { - "description": "The requester differs from the acting operator's own household requester", - "type": "boolean" - }, - "recommendation_profile": { - "type": "string" - }, - "requester": { - "description": "Household requester the request is attributed to", - "type": "string" - }, - "requester_username": { - "type": "string" - }, - "seasons": { - "description": "Explicit television seasons the operator asked for; empty for a movie", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "seerr": { - "$ref": "#/components/schemas/RequestReceiptSeerr" - }, - "source": { - "enum": [ - "title", - "recommendation" - ], - "type": "string" - }, - "state": { - "description": "Settled: every part has a verified outcome. Pending: a fact is unresolved. Retired: an administrator ended a stale-identity pending command without cancelling any Seerr request or reversing media effects", - "enum": [ - "settled", - "pending", - "retired" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "id", - "media_type", - "tmdb_id", - "state", - "requester", - "requester_username", - "actor", - "on_behalf", - "source", - "seasons", - "membership", - "seerr", - "coverage", - "attempts", - "created_at", - "updated_at" - ], - "type": "object" - }, - "RequestReceiptActor": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "requester": { - "description": "The acting operator's own household requester key", - "type": "string" - }, - "role": { - "enum": [ - "admin", - "household" - ], - "type": "string" - } - }, - "required": [ - "name", - "role", - "requester" - ], - "type": "object" - }, - "RequestReceiptCoverage": { - "additionalProperties": false, - "properties": { - "outcome": { - "enum": [ - "requested", - "already_requested", - "already_held", - "unresolved" - ], - "type": "string" - }, - "request_id": { - "format": "int64", - "type": "integer" - }, - "season": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "season", - "outcome" - ], - "type": "object" - }, - "RequestReceiptMembership": { - "additionalProperties": false, - "properties": { - "bucket": { - "description": "Library route the membership landed in", - "type": "string" - }, - "dispatch_log_id": { - "format": "int64", - "type": "integer" - }, - "status": { - "enum": [ - "added", - "already_owned", - "pending_dispatch", - "conflict", - "failed" - ], - "type": "string" - } - }, - "required": [ - "status" - ], - "type": "object" - }, - "RequestReceiptSeerr": { - "additionalProperties": false, - "properties": { - "outcome": { - "enum": [ - "requested", - "already_requested", - "not_needed", - "not_attempted", - "unresolved" - ], - "type": "string" - }, - "request_id": { - "description": "Seerr request attributed to the requester that covers this command", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "outcome" - ], - "type": "object" - }, - "RestartResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/RestartResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "kind": { - "description": "process | sibling", - "type": "string" - }, - "status": { - "description": "restarting | degraded", - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "target", - "kind", - "status" - ], - "type": "object" - }, - "ResumeBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ResumeBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/ResumeTitle" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "ResumeTitle": { - "additionalProperties": false, - "properties": { - "episode": { - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "progress": { - "format": "double", - "maximum": 1, - "minimum": 0, - "type": "number" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "title", - "progress" - ], - "type": "object" - }, - "Row": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "candidate_health": { - "type": "string" - }, - "candidate_key": { - "type": "string" - }, - "candidate_reason": { - "type": "string" - }, - "download_id": { - "type": "string" - }, - "episode": { - "type": "string" - }, - "episode_count": { - "format": "int64", - "type": "integer" - }, - "eta": { - "type": "string" - }, - "progress": { - "format": "double", - "type": "number" - }, - "protocol": { - "type": "string" - }, - "source": { - "type": "string" - }, - "state": { - "type": "string" - }, - "state_detail": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "title", - "protocol", - "progress", - "state", - "source" - ], - "type": "object" - }, - "Season": { - "additionalProperties": false, - "properties": { - "air_date": { - "type": "string" - }, - "coverage": { - "enum": [ - "none", - "partial", - "complete", - "unknown" - ], - "type": "string" - }, - "episode_count": { - "format": "int64", - "type": "integer" - }, - "name": { - "type": "string" - }, - "number": { - "format": "int64", - "type": "integer" - }, - "overview": { - "type": "string" - }, - "recorded_episodes": { - "format": "int64", - "type": "integer" - }, - "request_state": { - "enum": [ - "none", - "requested", - "unknown" - ], - "type": "string" - }, - "seerr_state": { - "enum": [ - "untracked", - "unknown", - "pending", - "processing", - "partial", - "available", - "blocklisted", - "deleted" - ], - "type": "string" - } - }, - "required": [ - "number", - "name", - "overview", - "episode_count", - "recorded_episodes", - "coverage", - "request_state", - "seerr_state" - ], - "type": "object" - }, - "SeasonCoverage": { - "additionalProperties": false, - "properties": { - "coverage": { - "enum": [ - "complete", - "partial", - "none", - "unknown" - ], - "type": "string" - }, - "season": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "season", - "coverage" - ], - "type": "object" - }, - "SeasonDetail": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/SeasonDetail.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "episodes": { - "items": { - "$ref": "#/components/schemas/Episode" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "tv" - ], - "type": "string" - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "observations": { - "items": { - "$ref": "#/components/schemas/LibraryObservation" - }, - "type": [ - "array", - "null" - ] - }, - "season": { - "$ref": "#/components/schemas/Season" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "metadata_state", - "season", - "episodes", - "observations" - ], - "type": "object" - }, - "SeasonGroup": { - "additionalProperties": false, - "properties": { - "episodes": { - "items": { - "$ref": "#/components/schemas/EpisodeRow" - }, - "type": [ - "array", - "null" - ] - }, - "season": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "season", - "episodes" - ], - "type": "object" - }, - "SeedContribution": { - "additionalProperties": false, - "properties": { - "added": { - "description": "A successful attributed addition contributed nonzero seed weight", - "type": "boolean" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "title": { - "description": "Observed display name; absent when unknown", - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "watched": { - "description": "A played history event contributed nonzero seed weight", - "type": "boolean" - } - }, - "required": [ - "watched", - "added", - "media_type", - "tmdb_id" - ], - "type": "object" - }, - "SeerrObservation": { - "additionalProperties": false, - "properties": { - "detail": { - "type": "string" - }, - "requests": { - "items": { - "$ref": "#/components/schemas/SeerrRequest" - }, - "type": [ - "array", - "null" - ] - }, - "state": { - "enum": [ - "observed", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "state", - "requests" - ], - "type": "object" - }, - "SeerrRequest": { - "additionalProperties": false, - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "status": { - "enum": [ - "pending", - "approved", - "declined", - "failed", - "completed", - "unknown" - ], - "type": "string" - } - }, - "required": [ - "id", - "status" - ], - "type": "object" - }, - "SelectEpisodesInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/SelectEpisodesInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "episode_ids": { - "description": "explicit arr episode ids to import (re-verified aired+monitored+belonging server-side)", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "episode_ids" - ], - "type": "object" - }, - "SourceDetail": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "imported": { - "description": "import timestamp (RFC3339)", - "type": "string" - }, - "indexer": { - "type": "string" - } - }, - "required": [ - "arr" - ], - "type": "object" - }, - "SourceUsage": { - "additionalProperties": false, - "properties": { - "item_count": { - "format": "int64", - "type": "integer" - }, - "source": { - "type": "string" - } - }, - "required": [ - "source", - "item_count" - ], - "type": "object" - }, - "SpottarrFreshnessView": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/SpottarrFreshnessView.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "age": { - "type": "string" - }, - "age_seconds": { - "format": "int64", - "type": "integer" - }, - "checked_at": { - "format": "date-time", - "type": "string" - }, - "count": { - "format": "int64", - "type": "integer" - }, - "detail": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "known": { - "type": "boolean" - }, - "newest": { - "format": "date-time", - "type": "string" - }, - "state": { - "enum": [ - "disabled", - "starting", - "healthy", - "degraded", - "unhealthy", - "stopped" - ], - "type": "string" - } - }, - "required": [ - "enabled", - "known", - "state" - ], - "type": "object" - }, - "Title": { - "additionalProperties": false, - "properties": { - "backdrop_url": { - "description": "Landscape artwork on the TMDB image CDN; absent when none is observed or the upstream path is not a CDN-relative path", - "type": "string" - }, - "genre_ids": { - "description": "TMDB genre IDs in this title's media-type namespace; absent is unobserved, empty is observed without genres", - "items": { - "format": "int64", - "type": "integer" - }, - "type": "array" - }, - "held": { - "enum": [ - "held", - "unheld", - "unknown" - ], - "type": "string" - }, - "libraries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "overview": { - "type": "string" - }, - "poster_url": { - "type": "string" - }, - "release_date": { - "type": "string" - }, - "request_state": { - "enum": [ - "none", - "requested", - "unknown" - ], - "type": "string" - }, - "seerr_state": { - "enum": [ - "untracked", - "unknown", - "pending", - "processing", - "partial", - "available", - "blocklisted", - "deleted" - ], - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "title", - "overview", - "held", - "request_state", - "seerr_state", - "libraries" - ], - "type": "object" - }, - "TitleCast": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TitleCast.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "credit_context": { - "description": "Seerr exposes only each TV performer's most frequent aggregate role, not every role or episode count", - "enum": [ - "movie-character", - "tv-leading-character" - ], - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/CastMember" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "metadata_state": { - "enum": [ - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "paging": { - "description": "One complete returned Seerr cast list, not invented upstream pages", - "enum": [ - "unpaged" - ], - "type": "string" - }, - "source": { - "enum": [ - "seerr" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "metadata_state", - "source", - "paging", - "credit_context", - "items" - ], - "type": "object" - }, - "TitleDiagnosis": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TitleDiagnosis.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "candidates": { - "items": { - "$ref": "#/components/schemas/TriageDetail" - }, - "type": [ - "array", - "null" - ] - }, - "dispatch": { - "items": { - "$ref": "#/components/schemas/DispatchLogEntry" - }, - "type": [ - "array", - "null" - ] - }, - "dispatch_observation": { - "$ref": "#/components/schemas/TitleDiagnosisObservation" - }, - "imports_observation": { - "$ref": "#/components/schemas/TitleDiagnosisObservation" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "missing": { - "items": { - "$ref": "#/components/schemas/MissingItem" - }, - "type": [ - "array", - "null" - ] - }, - "presence_observation": { - "$ref": "#/components/schemas/TitleDiagnosisObservation" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "candidates", - "dispatch", - "missing", - "imports_observation", - "dispatch_observation", - "presence_observation" - ], - "type": "object" - }, - "TitleDiagnosisObservation": { - "additionalProperties": false, - "properties": { - "detail": { - "type": "string" - }, - "state": { - "enum": [ - "observed", - "unavailable" - ], - "type": "string" - } - }, - "required": [ - "state" - ], - "type": "object" - }, - "TitleMediaHealth": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TitleMediaHealth.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "breaker": { - "$ref": "#/components/schemas/MediaHealthBreakerResult" - }, - "enabled": { - "type": "boolean" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "proposals": { - "items": { - "$ref": "#/components/schemas/TitleMediaHealthProposal" - }, - "type": [ - "array", - "null" - ] - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "enabled", - "breaker", - "proposals" - ], - "type": "object" - }, - "TitleMediaHealthFile": { - "additionalProperties": false, - "properties": { - "arr": { - "type": "string" - }, - "file_id": { - "format": "int64", - "type": "integer" - }, - "path": { - "type": "string" - }, - "record_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "arr", - "record_id", - "file_id", - "path" - ], - "type": "object" - }, - "TitleMediaHealthProposal": { - "additionalProperties": false, - "properties": { - "files": { - "items": { - "$ref": "#/components/schemas/TitleMediaHealthFile" - }, - "type": [ - "array", - "null" - ] - }, - "first_attempt_at": { - "format": "date-time", - "type": "string" - }, - "history": { - "items": { - "$ref": "#/components/schemas/MediaHealthRepairEvent" - }, - "type": [ - "array", - "null" - ] - }, - "proposal": { - "$ref": "#/components/schemas/MediaHealthProposalView" - }, - "usenet_attempts": { - "format": "int64", - "type": "integer" - }, - "verify_attempts": { - "description": "Recorded inconclusive verification retries; a successful first read does not increment this", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "files", - "usenet_attempts", - "verify_attempts", - "proposal", - "history" - ], - "type": "object" - }, - "TitleMetadata": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TitleMetadata.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "checked_at": { - "format": "date-time", - "type": "string" - }, - "genre_state": { - "enum": [ - "unobserved", - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "genres": { - "items": { - "$ref": "#/components/schemas/Genre" - }, - "type": [ - "array", - "null" - ] - }, - "genres_observed_at": { - "format": "date-time", - "type": "string" - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "provider_state": { - "enum": [ - "unobserved", - "observed", - "missing", - "unavailable" - ], - "type": "string" - }, - "providers": { - "items": { - "$ref": "#/components/schemas/Provider" - }, - "type": [ - "array", - "null" - ] - }, - "providers_observed_at": { - "format": "date-time", - "type": "string" - }, - "region": { - "enum": [ - "NL" - ], - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "tmdb_id", - "media_type", - "region", - "genre_state", - "provider_state", - "genres", - "providers" - ], - "type": "object" - }, - "TitleRequestsBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TitleRequestsBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "requests": { - "items": { - "$ref": "#/components/schemas/RequestReceipt" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "requests" - ], - "type": "object" - }, - "TitleVersion": { - "additionalProperties": false, - "properties": { - "file": { - "$ref": "#/components/schemas/FileDetail" - }, - "role": { - "enum": [ - "primary", - "mirror" - ], - "type": "string" - }, - "seasons": { - "items": { - "$ref": "#/components/schemas/SeasonGroup" - }, - "type": [ - "array", - "null" - ] - }, - "source": { - "type": "string" - } - }, - "required": [ - "source", - "role" - ], - "type": "object" - }, - "TorrentArmInputBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TorrentArmInputBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "acknowledge_trip": { - "description": "The owner acknowledged the trip reason; required to re-arm a tripped lane", - "type": "boolean" - }, - "revision": { - "description": "Plan revision the arm confirm listed", - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "revision" - ], - "type": "object" - }, - "TorrentArmingFloor": { - "additionalProperties": false, - "properties": { - "dead_file": { - "description": "At least one acceptable would-push for a dead-file proposal", - "type": "boolean" - }, - "missing": { - "description": "At least one acceptable would-push for a missing record", - "type": "boolean" - } - }, - "required": [ - "dead_file", - "missing" - ], - "type": "object" - }, - "TorrentArrRejectionCount": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Number of releases with this rejection", - "format": "int64", - "type": "integer" - }, - "rejection": { - "description": "Unsupported rejection returned by the Arr", - "type": "string" - } - }, - "required": [ - "rejection", - "count" - ], - "type": "object" - }, - "TorrentCandidateView": { - "additionalProperties": false, - "properties": { - "arr_kind": { - "description": "Arr kind holding the record", - "enum": [ - "sonarr", - "radarr" - ], - "type": "string" - }, - "arr_name": { - "description": "Primary Arr instance holding the record", - "type": "string" - }, - "attempts": { - "description": "Failed torrent attempts spent in the record's current qualification window", - "format": "int64", - "type": "integer" - }, - "attempts_per_window": { - "description": "Failed attempts one qualification window allows", - "format": "int64", - "type": "integer" - }, - "available_at": { - "description": "When the record became available (air date, or Radarr availability)", - "format": "date-time", - "type": "string" - }, - "detail": { - "description": "Human-readable state of the clock, or the outcome that set the record's budget stage", - "type": "string" - }, - "episode_key": { - "description": "SxxEyy for an episode, empty for a movie", - "type": "string" - }, - "kind": { - "description": "dead_file: a repair proposal's record; missing: an Arr missing monitored record", - "enum": [ - "dead_file", - "missing" - ], - "type": "string" - }, - "last_search_at": { - "description": "The lane's latest own usenet search", - "format": "date-time", - "type": "string" - }, - "last_search_outcome": { - "description": "Outcome of the lane's latest own usenet search", - "enum": [ - "silent", - "found", - "transport", - "error" - ], - "type": "string" - }, - "ledger": { - "description": "Ledger tracking the record's attempts: the repair proposal row, or import_candidates (source torrent)", - "enum": [ - "repair", - "import" - ], - "type": "string" - }, - "new_silent_days": { - "description": "Answered-silent days credited since the clock (or the seed) started", - "format": "int64", - "type": "integer" - }, - "next_at": { - "description": "Earliest a waiting, retry-due or resting record can start again, assuming one answered-silent day per day", - "format": "date-time", - "type": "string" - }, - "paused": { - "description": "A usenet grab or repair is in flight; the silence count is paused", - "type": "boolean" - }, - "proposal_id": { - "description": "Repair proposal for a dead_file record", - "format": "int64", - "type": "integer" - }, - "qualified": { - "description": "Enough silence days, and a seeded record has at least one new one", - "type": "boolean" - }, - "record_id": { - "description": "Arr episode or movie id; the clock and budget belong to this record", - "format": "int64", - "type": "integer" - }, - "record_type": { - "description": "Arr record type", - "enum": [ - "episode", - "movie" - ], - "type": "string" - }, - "searching_since": { - "description": "The conductarr-owned usenet-searching-since stamp; the clock start", - "format": "date-time", - "type": "string" - }, - "seeded": { - "description": "The count was seeded from history at the lane's first pass", - "type": "boolean" - }, - "silence_days": { - "description": "Answered-silent days, seeded proxy days included", - "format": "int64", - "type": "integer" - }, - "silence_needed": { - "description": "Silence days that qualify a record", - "format": "int64", - "type": "integer" - }, - "stage": { - "description": "Lane stage: silent while the record accrues answered-silent days; waiting after a start that spent no attempt (no acceptable release, or a magnet with no peers); retry due after a failed attempt with attempts left; resting once the window's attempts are spent", - "enum": [ - "silent", - "waiting", - "retry due", - "resting" - ], - "type": "string" - }, - "title": { - "description": "Series or movie title", - "type": "string" - }, - "tmdb_id": { - "description": "TMDB id of the series or movie", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "arr_kind", - "arr_name", - "record_type", - "record_id", - "tmdb_id", - "title", - "kind", - "ledger", - "stage", - "paused", - "silence_days", - "silence_needed", - "seeded", - "new_silent_days", - "qualified", - "available_at", - "searching_since", - "attempts", - "attempts_per_window", - "detail" - ], - "type": "object" - }, - "TorrentCandidatesResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TorrentCandidatesResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "candidates": { - "description": "Dead files first, then missing records, longest silence first", - "items": { - "$ref": "#/components/schemas/TorrentCandidateView" - }, - "type": [ - "array", - "null" - ] - }, - "qualified": { - "description": "How many rows qualify", - "format": "int64", - "type": "integer" - }, - "silence_needed": { - "description": "Silence days that qualify a record", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "silence_needed", - "qualified", - "candidates" - ], - "type": "object" - }, - "TorrentCoverage": { - "additionalProperties": false, - "properties": { - "anime": { - "$ref": "#/components/schemas/TorrentCoverageSlice", - "description": "Records on an anime Arr" - }, - "dead_file": { - "$ref": "#/components/schemas/TorrentCoverageSlice", - "description": "Dead-file proposal records" - }, - "missing": { - "$ref": "#/components/schemas/TorrentCoverageSlice", - "description": "Missing monitored records" - }, - "non_anime": { - "$ref": "#/components/schemas/TorrentCoverageSlice", - "description": "Records on a non-anime Arr" - } - }, - "required": [ - "dead_file", - "missing", - "anime", - "non_anime" - ], - "type": "object" - }, - "TorrentCoverageSlice": { - "additionalProperties": false, - "properties": { - "acceptable": { - "description": "Of those, rows holding an acceptable release (a would-push)", - "format": "int64", - "type": "integer" - }, - "dry_runs": { - "description": "Plan rows whose latest torrent search ran", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "dry_runs", - "acceptable" - ], - "type": "object" - }, - "TorrentDiskProjection": { - "additionalProperties": false, - "properties": { - "bytes": { - "description": "Summed size of those rows' would-push releases", - "format": "int64", - "type": "integer" - }, - "floor_bytes": { - "description": "Configured free-space floor below which no start runs", - "format": "int64", - "type": "integer" - }, - "free_bytes": { - "description": "Free space on the /data/import dataset when the plan was built", - "format": "int64", - "type": "integer" - }, - "starts": { - "description": "Plan rows counted: the first ones, up to the rolling start cap", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "starts", - "bytes", - "free_bytes", - "floor_bytes" - ], - "type": "object" - }, - "TorrentHandoff": { - "additionalProperties": false, - "properties": { - "arr_gate_drops": { - "items": { - "$ref": "#/components/schemas/ArrGateDrop" - }, - "type": [ - "array", - "null" - ] - }, - "candidate": { - "$ref": "#/components/schemas/TorrentCandidateView" - }, - "finished_at": { - "format": "date-time", - "type": "string" - }, - "note": { - "type": "string" - }, - "outcome": { - "type": "string" - }, - "started_at": { - "format": "date-time", - "type": "string" - } - }, - "required": [ - "started_at", - "finished_at", - "candidate", - "outcome", - "note" - ], - "type": "object" - }, - "TorrentHistoryResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TorrentHistoryResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "events": { - "items": { - "$ref": "#/components/schemas/TorrentLaneEvent" - }, - "type": [ - "array", - "null" - ] - }, - "handoffs": { - "items": { - "$ref": "#/components/schemas/TorrentHandoff" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "handoffs", - "events" - ], - "type": "object" - }, - "TorrentIndexerDrops": { - "additionalProperties": false, - "properties": { - "arr_rejections": { - "description": "Distinct unsupported Arr rejection strings and their release counts", - "items": { - "$ref": "#/components/schemas/TorrentArrRejectionCount" - }, - "type": [ - "array", - "null" - ] - }, - "coverage": { - "description": "Maps an episode that is not a monitored, qualified record without a usenet file", - "format": "int64", - "type": "integer" - }, - "gate": { - "description": "Refused by the Arr for any reason but ProtocolDisabled (or, for a dead file, the existing file)", - "format": "int64", - "type": "integer" - }, - "indexer": { - "description": "Indexer display name as the Arr reports it", - "type": "string" - }, - "language": { - "description": "Language: Not Original on a non-anime Arr, or dub-only on an anime Arr", - "format": "int64", - "type": "integer" - }, - "seeders": { - "description": "Fewer seeders than the minimum, or an unknown count", - "format": "int64", - "type": "integer" - }, - "tried": { - "description": "GUID or infohash already tried for a covered record, unprovable, or in the release-corpse ledger", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "indexer", - "gate", - "language", - "seeders", - "coverage", - "tried" - ], - "type": "object" - }, - "TorrentLaneEvent": { - "additionalProperties": false, - "properties": { - "at": { - "format": "date-time", - "type": "string" - }, - "detail": { - "type": "string" - }, - "kind": { - "description": "arm and disarm by the owner, a breaker trip, or unrecorded torrents removed at a qBittorrent child start", - "enum": [ - "arm", - "disarm", - "trip", - "unrecorded_removed" - ], - "type": "string" - } - }, - "required": [ - "at", - "kind", - "detail" - ], - "type": "object" - }, - "TorrentPick": { - "additionalProperties": false, - "properties": { - "guid": { - "type": "string" - }, - "indexer_id": { - "format": "int64", - "type": "integer" - }, - "release": { - "type": "string" - }, - "seeders": { - "format": "int64", - "type": "integer" - }, - "size": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "release", - "indexer_id", - "seeders", - "size", - "guid" - ], - "type": "object" - }, - "TorrentPlanResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TorrentPlanResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "dry_run_at": { - "description": "When the shadow plan was produced", - "format": "date-time", - "type": "string" - }, - "next_rotation": { - "description": "Next time the lane may start a handoff", - "format": "date-time", - "type": "string" - }, - "revision": { - "description": "Monotonic plan revision", - "format": "int64", - "type": "integer" - }, - "rows": { - "description": "Current handoff decisions; no row causes a grab", - "items": { - "$ref": "#/components/schemas/TorrentPlanRow" - }, - "type": [ - "array", - "null" - ] - } - }, - "required": [ - "revision", - "dry_run_at", - "rows" - ], - "type": "object" - }, - "TorrentPlanRow": { - "additionalProperties": false, - "properties": { - "arr_gate_drops": { - "description": "Rejections from each release refused by the Arr gate", - "items": { - "$ref": "#/components/schemas/ArrGateDrop" - }, - "type": [ - "array", - "null" - ] - }, - "candidate": { - "$ref": "#/components/schemas/TorrentCandidateView" - }, - "note": { - "type": "string" - }, - "outcome": { - "type": "string" - }, - "pick": { - "$ref": "#/components/schemas/TorrentPick" - }, - "usenet_check": { - "type": "string" - } - }, - "required": [ - "candidate", - "usenet_check", - "outcome", - "note" - ], - "type": "object" - }, - "TorrentRotation": { - "additionalProperties": false, - "properties": { - "day_one": { - "description": "Plan records the lane's first pass saw", - "format": "int64", - "type": "integer" - }, - "dry_run": { - "description": "Day-one plan records that have had a dry run", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "dry_run", - "day_one" - ], - "type": "object" - }, - "TorrentStatusResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TorrentStatusResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "active": { - "format": "int64", - "type": "integer" - }, - "armed_revision": { - "description": "Plan revision the owner armed on; absent in shadow and tripped, kept while paused", - "format": "int64", - "type": "integer" - }, - "child_detail": { - "description": "The qBittorrent child's supervision detail", - "type": "string" - }, - "child_state": { - "description": "The qBittorrent child's supervision state; anything but healthy pauses an armed lane", - "type": "string" - }, - "failure_limit": { - "description": "This many failed attempts in a row trip an armed lane", - "format": "int64", - "type": "integer" - }, - "failure_streak": { - "description": "Failed attempts in a row since the latest import; a handoff that spent no attempt neither counts nor resets it", - "format": "int64", - "type": "integer" - }, - "free_bytes": { - "format": "int64", - "type": "integer" - }, - "mode": { - "description": "shadow: starts are dry runs; armed: a start pushes its pick; tripped: a breaker returned the lane to shadow until an acknowledged re-arm; paused: armed, but the qBittorrent child is not healthy, so nothing starts", - "enum": [ - "shadow", - "armed", - "tripped", - "paused" - ], - "type": "string" - }, - "newly_qualified_24h": { - "description": "Candidates newly qualified in the rolling 24h, the day-one baseline and anything before the latest acknowledged trip left out", - "format": "int64", - "type": "integer" - }, - "next_start": { - "format": "date-time", - "type": "string" - }, - "spike_limit": { - "description": "More newly qualified candidates than this in 24h trip an armed lane", - "format": "int64", - "type": "integer" - }, - "starts_24h": { - "format": "int64", - "type": "integer" - }, - "trip_acknowledged": { - "description": "An arm acknowledged the latest trip", - "type": "boolean" - }, - "trip_reason": { - "description": "Why the latest breaker trip fired; absent before any trip", - "type": "string" - }, - "tripped_at": { - "description": "When the latest breaker trip fired", - "format": "date-time", - "type": "string" - } - }, - "required": [ - "mode", - "starts_24h", - "active", - "free_bytes", - "trip_acknowledged", - "newly_qualified_24h", - "spike_limit", - "failure_streak", - "failure_limit", - "child_state", - "child_detail" - ], - "type": "object" - }, - "TorrentSummaryResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TorrentSummaryResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "arming_floor": { - "$ref": "#/components/schemas/TorrentArmingFloor" - }, - "coverage": { - "$ref": "#/components/schemas/TorrentCoverage" - }, - "indexers": { - "description": "Drops per indexer and rule, by indexer name", - "items": { - "$ref": "#/components/schemas/TorrentIndexerDrops" - }, - "type": [ - "array", - "null" - ] - }, - "projection": { - "$ref": "#/components/schemas/TorrentDiskProjection" - }, - "revision": { - "description": "Plan revision the summary was summed over", - "format": "int64", - "type": "integer" - }, - "rotation": { - "$ref": "#/components/schemas/TorrentRotation" - }, - "usenet_won": { - "description": "Plan rows whose fresh usenet check found an acceptable release", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "revision", - "indexers", - "coverage", - "rotation", - "usenet_won", - "projection", - "arming_floor" - ], - "type": "object" - }, - "TriageCandidate": { - "additionalProperties": false, - "properties": { - "arr": { - "description": "kind/name of the holding MAIN arr", - "type": "string" - }, - "candidate_key": { - "type": "string" - }, - "episode_ids": { - "description": "Arr episode ids this candidate was expected to satisfy", - "items": { - "format": "int64", - "type": "integer" - }, - "type": [ - "array", - "null" - ] - }, - "group": { - "description": "active | issues | exhausted | resolved", - "type": "string" - }, - "health_state": { - "description": "the closed item-health grammar", - "type": "string" - }, - "media_type": { - "type": "string" - }, - "observed_at": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "poster_ref": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "resolved_at": { - "type": "string" - }, - "retry_count": { - "format": "int64", - "type": "integer" - }, - "source": { - "type": "string" - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "year": { - "format": "int64", - "type": "integer" - } - }, - "required": [ - "candidate_key", - "group", - "health_state", - "source", - "tmdb_id", - "media_type", - "arr", - "retry_count", - "observed_at" - ], - "type": "object" - }, - "TriageDetail": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TriageDetail.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "candidate": { - "$ref": "#/components/schemas/TriageCandidate" - }, - "history": { - "items": { - "$ref": "#/components/schemas/TriageHistoryEvent" - }, - "type": [ - "array", - "null" - ] - }, - "payload_sanitize_block": { - "$ref": "#/components/schemas/PayloadSanitizeBlockView" - } - }, - "required": [ - "candidate", - "history" - ], - "type": "object" - }, - "TriageHistoryEvent": { - "additionalProperties": false, - "properties": { - "at": { - "type": "string" - }, - "from_state": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "to_state": { - "type": "string" - } - }, - "required": [ - "to_state", - "at" - ], - "type": "object" - }, - "TriageResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/TriageResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "candidates": { - "items": { - "$ref": "#/components/schemas/TriageCandidate" - }, - "type": [ - "array", - "null" - ] - }, - "group": { - "description": "the requested group filter (empty = all)", - "type": "string" - } - }, - "required": [ - "candidates" - ], - "type": "object" - }, - "ViewerReadiness": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/ViewerReadiness.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "blocking_stage": { - "enum": [ - "request", - "acquisition", - "visibility", - "playback" - ], - "type": "string" - }, - "coverage": { - "description": "Recorded-file coverage of the requested seasons (or every listed season when none were recorded); empty for a movie", - "items": { - "$ref": "#/components/schemas/SeasonCoverage" - }, - "type": [ - "array", - "null" - ] - }, - "media_type": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - }, - "open_link": { - "$ref": "#/components/schemas/OpenLink" - }, - "seerr": { - "$ref": "#/components/schemas/SeerrObservation" - }, - "sources": { - "items": { - "$ref": "#/components/schemas/EvidenceSource" - }, - "type": [ - "array", - "null" - ] - }, - "stages": { - "items": { - "$ref": "#/components/schemas/ReadinessStage" - }, - "type": [ - "array", - "null" - ] - }, - "title": { - "type": "string" - }, - "tmdb_id": { - "format": "int64", - "type": "integer" - }, - "viewer": { - "$ref": "#/components/schemas/Person" - } - }, - "required": [ - "tmdb_id", - "media_type", - "viewer", - "stages", - "seerr", - "coverage", - "open_link", - "sources" - ], - "type": "object" - }, - "WatchConfigBody": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/WatchConfigBody.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "status": { - "type": "string" - } - }, - "required": [ - "status" - ], - "type": "object" - }, - "WatchStateRequest": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/WatchStateRequest.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "episode_key": { - "description": "SxxEyy for a tv episode; empty for a movie", - "type": "string" - }, - "played": { - "type": "boolean" - }, - "user": { - "description": "jellyfin username (must be covered by the item's sees: matrix)", - "type": "string" - } - }, - "required": [ - "user", - "played" - ], - "type": "object" - }, - "WatchStateResult": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/WatchStateResult.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "type": "string" - }, - "played": { - "description": "the read-back value", - "type": "boolean" - }, - "status": { - "description": "ok | forbidden | no_controller | missing | error", - "type": "string" - } - }, - "required": [ - "status", - "played" - ], - "type": "object" - }, - "WebhookAck": { - "additionalProperties": false, - "properties": { - "$schema": { - "description": "A URL to the JSON Schema for this object.", - "examples": [ - "https://example.com/schemas/WebhookAck.json" - ], - "format": "uri", - "readOnly": true, - "type": "string" - }, - "detail": { - "description": "Human-readable context", - "type": "string" - }, - "status": { - "description": "accepted | ignored | test | error", - "type": "string" - } - }, - "required": [ - "status" - ], - "type": "object" - } - }, - "securitySchemes": { - "authentikSession": { - "description": "Existing Authentik forward-single proxy session cookie, verified against the configured outpost. Caller identity/role headers are never trusted.", - "in": "header", - "name": "Cookie", - "type": "apiKey" - }, - "jellyfinToken": { - "description": "Jellyfin access token: MediaBrowser Token=\"\u003c32 hex\u003e\". Verified per request against the configured Jellyfin GET /Users/Me; the immutable user id maps to a configured household person. Household authority only, TV-allowlisted operations only. Inert unless server.cockpit.jellyfin_sign_in.enabled.", - "in": "header", - "name": "Authorization", - "type": "apiKey" - } - } - }, - "info": { - "description": "The tailored media bundle: supervisor, dispatch, layer, watch-state, recommendations, curation, backfill.", - "title": "conductarr", - "version": "0.1.0-dev" - }, - "openapi": "3.1.0", - "paths": { - "/api/v1/activity": { - "get": { - "description": "Defaults to requests attributed to the signed-in operator; requester=all shows household progress. Items carry the recorded requester and actor exactly as filed, so an on-behalf request belongs to its requester. Stages are separate evidence from separate owners and are never collapsed into a ready flag: a request receipt, an Arr file, a library membership or Seerr availability each prove only their own stage, and unknown, stale, conflicting or failed evidence stays in its stage. The feed reads local ledgers and cached authenticated server playback admission only; no provider is called per row. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-activity", - "parameters": [ - { - "description": "Household requester key to follow, or all for household progress. Defaults to the signed-in operator's own requests. This selects whose requests are shown; it never changes who is acting", - "explode": false, - "in": "query", - "name": "requester", - "schema": { - "description": "Household requester key to follow, or all for household progress. Defaults to the signed-in operator's own requests. This selects whose requests are shown; it never changes who is acting", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActivityFeed" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Recent requests with their request, acquisition, visibility and playback stages" - } - }, - "/api/v1/activity/titles/{tmdb}/{type}": { - "get": { - "description": "Reads Seerr request status, the holding Arr and a viewer-scoped Jellyfin view for this one title. Each live source fails independently and degrades only its own stage; a Seerr, Arr or Jellyfin outage leaves its stage stale or unknown and never proves readiness. Playback joins cached Jellyfin PlaybackInfo queried with the server key on the viewer's behalf, using a separate request pool sized by the layer visibility item_budget, with watch records and media-health faults as fallback. Items without a recorded Video stream in the first source and .strm paths are not sampled because Jellyfin can refresh metadata and inspect media for them. Admission proves neither playback policy, phone compatibility nor media bytes. open_link is the viewer's own Jellyfin item page, offered only after the viewer-scoped read found that item and only for a viewer the operator may open Jellyfin as; it carries no credential. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-activity-title", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - }, - { - "description": "Household member to read readiness for. Defaults to the signed-in operator", - "explode": false, - "in": "query", - "name": "viewer", - "schema": { - "description": "Household member to read readiness for. Defaults to the signed-in operator", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewerReadiness" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "One title's viewer readiness with live Seerr, Arr and Jellyfin evidence and the Jellyfin hand-off" - } - }, - "/api/v1/home/resume": { - "get": { - "description": "Read-only Jellyfin metadata: at most twelve resume items and one parent-series batch, bounded to five seconds. No caller-selected viewer, media-source lookup or playback write. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-home-resume", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResumeBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Unfinished titles for the authenticated viewer" - } - }, - "/api/v1/operator": { - "get": { - "description": "Identity is verified from the existing Authentik proxy session or, when enabled, the caller's Jellyfin token. Roles and targets come only from server configuration, never identity headers, requester bodies or selected profiles. Jellyfin sign-in grants household-only authority without library-management powers. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-cockpit-operator", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Operator" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik or Jellyfin sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - }, - { - "jellyfinToken": [] - } - ], - "summary": "Authenticated cockpit operator and household authority" - } - }, - "/api/v1/recommendations/profiles": { - "get": { - "description": " Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-recommendation-profiles", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationProfiles" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Configured household recommendation read profiles" - } - }, - "/api/v1/recommendations/profiles/{profile}/dismissals": { - "get": { - "description": "Read-only durable list, newest first. Like the feed read it is not limited to the operator's own profile. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-recommendation-dismissals", - "parameters": [ - { - "in": "path", - "name": "profile", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationDismissals" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Titles hidden from one profile's personalised feed" - }, - "post": { - "description": "Durable until an explicit restore. Requires the profile in the operator's recommendation_profiles capability. The title leaves the retained feed, its watched-seed and service projections and the profile's Jellyfin recommendation folders; it is never requested, and real media, general search and watch-state are untouched. Repeating the command is a no-op. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "post-recommendation-dismissal", - "parameters": [ - { - "in": "path", - "name": "profile", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationHideInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationHideResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Hide a title from one profile's personalised feed" - } - }, - "/api/v1/recommendations/profiles/{profile}/dismissals/{type}/{tmdb}": { - "delete": { - "description": "Requires the profile in the operator's recommendation_profiles capability. The title is eligible again at the next ranking cycle under the same scorer and filters; it is not inserted into any feed and not requested. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "delete-recommendation-dismissal", - "parameters": [ - { - "in": "path", - "name": "profile", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - }, - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationRestoreResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Restore a hidden title's eligibility for one profile" - } - }, - "/api/v1/recommendations/profiles/{profile}/feed": { - "get": { - "description": "Read-only in-memory observation; current expires at the configured feed interval. Ranking and Jellyfin materialization are independent. Unavailable means no successful ranking observation; stale preserves the last known result after expiry or failed refresh. Private conditional reads authenticate first and hash the complete retained projection plus operator authority and filters; matching If-None-Match returns a bodyless 304. This saves repeat transfer, not projection or hashing work. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-recommendation-feed", - "parameters": [ - { - "description": "Private display validator from a prior authenticated feed read", - "in": "header", - "name": "If-None-Match", - "schema": { - "description": "Private display validator from a prior authenticated feed read", - "type": "string" - } - }, - { - "in": "path", - "name": "profile", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Typed contributing watched seed; requires seed_tmdb_id", - "explode": false, - "in": "query", - "name": "seed_media_type", - "schema": { - "description": "Typed contributing watched seed; requires seed_tmdb_id", - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - }, - { - "description": "Contributing watched seed identity; requires seed_media_type", - "explode": false, - "in": "query", - "name": "seed_tmdb_id", - "schema": { - "description": "Contributing watched seed identity; requires seed_media_type", - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Observed candidate genre; filters retained rank without rescoring", - "explode": false, - "in": "query", - "name": "genre_id", - "schema": { - "description": "Observed candidate genre; filters retained rank without rescoring", - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Retained candidate type scope; never a new discovery query", - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "description": "Retained candidate type scope; never a new discovery query", - "enum": [ - "all", - "movie", - "tv" - ], - "type": "string" - } - }, - { - "description": "Observed NL flatrate provider ID; unknown candidate providers remain unresolved", - "explode": false, - "in": "query", - "name": "service_id", - "schema": { - "description": "Observed NL flatrate provider ID; unknown candidate providers remain unresolved", - "format": "int64", - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationFeed" - } - } - }, - "description": "OK", - "headers": { - "Cache-Control": { - "schema": { - "type": "string" - } - }, - "ETag": { - "schema": { - "type": "string" - } - } - } - }, - "304": { - "description": "Authenticated display representation unchanged; reuse the private cached feed" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error, including 404 for an unknown profile and 422 for invalid input" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Latest known native ranked recommendation feed" - } - }, - "/api/v1/recommendations/sync": { - "post": { - "description": "Waits for any active scheduled cycle, then resolves, scores, materializes, and read-back verifies every configured profile feed. Administrative cockpit authority required.", - "operationId": "post-recommendations-sync", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecommendationSyncResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Run a complete recommendation feed cycle" - } - }, - "/api/v1/requests": { - "post": { - "description": "Files the request for the chosen household requester (default: the signed-in operator) through the non-admin Seerr on-behalf path, or, for media a household library already holds, adds the requester's membership without a second grab. The acting operator is recorded separately from the requester. Missing selected seasons stay requestable; a season is excluded only when its own files are complete or an active Seerr request for the same requester already covers it. Duplicate, lost or inconclusive Seerr responses are reconciled against Seerr's own request list: unproven coverage returns a pending, unresolved receipt, and resubmitting the same command resumes it before filing anything. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "post-request-intent", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestIntentBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestReceipt" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Request a movie or explicit television seasons for a household requester" - } - }, - "/api/v1/requests/titles/{tmdb}/{type}": { - "get": { - "description": "Read-only receipts: acting operator, requester, outcomes per season, and the Seerr and dispatch receipts each command produced. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-title-requests", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TitleRequestsBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Native request records for one typed title" - } - }, - "/api/v1/requests/{id}/retire": { - "post": { - "description": "Administrator-only local closeout after a fresh Seerr identity read proves the pending record no longer matches its household target. Preserves attribution and effect evidence. Does not cancel any filed Seerr request, undo membership, settle recommendations or submit another request. Unchanged identities, settled/retired records and concurrent title operations conflict. Administrative cockpit authority required.", - "operationId": "post-retire-request", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestReceipt" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Retire an exact stale-identity pending native request record" - } - }, - "/arr/webhook": { - "post": { - "description": "Download and non-upgrade EpisodeFileDelete (Sonarr) or MovieFileDelete (Radarr) events are inserted into webhook_dedup and acknowledged immediately. File-delete events only re-materialize the title because a deleted Arr file otherwise leaves a dangling per-file Jellyfin layer link. Test events and other event types are ignored. Restricted to direct loopback bundled-service callers without browser/session/proxy headers; no cockpit role is granted.", - "operationId": "post-arr-webhook", - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "contentMediaType": "application/octet-stream", - "format": "binary", - "type": "string" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebhookAck" - } - } - }, - "description": "OK" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Bundled-service caller required" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "summary": "Sonarr/Radarr primary intake webhook (insert + 200; worker materializes)" - } - }, - "/arr/webhook/mirror": { - "post": { - "description": "Download and non-upgrade EpisodeFileDelete (Sonarr) or MovieFileDelete (Radarr) events are inserted into webhook_dedup and acknowledged immediately. The worker only re-materializes existing memberships; file deletes prune dangling per-file Jellyfin layer links. Restricted to direct loopback bundled-service callers without browser/session/proxy headers; no cockpit role is granted.", - "operationId": "post-arr-mirror-webhook", - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "contentMediaType": "application/octet-stream", - "format": "binary", - "type": "string" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebhookAck" - } - } - }, - "description": "OK" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Bundled-service caller required" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "summary": "Sonarr/Radarr mirror refresh webhook (insert + 200; worker refreshes)" - } - }, - "/auth/login": { - "get": { - "description": "Returns to the configured cockpit origin only; caller-supplied redirect destinations are not accepted. The existing reverse proxy routes /outpost.goauthentik.io to Authentik.", - "operationId": "get-cockpit-login", - "responses": { - "302": { - "description": "Found", - "headers": { - "Location": { - "schema": { - "type": "string" - } - } - } - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "summary": "Enter the existing Authentik proxy sign-in flow" - } - }, - "/catalogue/discover": { - "get": { - "description": "General-interest provider suggestions, not personalised Conductarr recommendations. Browsing cannot request or acquire media. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-discover", - "parameters": [ - { - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "default": "all", - "enum": [ - "all", - "movie", - "tv" - ], - "type": "string" - } - }, - { - "explode": false, - "in": "query", - "name": "page", - "schema": { - "default": 1, - "format": "int64", - "maximum": 500, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "TMDB genre ID in the selected movie or TV namespace; zero means no genre constraint", - "explode": false, - "in": "query", - "name": "genre_id", - "schema": { - "description": "TMDB genre ID in the selected movie or TV namespace; zero means no genre constraint", - "format": "int64", - "minimum": 0, - "type": "integer" - } - }, - { - "description": "Configured TMDB streaming-provider ID; zero means no service constraint", - "explode": false, - "in": "query", - "name": "service_id", - "schema": { - "description": "Configured TMDB streaming-provider ID; zero means no service constraint", - "format": "int64", - "minimum": 0, - "type": "integer" - } - }, - { - "description": "Requires movie or TV. Popular sorts by popularity; New by primary release date (movies) or first air date (TV), capped inclusively at today's UTC date, not service arrival; Top by vote average with at least 200 TMDB votes; Upcoming uses Seerr's premiere discovery from today's UTC date, ordered by popularity", - "explode": false, - "in": "query", - "name": "sort", - "schema": { - "description": "Requires movie or TV. Popular sorts by popularity; New by primary release date (movies) or first air date (TV), capped inclusively at today's UTC date, not service arrival; Top by vote average with at least 200 TMDB votes; Upcoming uses Seerr's premiere discovery from today's UTC date, ordered by popularity", - "enum": [ - "popular", - "new", - "top", - "upcoming" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Page" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "General Seerr discovery" - } - }, - "/catalogue/options": { - "get": { - "description": "Read-only cached genre and configured regional streaming-provider options. Independent source observations distinguish answered empty lists from unavailable metadata. No title/lane preload, personalised ranking or availability claim. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-options", - "parameters": [ - { - "explode": false, - "in": "query", - "name": "media_type", - "required": true, - "schema": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Options" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Media-type genres and curated streaming services" - } - }, - "/catalogue/performers/{performer}": { - "get": { - "description": "Read-only Seerr person facts with strict performer identity. Observed identity with empty optional facts differs from unavailable or explicitly missing metadata. Seerr lookup failures, including unknown people, return unavailable, not verified absence. No title holding, request, visibility or playback claim follows from person metadata. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-performer", - "parameters": [ - { - "in": "path", - "name": "performer", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PerformerDetail" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Selected cast performer identity and biography" - } - }, - "/catalogue/performers/{performer}/filmography": { - "get": { - "description": "Read-only native pages of Seerr's unpaged non-adult cast credits, not an exhaustive career. Groups movie/series identities while preserving every role in returned order. Counts describe this source snapshot and are absent on failure. Household holding, configured audience and incomplete request relations remain independent; none proves playback. No per-credit detail fetch or mutation. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-filmography", - "parameters": [ - { - "in": "path", - "name": "performer", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "explode": false, - "in": "query", - "name": "page", - "schema": { - "default": 1, - "format": "int64", - "maximum": 1000000, - "minimum": 1, - "type": "integer" - } - }, - { - "explode": false, - "in": "query", - "name": "page_size", - "schema": { - "default": 20, - "format": "int64", - "maximum": 100, - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FilmographyPage" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Selected performer's typed acting filmography" - } - }, - "/catalogue/search": { - "get": { - "description": "Authenticated read-only catalogue search. Request, local holding and provider availability are separate observations; failed provider reads are unavailable, never fabricated empty results. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-search", - "parameters": [ - { - "explode": false, - "in": "query", - "name": "q", - "required": true, - "schema": { - "maxLength": 300, - "minLength": 1, - "type": "string" - } - }, - { - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "default": "all", - "enum": [ - "all", - "movie", - "tv" - ], - "type": "string" - } - }, - { - "explode": false, - "in": "query", - "name": "page", - "schema": { - "default": 1, - "format": "int64", - "maximum": 500, - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Page" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Native Seerr movie and series search" - } - }, - "/catalogue/titles/{tmdb}/{type}": { - "get": { - "description": "Reads provider metadata and the existing held-title owner independently. Unavailable metadata never erases known held files or implies a title is unheld. No request, membership, watched-state or acquisition writer is called. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-title", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Detail" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Native movie or series title workspace" - } - }, - "/catalogue/titles/{tmdb}/{type}/cast": { - "get": { - "description": "Read-only unpaged Seerr cast with performer IDs distinct from typed title IDs. Series expose only the leading aggregate character per performer. Missing, unavailable and observed empty cast differ. Cast metadata proves neither holding, requests, visibility nor playback. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-cast", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TitleCast" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Movie or series cast performers" - } - }, - "/catalogue/titles/{tmdb}/{type}/display": { - "get": { - "description": "Authenticated Seerr metadata read for visible request posters. Contains no holding, files, mirrors, watch state, request status or ratings. Missing or unavailable artwork says nothing about a request or media availability. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-display", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Display" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Public title presentation" - } - }, - "/catalogue/titles/{tmdb}/{type}/enrichment": { - "get": { - "description": "Authenticated fresh Seerr metadata and ratings for an already observed workspace or a visible hero. Reads no Arr, files, mirrors, memberships or watch state. Local holding and coverage remain unknown; this response never authorizes a title action. Metadata, providers and ratings preserve their independent unavailable states. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-enrichment", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Enrichment" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Title metadata and source ratings" - } - }, - "/catalogue/titles/{tmdb}/{type}/metadata": { - "get": { - "description": "Reuses bounded observations from committed title metadata reads, restored from SQLite and served from memory, with zero upstream or holding acquisition in this request. A single paced background fill observes held titles; cold and evicted titles remain unobserved until admitted. Restart preserves committed facts, their age and failure outcomes. States describe the latest attempt; timestamps identify last-known genre/provider facts, not a freshness guarantee. Provider observation requires an explicit NL result: empty or NL-absent regional results are ambiguous and preserve prior offers/timestamps as unobserved, not complete coverage. NL offers are Seerr-reported regional catalogue metadata, not subscriptions, membership, request eligibility or playback proof. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-metadata", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TitleMetadata" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Retained title genres and NL streaming offers" - } - }, - "/catalogue/titles/{tmdb}/{type}/related": { - "get": { - "description": "Typed title-related provider suggestions kept separate from Conductarr's personalised ranking. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-related", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - }, - { - "explode": false, - "in": "query", - "name": "page", - "schema": { - "default": 1, - "format": "int64", - "maximum": 500, - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Page" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Related-title Seerr suggestions" - } - }, - "/catalogue/tv/{tmdb}/seasons/{season}": { - "get": { - "description": "Reads an explicitly selected provider season and joins local primary episode evidence without requesting missing seasons or claiming playback. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-catalogue-season", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "season", - "required": true, - "schema": { - "format": "int64", - "minimum": 0, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SeasonDetail" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Selected television season metadata and recorded coverage" - } - }, - "/curation/items": { - "get": { - "description": "Every enabled PRIMARY arr of the media type's kind joined with its bucket assignment, mapped to an owner (Justin/Mindel/Shared/Unassigned). The anime badge is SERVER-derived from the holding instance's is_anime flag (rename-proof). An arr that fails to list omits its items, never the whole view. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-curation-items", - "parameters": [ - { - "description": "tv | movie (the TV/Movies toggle)", - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "default": "tv", - "description": "tv | movie (the TV/Movies toggle)", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemsBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Poster-grid items + owner-rail counts for one media type" - } - }, - "/curation/items/{tmdb}/tv/episodes": { - "get": { - "description": "The drill-in panel: seasons/episodes joined from the holding sonarr (SxxEyy, title, air date, monitored, has-file, per-episode file quality/size/release) + per-user Jellyfin watch chips. Users included are EXACTLY those whose sees: matrix covers the item's library (server-derived, owners-not-buckets). One episodes call + one files call + one Jellyfin list per user โ€” never per-episode. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-curation-episodes", - "parameters": [ - { - "description": "TMDB id of the series", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the series", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EpisodeView" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Season-grouped episodes + per-user watch state for a TV item" - } - }, - "/curation/items/{tmdb}/tv/fill-gaps": { - "post": { - "description": "Lists the holding sonarr's episodes ONCE, filters to the gaps (monitored, without a file, and already aired โ€” unaired/future and unmonitored episodes excluded), and issues a single EpisodeSearch for their ids. Shares the one-in-flight-per-series guard with regrab (a second answers status=busy). Response carries searched_count; 0 with status=ok means there were no gaps (a quiet no-op). Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-curation-fill-gaps", - "parameters": [ - { - "description": "TMDB id of the series", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the series", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FillGapsResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Search the gap episodes of a TV series (monitored + fileless + aired)" - } - }, - "/curation/items/{tmdb}/{type}": { - "get": { - "description": "The always-open inspector's payload for a single tile (design ยง2.3): Identity (title/year/runtime/owners), File (size + media info), Source (arr/imported/indexer), Health (has-file/queued/failed/missing). Fetched ONLY on selection โ€” the targeted per-item File/Source/Health reads never run across the grid (library-scale rule). The grid list carries owners + anime already; this adds the drill-in facts. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-curation-item-detail", - "parameters": [ - { - "description": "TMDB id of the focused item", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the focused item", - "format": "int64", - "type": "integer" - } - }, - { - "description": "tv | movie", - "in": "path", - "name": "type", - "required": true, - "schema": { - "description": "tv | movie", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemDetail" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Inspector detail for one focused item" - } - }, - "/curation/items/{tmdb}/{type}/delete": { - "post": { - "description": "Requires the scope_id of the deletion impact the operator confirmed. The current scope is observed again under the title lock; a different scope answers conflict and an unobservable one inconclusive, both before any effect. Only the confirmed primary and mirror records are FULL-removed (deleteFiles=true so the arr's own stale library symlink goes too; removeFromClient is NEVER sent โ€” that GCs shared NzbDAV blobs the layer points at). The confirmed bucket-layer symlink(s) are pruned, the bucket_memberships rows dropped, and a targeted Jellyfin refresh issued. A mirror copy that cannot be removed after the primary is gone answers partial, never ok. Works for ANY provenance incl. usenet-streamed (the arr's on-disk file is its own symlink; the NzbDAV import tree is never touched). Bulk delete is a client-side fan-out over this endpoint โ€” one confirmed scope and one result per item. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-curation-delete", - "parameters": [ - { - "description": "TMDB id of the item to delete", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the item to delete", - "format": "int64", - "type": "integer" - } - }, - { - "description": "tv | movie", - "in": "path", - "name": "type", - "required": true, - "schema": { - "description": "tv | movie", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Delete one library item across all provenance (R12) within its confirmed scope" - } - }, - "/curation/items/{tmdb}/{type}/deletion-impact": { - "get": { - "description": "Read-only observation of the existing deletion owner's primary and rule-derived mirrors, exact membership and managed layer-link effects (including orphan collateral), and configured libraries/viewers. ready supplies the scope the operator confirms: its scope_id binds a later deletion request, which observes the scope again and deletes nothing when it differs. It is not a reservation or authorization. missing and inconclusive never supply a scope; unavailable reads cannot prove absence. Global deletion removes the confirmed Arr records with deleteFiles=true, prunes managed links and emptied directories, reconciles memberships/dispatch and requests a best-effort Jellyfin refresh. It never removes download-client payloads, blocklists, deletes NzbDAV blobs or erases watched state. Separate membership Remove is non-destructive. This read changes nothing. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-curation-deletion-impact", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeletionImpact" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Observe exact global media deletion impact without changing media" - } - }, - "/curation/items/{tmdb}/{type}/regrab": { - "post": { - "description": "Blocklists the grabbed release(s) (POST history/failed) + clears any queued grab (DELETE queue, removeFromClient=FALSE always, blocklist=true) then issues the replacement search (EpisodeSearch/SeasonSearch/MoviesSearch). Never touches the import tree; the arr replaces the file on the new import. One in-flight regrab per item (shared soak account) โ€” a second answers status=busy. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-curation-regrab", - "parameters": [ - { - "description": "TMDB id of the item", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the item", - "format": "int64", - "type": "integer" - } - }, - { - "description": "tv | movie", - "in": "path", - "name": "type", - "required": true, - "schema": { - "description": "tv | movie", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegrabRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegrabResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Regrab episodes/season (tv) or the movie (R12 regrab, pulled forward)" - } - }, - "/curation/items/{tmdb}/{type}/watchstate": { - "post": { - "description": "Marks one user's episode (SxxEyy) or movie played/unplayed with a LIVE Jellyfin write, verified by read-back. Refuses a user not covered by the item's sees: matrix (status=forbidden). This is a direct operator write โ€” NOT the capture/restore machinery โ€” so it never touches the clobber guard. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "post-curation-watchstate", - "parameters": [ - { - "description": "TMDB id of the item", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the item", - "format": "int64", - "type": "integer" - } - }, - { - "description": "tv | movie", - "in": "path", - "name": "type", - "required": true, - "schema": { - "description": "tv | movie", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WatchStateRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WatchStateResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Direct operator watch-state toggle (live Jellyfin write + read-back)" - } - }, - "/curation/layer/movie-cutover": { - "get": { - "description": "Lists legacy movie links and started or failed cutovers that have a durable path plan and watch snapshot. RecoveryCutoverID identifies a bracket that can be resumed by posting the same TMDB id even if no legacy links remain. Administrative cockpit authority required.", - "operationId": "get-curation-layer-movie-cutover", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MovieCutoverPendingBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "List legacy movies and interrupted movie cutovers" - } - }, - "/curation/layer/movie-cutover/{tmdb}": { - "get": { - "description": "Read-only evidence, including a retained bracket after legacy links disappear. Snapshot and restored counts are durable reported counts, not a new capture or live playback proof. The existing POST re-observes scope and preserves its capture/restore policy. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-title-movie-cutover", - "parameters": [ - { - "description": "TMDB id of the movie to convert", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the movie to convert", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MovieCutoverEvidence" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read one movie's legacy links and latest durable cutover" - }, - "post": { - "description": "A fresh cutover requires an exact indexed Movie at each old primary path for every relevant viewer, then persists the planned old-to-new paths and per-user snapshot before converting any entry. Re-posting an unfinished bracket re-indexes the planned new paths and field-wise merges the durable snapshot with current live UserData only for indexed targets. Partial indexing restores the reachable subset and reports unresolved paths for another retry; any still-legacy entries need a fresh capture before conversion. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-curation-layer-movie-cutover", - "parameters": [ - { - "description": "TMDB id of the movie to convert", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the movie to convert", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MovieCutoverBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Convert or safely resume one movie cutover" - } - }, - "/curation/library": { - "get": { - "description": "Unpaged retained-metadata filtering. Items are confirmed matches; unresolved contains possible matches, never known exclusions. Coverage counts the entire named/my/all, type and text scope before genre/service/watched predicates, not a rendered page. Counts and source statistics cover the captured media-type collection. Source observation time is explicit; membership and configured viewers are rejoined on every read. A snapshot reuses the last collection across scopes; omit it for a fresh capture. Watched filtering indexes only the verified viewer's collection once per snapshot. Facts retain independent latest genre/provider outcomes and last-success timestamps: no TTL or freshness guarantee. Committed metadata survives restart; cold, evicted and capacity-overflow identities remain unobserved until admitted. A single paced background fill observes held titles; this request never hydrates metadata. Filter completeness is not complete metadata coverage, subscription, request eligibility or playback proof. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-household-library", - "parameters": [ - { - "description": "Title or year search across household libraries by default", - "explode": false, - "in": "query", - "name": "q", - "schema": { - "description": "Title or year search across household libraries by default", - "maxLength": 300, - "type": "string" - } - }, - { - "description": "my/all (reserved), an enabled exact library display, or a legacy owner/Unassigned label; configured named libraries take precedence over owner labels", - "explode": false, - "in": "query", - "name": "library", - "schema": { - "description": "my/all (reserved), an enabled exact library display, or a legacy owner/Unassigned label; configured named libraries take precedence over owner labels", - "type": "string" - } - }, - { - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "default": "all", - "enum": [ - "all", - "tv", - "movie" - ], - "type": "string" - } - }, - { - "description": "Observed genre ID in each title's movie/TV namespace; zero disables", - "explode": false, - "in": "query", - "name": "genre_id", - "schema": { - "description": "Observed genre ID in each title's movie/TV namespace; zero disables", - "format": "int64", - "minimum": 0, - "type": "integer" - } - }, - { - "description": "Observed NL flatrate provider ID; zero disables", - "explode": false, - "in": "query", - "name": "service_id", - "schema": { - "description": "Observed NL flatrate provider ID; zero disables", - "format": "int64", - "minimum": 0, - "type": "integer" - } - }, - { - "description": "Verified operator's Jellyfin Played flag, never a submitted viewer identity", - "explode": false, - "in": "query", - "name": "watched", - "schema": { - "default": "all", - "description": "Verified operator's Jellyfin Played flag, never a submitted viewer identity", - "enum": [ - "all", - "watched", - "unwatched" - ], - "type": "string" - } - }, - { - "description": "Reuse the last retained collection without Arr reads; omitted captures anew, replaced or restarted snapshots return 409", - "explode": false, - "in": "query", - "name": "snapshot", - "schema": { - "description": "Reuse the last retained collection without Arr reads; omitted captures anew, replaced or restarted snapshots return 409", - "type": "string" - } - }, - { - "description": "Force a new Arr acquisition instead of the shared one taken within the last few seconds; ignored with a snapshot", - "explode": false, - "in": "query", - "name": "refresh", - "schema": { - "description": "Force a new Arr acquisition instead of the shared one taken within the last few seconds; ignored with a snapshot", - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LibraryResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Unified held title search and configured household visibility" - } - }, - "/curation/library-audiences": { - "get": { - "description": "Projects named membership routes, enabled Jellyfin libraries and users' configured sees by media type and anime scope. Includes empty libraries and scopes with no configured viewer. This read does not inspect current title unions, watch-write permissions or live media, and grants no mutation authority. An unwired audience read is unavailable, never an empty preview. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-library-audiences", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LibraryAudiencesBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Configured viewing audience for every enabled household membership scope" - } - }, - "/curation/library/activity": { - "get": { - "description": "Acquired separately from the title collection so a slow queue or history read never delays title presence. Each source reports whether its queue and history answered; a count is only meaningful for sources that did, and an unanswered source is never a zero. Per-source read times are explicit and may predate the request by the shared-acquisition window; refresh forces a new read. Administrative cockpit authority required.", - "operationId": "get-library-activity", - "parameters": [ - { - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "default": "all", - "enum": [ - "all", - "tv", - "movie" - ], - "type": "string" - } - }, - { - "description": "Force a new queue and history read instead of the shared one taken within the last few seconds", - "explode": false, - "in": "query", - "name": "refresh", - "schema": { - "description": "Force a new queue and history read instead of the shared one taken within the last few seconds", - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LibraryActivity" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Queue depth and import history of the primaries serving a media type" - } - }, - "/curation/library/{tmdb}/{type}": { - "get": { - "description": "Reads one movie or series identity, its canonical household memberships/viewers, Arr-recorded primary and mirror files, and the existing watched-state interface. A missing title requires every relevant primary to answer; unavailable or partially answered evidence is not missing media or playback proof. Reads are performed only after explicit detail navigation, never pointer hover or bulk selection. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-held-title", - "parameters": [ - { - "description": "TMDB id of the focused item", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the focused item", - "format": "int64", - "type": "integer" - } - }, - { - "description": "tv | movie", - "in": "path", - "name": "type", - "required": true, - "schema": { - "description": "tv | movie", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeldTitle" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Committed held title workspace with seasons, versions and configured viewers" - } - }, - "/curation/membership-jobs": { - "get": { - "description": " Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-curation-memberships", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MembershipJobsBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read recent membership commands" - } - }, - "/curation/memberships": { - "post": { - "description": "Validates the unchanged admission rules and durably queues the change without waiting on Arr or Jellyfin. The existing intake worker applies watch-state capture, materialization, convergence and read-back. Poll the returned receipt ID for per-title outcomes; acceptance is not completion. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "post-curation-memberships", - "parameters": [ - { - "description": "Reuse this key only for the same confirmed membership change", - "in": "header", - "name": "Idempotency-Key", - "schema": { - "description": "Reuse this key only for the same confirmed membership change", - "maxLength": 80, - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MembershipInputBody" - } - } - }, - "required": true - }, - "responses": { - "202": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MembershipJob" - } - } - }, - "description": "Accepted" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Dispatch a curation membership change" - } - }, - "/curation/memberships/{id}": { - "get": { - "description": " Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-curation-membership", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MembershipJob" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read membership command completion" - } - }, - "/curation/rename/{tmdb}": { - "get": { - "description": "Sonarr's own GET /api/v3/rename for the holding primary: every episode file whose on-disk name differs from what the naming config builds, with its existing and new series-folder-relative path. Changes nothing. An EMPTY files list means the series is already converged, and the POST will refuse. 503 while arr_naming.rename_episodes is false. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-curation-rename", - "parameters": [ - { - "description": "TMDB id of the series to rename (television only)", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the series to rename (television only)", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RenamePlanBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Preview the Sonarr rename pass for one series" - }, - "post": { - "description": "Renames ONE series' episode files to the naming config's canonical names and carries its watch-state across: durable scoped snapshot plus a same-operation capture of every user who sees the item's buckets, Sonarr's RenameFiles command over exactly the previewed episodeFileIds (waited to a terminal status), per-file layer links repointed and renamed where a bucket entry is a real directory rather than a whole-title symlink, a Jellyfin notify + structural validate + bounded re-index poll, then the capture remapped onto the new item ids BY PATH and restored through the live-wins merge with read-back. Refuses an empty preview (409) and never runs automatically. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-curation-rename", - "parameters": [ - { - "description": "TMDB id of the series to rename (television only)", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the series to rename (television only)", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RenameResultBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Run the Sonarr rename pass for one series" - } - }, - "/curation/rename/{tmdb}/passes": { - "get": { - "description": "Newest first, including unfinished passes, independently of preview availability. Files is a planned count, not actual renames. Restored and repointed counts are retained; skipped files, convergence and the full response are not. A retained request_id matches the originating POST's optional X-Request-ID transport metadata; absent or different identities cannot prove that original finished. This is not idempotency or a reconstructed response. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-curation-rename-passes", - "parameters": [ - { - "description": "TMDB id of the series to rename (television only)", - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "description": "TMDB id of the series to rename (television only)", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RenameHistoryOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read all durable rename passes for one television title" - } - }, - "/curation/watch-config": { - "post": { - "description": "Operator override for the boot-seeded watch guard. Resolves only enabled config Jellyfin usernames to runtime ids and atomically rewires reassign watch-state preservation. Boot retries this wiring while Jellyfin is unavailable. Answers 503 until serve supplies the resolver. Administrative cockpit authority required.", - "operationId": "post-curation-watch-config", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WatchConfigBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Rewire the reassign watch-guard to enabled Jellyfin users" - } - }, - "/dispatch/dry-run": { - "get": { - "description": "Returns the rule, routed primary arr and rule-derived mirror instances an add would fan out to. No side effects. Administrative cockpit authority required.", - "operationId": "get-dispatch-dry-run", - "parameters": [ - { - "description": "TMDB id to resolve", - "explode": false, - "in": "query", - "name": "tmdb_id", - "required": true, - "schema": { - "description": "TMDB id to resolve", - "format": "int64", - "type": "integer" - } - }, - { - "description": "tv | movie", - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "default": "tv", - "description": "tv | movie", - "enum": [ - "tv", - "movie" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DryRunPlan" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Resolve routing for an item without adding (engine.Plan as data)" - } - }, - "/dispatch/missing": { - "get": { - "description": "Compares each dispatch_log success against live arr state INCLUDING rule-derived mirror membership. An item deleted from an arr after its success row is surfaced here โ€” the forever-dedup would otherwise never retry it. Administrative cockpit authority required.", - "operationId": "get-dispatch-missing", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MissingOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Success rows whose item is absent from an arr (incl. rule-derived mirrors)" - }, - "post": { - "description": "Forces a re-dispatch for the given items so a mirror deleted after a success row is re-added. The primary re-add is a safe 400-already_added. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-dispatch-missing-redispatch", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RedispatchInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RedispatchOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Re-dispatch missing items (bypasses the success dedup)" - } - }, - "/dispatch/recent": { - "get": { - "description": " Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-dispatch-recent", - "parameters": [ - { - "description": "max rows (newest first)", - "explode": false, - "in": "query", - "name": "limit", - "schema": { - "default": 50, - "description": "max rows (newest first)", - "format": "int64", - "maximum": 500, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "filter by outcome (empty = all)", - "explode": false, - "in": "query", - "name": "outcome", - "schema": { - "description": "filter by outcome (empty = all)", - "enum": [ - "success", - "partial", - "failed" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecentOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Paginated dispatch_log (newest first)" - } - }, - "/downloads": { - "get": { - "description": "Groups Arr queue records by download id so a pack is one row, joins qBittorrent by infohash, and classifies active, client-queued, delay-pending, import-blocked, and imported-awaiting-seed-cleanup states server-side. Also returns a cached library provenance snapshot (torrent, spot, manual, usenet-streamed) with bounded on-demand local-dataset usage. Primary Arrs are listed once and indexed; no per-item API calls or per-poll filesystem walks. Authenticated household cockpit authority required; watch-state writes are limited to capability-listed viewers.", - "operationId": "get-downloads", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadsBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Grouped download activity and library storage provenance" - } - }, - "/healthz": { - "get": { - "description": "HTTP 503 only when the aggregate is unhealthy (a restart may fix it); degraded states stay 200 โ€” restart churn must not destroy healthy state.", - "operationId": "get-healthz", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthzBody" - } - } - }, - "description": "OK" - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthzBody" - } - } - }, - "description": "Aggregate unhealthy (a restart may fix it)" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "summary": "Aggregate + per-module health" - } - }, - "/healthz/history": { - "get": { - "description": "Reads the health-history ring table (a clock-driven sampler snapshots the registry's cached states ~every 60s, retained 14 days) and returns, per module, the ordered per-sample state series (the supervision-panel sparkline input), the uptime% (fraction of present samples healthy), and the restart-transition count (recoveries into healthy). Window is a Go duration query param (default 24h, clamped to the 14-day retention). Purely derived from the ring โ€” no live probing. Administrative cockpit authority required.", - "operationId": "get-healthz-history", - "parameters": [ - { - "description": "look-back window as a Go duration (e.g. 24h, 6h); clamped to the 14-day retention", - "explode": false, - "in": "query", - "name": "window", - "schema": { - "default": "24h", - "description": "look-back window as a Go duration (e.g. 24h, 6h); clamped to the 14-day retention", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HistoryReport" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Per-module health series + uptime%/restart-transitions" - } - }, - "/import-health/candidates": { - "get": { - "description": "The intake triage panel: every open candidate grouped by health (active = searching/downloading/source_complete/import_pending/retry_wait/regrab_eligible; issues = import_blocked/blocked_ambiguous; exhausted; resolved = an operator-resolved candidate). Optional group query filter. Read ONCE from the ledger โ€” never a per-candidate query. Administrative cockpit authority required.", - "operationId": "get-import-health-candidates", - "parameters": [ - { - "description": "triage group filter (empty = all groups)", - "explode": false, - "in": "query", - "name": "group", - "schema": { - "description": "triage group filter (empty = all groups)", - "enum": [ - "active", - "issues", - "exhausted", - "resolved" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TriageResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Candidate triage grouped active/issues/exhausted/resolved" - } - }, - "/import-health/candidates/{key}": { - "get": { - "description": "The inspector's per-item health view: the candidate's current health state + reason and its full append-only transition trail. Two indexed reads (get + history). Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-import-health-candidate", - "parameters": [ - { - "description": "the candidate's idempotent key", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TriageDetail" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Current health + transition history for one candidate" - } - }, - "/import-health/candidates/{key}/abandon": { - "post": { - "description": "Resolves ONE exhausted candidate with a required non-empty operator reason (audited in the ledger). The ledger enforces exhausted-only; a non-exhausted attempt answers 409 and an empty reason answers 400. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-import-health-abandon", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AbandonInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Audited abandon of an exhausted candidate" - } - }, - "/import-health/candidates/{key}/accept-review-mapping": { - "post": { - "description": "Operator-confirmed form of the shipped confident remap. The request carries no episode id. Under the shared exact-import guard, the handler re-probes the Arr and requires exactly one file mapped to exactly one aired, monitored, fileless episode of the candidate's series with only numbering-confidence rejections. It submits that Arr episode id through the existing exact ManualImport seam and records import_pending. No queue, blocklist, file-delete, or guessed-id path is reachable. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-import-health-accept-review-mapping", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Accept the Arr's current 1:1 episode mapping" - } - }, - "/import-health/candidates/{key}/clear-payload-sanitize-block": { - "post": { - "description": "Removes ONE candidate's payload-name-sanitize refusal marker. The automatic class records a marker only when the server's answer was evidence that the rename must not be repeated, and it never re-arms itself; this is the only way back. It performs no arr call, no queue mutation, no rename, and no candidate state change โ€” the next sweep simply re-proves eligibility from scratch and may refuse again. A candidate carrying no marker answers not_eligible. Administrative cockpit authority required.", - "operationId": "post-import-health-clear-payload-sanitize-block", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Re-arm the automatic payload-name sanitizer for one candidate" - } - }, - "/import-health/candidates/{key}/episode-options": { - "get": { - "description": "Joins ONE scoped manual-import candidate probe to ONE series episode read and marks each episode selectable only when it is in the payload, belongs to the expected series, is aired + monitored, and has no file. A probe failure returns the series rows with in_payload=false for the raw-id fallback. This read never authorizes an import; select-episodes re-verifies every posted id server-side. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-import-health-episode-options", - "parameters": [ - { - "description": "the candidate's idempotent key", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/EpisodeOption" - }, - "type": [ - "array", - "null" - ] - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Browsable advisory episode options for one TV candidate" - } - }, - "/import-health/candidates/{key}/regrab": { - "post": { - "description": "Allowed ONLY for a regrab_eligible candidate: re-searches its recorded primary Arr and exact candidate episode scope through the existing candidate curation primitive, never current membership or a whole-series fallback. Fresh recorded identity and monitored/fileless target proof precede mutation. The shared one-in-flight guard preserves removeFromClient=FALSE and skipRedownload=TRUE while blocklisting the failed release, then records the retry_wait cooldown. Any other health state returns not_eligible with the state named and never touches the queue โ€” a usable/ambiguous payload is an import problem, not a grab problem. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-import-health-regrab", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Eligible regrab/retry through the curation regrab primitive" - } - }, - "/import-health/candidates/{key}/remove-from-queue": { - "post": { - "description": "Eligible only for import_blocked, blocked_ambiguous, or exhausted. DownloadID locates a whole queue group; it is not removal authority. With or without a DownloadID, requires the verified recorded movie or series identity and, for TV, the complete expected episode set. Every row of a located download must match that scope before any DELETE; without a DownloadID, requires one exact fallback group. Shares the regrab guard and calls only queue delete with removeFromClient structurally false; the library deleteFiles path is unreachable. then=reopen transitions to regrab_eligible; then=resolve records a same-health audited resolution. Wrong health state or insufficient/conflicting queue scope answers 409 without mutation; refresh evidence and review the full download on a scope refusal. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-import-health-remove-from-queue", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RemoveFromQueueInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Reviewed operator removal of one blocked candidate's queue slot" - } - }, - "/import-health/candidates/{key}/retry-import": { - "post": { - "description": "Re-probes the arr's manual-import read-back for ONE candidate, rebuilds the expectation from the arr's own records, re-evaluates exact, and submits ONLY a go-decision (import_pending). A still-failing result re-persists the typed reason (import_blocked/blocked_ambiguous) and issues NO import and NO regrab. Eligible only for import_blocked/blocked_ambiguous/source_complete; any other state answers 409 with the state named. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-import-health-retry-import", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Operator re-drive of a blocked/complete candidate's exact import" - } - }, - "/import-health/candidates/{key}/review-mapping": { - "get": { - "description": "Returns available=true only when the Arr currently reports exactly one payload file mapped to exactly one aired, monitored, fileless episode of the candidate's series and every blocking rejection is a numbering-confidence rejection. This endpoint is read-only. The Accept request carries no episode id; its handler re-probes and submits the Arr's current mapping server-side. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-import-health-review-mapping", - "parameters": [ - { - "description": "the candidate's idempotent key", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CandidateReviewMapping" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Arr-proven 1:1 episode mapping for operator review" - } - }, - "/import-health/candidates/{key}/select-episodes": { - "post": { - "description": "Rebuilds a TV candidate's expectation with EXACTLY the posted episode ids, re-verified against the arr's own records (each must exist, belong to the series, be monitored + aired, and have no file) โ€” the posted ids are never trusted blind. Any cross-series / unaired / unmonitored / file-backed / unknown id rejects the whole request with no submission. On a go-decision, submits exactly those ids. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-import-health-select-episodes", - "parameters": [ - { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "in": "path", - "name": "key", - "required": true, - "schema": { - "description": "the candidate's idempotent key (exactly one candidate per call)", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SelectEpisodesInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Operator-supplied exact TV episode mapping (re-verified)" - } - }, - "/media-health/breaker/hold": { - "post": { - "description": "The only operator override of the detection breaker, and it is fail-safe by construction: it can force the breaker open (suspending proposals and repairs) and nothing else. Authenticated operator attribution and a nonempty reason are required. Holding an already-held breaker re-attributes it. No request to this API can close the breaker, credit a control epoch, or shorten the two-clean-epoch recovery. Administrative cockpit authority required.", - "operationId": "post-media-health-breaker-hold", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthBreakerHoldInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthBreakerResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Force the detector breaker OPEN under operator attribution" - } - }, - "/media-health/breaker/release": { - "post": { - "description": "Clears the hold attribution only. The breaker stays OPEN with zero healthy control epochs, so the standard two-clean-epoch recovery runs from scratch afterwards โ€” this endpoint uses authenticated operator attribution; releasing a breaker that is not held is a no-op. Administrative cockpit authority required.", - "operationId": "post-media-health-breaker-release", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthBreakerResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Release an operator hold WITHOUT closing the breaker" - } - }, - "/media-health/probe": { - "post": { - "description": "Persists one operator-requested probe for exactly one blob. Use blob_id or webdav_path, but never both. The request survives restart, is selected before scheduled targets within the same budget, and is consumed with the next recorded probe outcome. A disabled detector answers 503. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-media-health-probe", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthProbeInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthProbeResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Request ONE operator media-health probe" - } - }, - "/media-health/proposals": { - "get": { - "description": "The media-health review panel: every confirmed NzbDAV fault the detector proposed a repair for, grouped by whether it awaits a decision, is being repaired, or is finished. Each proposal carries the COMPLETE consumer snapshot, so a season pack shows every episode the repair covers. The response also reports the detector's global breaker: while it is open, an approval is recorded but no repair executes. Read once from the ledger. Administrative cockpit authority required.", - "operationId": "get-media-health-proposals", - "parameters": [ - { - "description": "group filter (empty = all groups)", - "explode": false, - "in": "query", - "name": "group", - "schema": { - "description": "group filter (empty = all groups)", - "enum": [ - "review", - "repairing", - "closed" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthReviewResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Media-fault repair proposals grouped review/repairing/closed" - } - }, - "/media-health/proposals/{id}": { - "get": { - "description": "The inspector view: the proposal's blob identity, every Arr record it backs, and the append-only stage trail of its repair (queued, deleting, searching, awaiting, verifying, done/failed) with the reason recorded at each transition. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-media-health-proposal", - "parameters": [ - { - "description": "the proposal id (exactly one proposal per call)", - "in": "path", - "name": "id", - "required": true, - "schema": { - "description": "the proposal id (exactly one proposal per call)", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthDetail" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "One proposal with its repair transition trail" - } - }, - "/media-health/proposals/{id}/approve": { - "post": { - "description": "Records operator consent for exactly one proposal and opens its repair at the queued stage. NO Arr call is made here: the fault-kind verifier re-probes immediately before acting and abandons the repair if its proof no longer holds. Only a pending proposal is eligible; an already decided, executing, or retracted one answers 409. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-media-health-approve", - "parameters": [ - { - "description": "the proposal id (exactly one proposal per call)", - "in": "path", - "name": "id", - "required": true, - "schema": { - "description": "the proposal id (exactly one proposal per call)", - "format": "int64", - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthDecisionInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthDecisionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Approve the repair of ONE media fault" - } - }, - "/media-health/proposals/{id}/reject": { - "post": { - "description": "Closes exactly one proposal with a required non-empty audited reason. No repair row is created and no Arr delete, blocklist, queue removal, search, or grab is ever issued for it. An empty reason answers 400; a non-pending proposal answers 409. The blob stays in the detector's inventory and re-surfaces only if its same fault kind is confirmed again. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-media-health-reject", - "parameters": [ - { - "description": "the proposal id (exactly one proposal per call)", - "in": "path", - "name": "id", - "required": true, - "schema": { - "description": "the proposal id (exactly one proposal per call)", - "format": "int64", - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthDecisionInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MediaHealthDecisionResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Reject the repair of ONE media fault" - } - }, - "/notify/recent": { - "get": { - "description": "Administrator-only notify_log feed: media_arrived, media_stalled, media_regrab, and media_import_issue pushes with the descriptor recorded at send time (title, year, owner, hint). Administrative cockpit authority required.", - "operationId": "get-notify-recent", - "parameters": [ - { - "description": "max rows (newest first)", - "explode": false, - "in": "query", - "name": "limit", - "schema": { - "default": 15, - "description": "max rows (newest first)", - "format": "int64", - "maximum": 200, - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotifyRecentOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Recent notifications pushed to Home Assistant (newest first)" - } - }, - "/operations/titles/{tmdb}/{type}": { - "get": { - "description": "Administrative read only. Includes terminal candidates and complete retained histories. Import, dispatch and current route-presence observations fail independently; unavailable evidence never proves empty, imported or playable media. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-title-diagnosis", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TitleDiagnosis" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read exact retained import and dispatch evidence for a typed title" - } - }, - "/operations/titles/{tmdb}/{type}/media-health": { - "get": { - "description": "Read only. Matches any recorded consumer and retains the entire proposal scope, including other titles and mirrors. File identities are proposal snapshots, not fresh Arr proof. No probe, repair, retry or policy change occurs. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-title-media-health", - "parameters": [ - { - "in": "path", - "name": "tmdb", - "required": true, - "schema": { - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TitleMediaHealth" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read complete media-health proposals and repair evidence for one typed title" - } - }, - "/reconcile/replacement-holds": { - "get": { - "description": "Lists replacement holds whose expiry is still in the future. Administrative cockpit authority required.", - "operationId": "list-replacement-holds", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplacementHoldsResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "List active operator replacement holds" - }, - "post": { - "description": "Creates or replaces a TTL-bound hold for exactly one primary Radarr movie or a set of primary Sonarr episodes. A hold prevents auto-resolve from clearing the matching Arr queue candidate; it does not import the replacement. Administrative cockpit authority required.", - "operationId": "create-replacement-holds", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateReplacementHoldInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplacementHoldsResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Hold satisfied candidate auto-resolve for a replacement grab" - } - }, - "/reconcile/replacement-holds/{arr}/{record_type}/{record_id}": { - "delete": { - "description": "Deletes the hold for one Arr record. Releasing a missing hold is an idempotent no-op. Administrative cockpit authority required.", - "operationId": "release-replacement-hold", - "parameters": [ - { - "description": "Arr instance name", - "in": "path", - "name": "arr", - "required": true, - "schema": { - "description": "Arr instance name", - "type": "string" - } - }, - { - "description": "held record type", - "in": "path", - "name": "record_type", - "required": true, - "schema": { - "description": "held record type", - "enum": [ - "episode", - "movie" - ], - "type": "string" - } - }, - { - "description": "Arr episode or movie id", - "in": "path", - "name": "record_id", - "required": true, - "schema": { - "description": "Arr episode or movie id", - "format": "int64", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplacementHoldReleaseResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Release one operator replacement hold" - } - }, - "/recovery/layer/actions": { - "get": { - "description": "Returns durable MaterializeAndRefresh proposals and execution stages. Paired tmdb_id/media_type selects complete retained title history, including terminal actions, without the global worklist limit. Approval is a separate, explicitly named POST. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits. Non-administrators must supply paired positive tmdb_id and movie/tv media_type filters; global worklists remain administrative.", - "operationId": "get-recovery-layer-actions", - "parameters": [ - { - "description": "exact title TMDB identity; requires media_type", - "explode": false, - "in": "query", - "name": "tmdb_id", - "schema": { - "description": "exact title TMDB identity; requires media_type", - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "description": "exact title type; requires tmdb_id", - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "description": "exact title type; requires tmdb_id", - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - }, - { - "description": "exact durable stage filter (empty = all)", - "explode": false, - "in": "query", - "name": "stage", - "schema": { - "description": "exact durable stage filter (empty = all)", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LayerVisibilityActionsOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read reviewed layer convergence actions" - } - }, - "/recovery/layer/actions/{action_id}": { - "get": { - "description": "Returns one stable action identity with its latest exact represented-child evidence and append-only durable stage history. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "get-recovery-layer-action", - "parameters": [ - { - "description": "stable reviewed action identity", - "in": "path", - "name": "action_id", - "required": true, - "schema": { - "description": "stable reviewed action identity", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LayerVisibilityActionOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read one layer action, exact proof, and history" - } - }, - "/recovery/layer/actions/{action_id}/approve": { - "post": { - "description": "Runs the reviewed MaterializeAndRefresh for exactly one proposed action IN THE SERVING PROCESS, and returns the stage the executor left it in. Every stage is a durable compare-and-set commit, so the trail is readable from the action route while this call is in flight. The executor re-reads fresh proof and re-confirms the content behind the item before it materialises anything: an already-converged item closes without remote work, and unreadable content aborts as execution_uncertain rather than re-advertising a broken item. An unknown id answers 404, an action that is not proposed answers 409, and an unwired executor answers 503. There is no bulk approval by construction. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits.", - "operationId": "post-recovery-layer-approve", - "parameters": [ - { - "description": "stable reviewed action identity", - "in": "path", - "name": "action_id", - "required": true, - "schema": { - "description": "stable reviewed action identity", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LayerVisibilityApprovalView" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Approve and execute ONE layer convergence action" - } - }, - "/recovery/layer/observations": { - "get": { - "description": "Returns exact Arr-to-owned-link-to-Jellyfin observations. Paired tmdb_id/media_type selects every recorded bucket for that title without the global worklist limit. Parent Series, Season, and watch metadata are not convergence proof. Library-management cockpit capability required (administrators included); this grants authority across household titles, not other viewers' watched-state or recommendation edits. Non-administrators must supply paired positive tmdb_id and movie/tv media_type filters; global worklists remain administrative.", - "operationId": "get-recovery-layer-observations", - "parameters": [ - { - "description": "exact title TMDB identity; requires media_type", - "explode": false, - "in": "query", - "name": "tmdb_id", - "schema": { - "description": "exact title TMDB identity; requires media_type", - "format": "int64", - "minimum": 1, - "type": "integer" - } - }, - { - "description": "exact title type; requires tmdb_id", - "explode": false, - "in": "query", - "name": "media_type", - "schema": { - "description": "exact title type; requires tmdb_id", - "enum": [ - "movie", - "tv" - ], - "type": "string" - } - }, - { - "description": "status filter (empty = all)", - "explode": false, - "in": "query", - "name": "status", - "schema": { - "description": "status filter (empty = all)", - "enum": [ - "converged", - "drift", - "scanner_pending", - "unknown", - "no_files", - "unreleased", - "needs_operator" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LayerVisibilityObservationsOutputBody" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read current layer and Jellyfin convergence evidence" - } - }, - "/recovery/spottarr/freshness": { - "get": { - "description": "Returns the cached read-only SQLite freshness observation: newest IndexedAt/UpdatedAt timestamp, age, and row count. HTTP liveness is not indexing proof. Administrative cockpit authority required.", - "operationId": "get-recovery-spottarr-freshness", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SpottarrFreshnessView" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Read cached Spottarr index freshness" - } - }, - "/seerr/webhook": { - "post": { - "description": "Classifies notification_type: MEDIA_APPROVED/MEDIA_AUTO_APPROVED are inserted into webhook_dedup and answered 200 immediately (the worker runs the dispatch engine). TEST_NOTIFICATION answers 200 and does nothing. MEDIA_DECLINED and everything else are ignored (deletions are out of scope). Restricted to direct loopback bundled-service callers without browser/session/proxy headers; no cockpit role is granted.", - "operationId": "post-seerr-webhook", - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "contentMediaType": "application/octet-stream", - "format": "binary", - "type": "string" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebhookAck" - } - } - }, - "description": "OK" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Bundled-service caller required" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "summary": "Jellyseerr approval webhook (insert + 200; worker dispatches)" - } - }, - "/supervise/restart/{target}": { - "post": { - "description": "Cycles a process-backed bundled service (nzbdav, an arr, prowlarr, seerr) via the supervisor, or restarts an allowlisted sibling container through the socket-proxy (degrading cleanly when the proxy is absent). Worker-loop modules (dispatch-sweep, webhook-worker) are NOT restartable โ€” they ride the conductarr process โ€” and answer 400. Administrative cockpit authority required.", - "operationId": "post-supervise-restart", - "parameters": [ - { - "description": "module or sibling name to restart", - "in": "path", - "name": "target", - "required": true, - "schema": { - "description": "module or sibling name to restart", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RestartResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Restart a bundled service or managed sibling" - } - }, - "/torrent/arm": { - "post": { - "description": "Persists the armed state on the plan revision the owner read in the arm confirm; the next start pushes its pick. Answers 409 when that revision is no longer the current plan, and 503 while features.torrent_fallback is off. No restart is involved. Administrative cockpit authority required.", - "operationId": "arm-torrent-lane", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentArmInputBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentStatusResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Arm the torrent lane" - } - }, - "/torrent/candidates": { - "get": { - "description": "The joined candidates table of the torrent lane: every dead-file record of a repair proposal and every missing monitored record on a primary Arr, with the ledger that tracks it, its answered-silent days, whether the count was seeded from history, and whether it qualifies. Read from the lane's ledger alone; answers 503 while features.torrent_fallback is off. Administrative cockpit authority required.", - "operationId": "get-torrent-candidates", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentCandidatesResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Torrent candidates with their silence clocks" - } - }, - "/torrent/disarm": { - "post": { - "description": "Clears the armed state: the next start is a dry run again, and pushed handoffs run to their terminal. Answers 503 while features.torrent_fallback is off. Administrative cockpit authority required.", - "operationId": "disarm-torrent-lane", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentStatusResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Return the torrent lane to shadow" - } - }, - "/torrent/history": { - "get": { - "description": "Returns recorded handoffs from the lane ledger. Answers 503 while features.torrent_fallback is off. Administrative cockpit authority required.", - "operationId": "get-torrent-history", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentHistoryResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Torrent handoff history" - } - }, - "/torrent/plan": { - "get": { - "description": "Returns the latest cached dry-run plan and its row decisions without issuing grabs. Answers 503 while features.torrent_fallback is off. Administrative cockpit authority required.", - "operationId": "get-torrent-plan", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentPlanResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Latest torrent shadow plan" - } - }, - "/torrent/status": { - "get": { - "description": "Returns cached lane mode and handoff capacity. Answers 503 while features.torrent_fallback is off. Administrative cockpit authority required.", - "operationId": "get-torrent-status", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentStatusResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Torrent lane shadow status" - } - }, - "/torrent/summary": { - "get": { - "description": "Sums the current plan's rows: torrent releases each acceptability rule dropped per indexer, coverage by kind and anime flag, rotation over the day-one set, usenet wins, the disk of the next starts against the floor, and whether the arming floor holds per kind. Reads persisted state only; answers 503 while features.torrent_fallback is off. Administrative cockpit authority required.", - "operationId": "get-torrent-summary", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TorrentSummaryResult" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Authentik sign-in required" - }, - "403": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Operator or target is not permitted" - }, - "503": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Identity verification or operation unavailable" - }, - "default": { - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - } - } - }, - "description": "Error" - } - }, - "security": [ - { - "authentikSession": [] - } - ], - "summary": "Torrent shadow summary" - } - } - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/CachePolicy.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/CachePolicy.kt deleted file mode 100644 index 5674638c..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/CachePolicy.kt +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr - -/** Caller lifecycle events; no background poller or UI is owned by the client. */ -enum class ReadTrigger { IF_STALE, LAUNCH, FOREGROUND, OPEN, POLL, FORCE, CACHE_ONLY } - -/** Refresh lifetimes do not expire last-known-good data or delete it on failure. */ -enum class CachePolicy(internal val lifetimeMillis: Long, internal val capacity: Int? = null) { - LAUNCH(Long.MAX_VALUE), - DAILY(24 * 60 * 60 * 1000L), - FEED(0), - FOREGROUND(Long.MAX_VALUE), - DISCOVERY(30 * 60 * 1000L), - RATINGS(6 * 60 * 60 * 1000L, 300), - ON_OPEN(Long.MAX_VALUE), - SEASON(Long.MAX_VALUE, 300), - ACTIVITY(30 * 1000L), - NO_CACHE(0), - ; - - internal fun shouldRefresh(trigger: ReadTrigger, entry: CacheEntry?, now: Long): Boolean { - if (trigger == ReadTrigger.CACHE_ONLY) return false - if (entry == null || trigger == ReadTrigger.FORCE) return true - if (this == LAUNCH && trigger == ReadTrigger.LAUNCH) return true - if ((this == FOREGROUND || this == ACTIVITY) && trigger == ReadTrigger.FOREGROUND) return true - if ((this == ON_OPEN || this == SEASON) && trigger == ReadTrigger.OPEN) return true - val expiry = entry.expiresAtMillis - return if (this == FEED) expiry == null || now >= expiry - else lifetimeMillis != Long.MAX_VALUE && now - entry.validatedAtMillis >= lifetimeMillis - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/CacheStore.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/CacheStore.kt deleted file mode 100644 index 3bc1c794..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/CacheStore.kt +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr - -import kotlinx.serialization.Serializable - -/** A validated response; saved time is preserved across ETag revalidation. */ -@Serializable -data class CacheEntry( - val body: String, - val etag: String?, - val savedAtMillis: Long, - val validatedAtMillis: Long, - val expiresAtMillis: Long? = null, - val absent: Boolean = false, - val group: String, -) - -/** Storage is partitioned by server and credential digest; implementations never receive tokens. */ -interface CacheStore { - fun read(scope: String, key: String): CacheEntry? - fun write(scope: String, key: String, entry: CacheEntry, capacity: Int? = null) - fun remove(scope: String, key: String) - fun clear(scope: String) -} - -/** Deterministic fake with the same group capacity semantics as the disk store. */ -class MemoryCacheStore : CacheStore { - private val entries = mutableMapOf, CacheEntry>() - - @Synchronized override fun read(scope: String, key: String): CacheEntry? = entries[scope to key] - - @Synchronized override fun write(scope: String, key: String, entry: CacheEntry, capacity: Int?) { - entries[scope to key] = entry - if (capacity != null) { - entries.filter { it.key.first == scope && it.value.group == entry.group } - .entries.sortedByDescending { it.value.validatedAtMillis } - .drop(capacity).forEach { entries.remove(it.key) } - } - } - - @Synchronized override fun remove(scope: String, key: String) { entries.remove(scope to key) } - @Synchronized override fun clear(scope: String) { entries.keys.removeAll { it.first == scope } } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/ConductarrClient.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/ConductarrClient.kt deleted file mode 100644 index 5559f15a..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/ConductarrClient.kt +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr - -import java.io.Closeable -import java.io.IOException -import java.time.OffsetDateTime -import java.time.format.DateTimeParseException -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import kotlinx.coroutines.withContext -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import okhttp3.Call -import okhttp3.Callback -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response - -/** Failures contain no response bodies, credentials or transport exception text. */ -sealed interface ClientError { - data object NeedsSignIn : ClientError - data class Http(val status: Int) : ClientError - data object Unreachable : ClientError - data object InvalidResponse : ClientError -} - -/** A successful copy includes the time needed by the Saved HH:MM indicator. */ -data class Saved(val value: T?, val savedAtMillis: Long, val fromDisk: Boolean, val absent: Boolean = false) - -/** Saved arrives before network work; a refresh failure retains it except on 401. */ -sealed interface ReadResult { - data class Value(val saved: Saved) : ReadResult - data class Failure(val error: ClientError, val saved: Saved? = null) : ReadResult -} - -/** Mutation results deliberately cannot contain a cached receipt. */ -sealed interface MutationResult { - data class Success(val value: T) : MutationResult - data class Failure(val error: ClientError) : MutationResult -} - -/** Safe diagnostic vocabulary: there is no string sink that could receive a token. */ -enum class ClientEvent { REFRESHED, REVALIDATED, NEEDS_SIGN_IN, HTTP_FAILURE, UNREACHABLE, INVALID_RESPONSE, CACHE_UNAVAILABLE } - -/** Time limits apply to the entire call as well as connection and response I/O. */ -data class ClientTimeouts(val connectSeconds: Long = 10, val readSeconds: Long = 20, val callSeconds: Long = 30) - -/** An immutable identity-scoped client; create another client when the selected person changes. */ -class ConductarrClient( - baseUrl: String, - private val jellyfinToken: String, - private val cache: CacheStore, - timeouts: ClientTimeouts = ClientTimeouts(), - private val nowMillis: () -> Long = System::currentTimeMillis, - private val onEvent: (ClientEvent) -> Unit = {}, -) : Closeable { - private val base = baseUrl.toHttpUrl() - private val scope: String - private val revoked = AtomicBoolean(false) - private val cacheIdentityLock = Any() - private val catalogueReads = Semaphore(2) - private val http = OkHttpClient.Builder() - .connectTimeout(timeouts.connectSeconds, TimeUnit.SECONDS) - .readTimeout(timeouts.readSeconds, TimeUnit.SECONDS) - .callTimeout(timeouts.callSeconds, TimeUnit.SECONDS) - .followRedirects(false) - .followSslRedirects(false) - .retryOnConnectionFailure(false) - .build() - - init { - require(base.username.isEmpty() && base.password.isEmpty() && base.query == null && base.fragment == null) { "Base URL must not contain credentials, query or fragment" } - require(base.isHttps || base.host in setOf("localhost", "127.0.0.1", "::1")) { "Conductarr requires HTTPS" } - require(jellyfinToken.matches(Regex("[0-9a-fA-F]{32}"))) { "Expected a Jellyfin access token" } - scope = digest("$base\n$jellyfinToken") - } - - /** Emit disk LKG first, then revalidate when the endpoint lifetime or caller event requires it. */ - fun read(call: ReadCall, trigger: ReadTrigger = ReadTrigger.IF_STALE): Flow> = flow { - if (revoked.get()) { - emit(ReadResult.Failure(ClientError.NeedsSignIn)) - return@flow - } - val url = url(call.contract, call.segments, call.query) - val group = call.contract.path + if (call.policy == CachePolicy.RATINGS) "-${call.segments["type"]}" else "" - val key = "$group:${digest(url.toString())}" - val entry = if (call.policy != CachePolicy.NO_CACHE) cacheRead(key) else null - val saved = entry?.let { decodeSaved(call.serializer, it, true) } - if (saved != null) emit(ReadResult.Value(saved)) - if (!call.policy.shouldRefresh(trigger, if (saved != null) entry else null, nowMillis())) return@flow - val request = request(url).apply { if (saved != null) entry.etag?.let { header("If-None-Match", it) } }.build() - val result = if (call.contract.path.startsWith("/catalogue/")) { - catalogueReads.withPermit { refresh(call, request, key, group, entry.takeIf { saved != null }, saved) } - } else refresh(call, request, key, group, entry.takeIf { saved != null }, saved) - emit(result) - }.flowOn(Dispatchers.IO) - - /** Execute once; an uncertain request outcome must be checked with the title-requests read. */ - suspend fun mutate(call: MutationCall): MutationResult = withContext(Dispatchers.IO) { - if (revoked.get()) return@withContext MutationResult.Failure(ClientError.NeedsSignIn) - val body = call.body?.toRequestBody("application/json".toMediaType()) - val request = request(url(call.contract, call.segments, emptyMap())).method(call.contract.method, body).build() - try { - http.newCall(request).awaitResponse().use { response -> - if (response.code == 401) return@withContext MutationResult.Failure(revoke()) - if (!response.isSuccessful) return@withContext MutationResult.Failure(ClientError.Http(response.code)) - val value = WireJson.decodeFromString(call.serializer, response.body.string()) - if (revoked.get()) MutationResult.Failure(ClientError.NeedsSignIn) - else MutationResult.Success(value) - } - } catch (_: IOException) { - onEvent(ClientEvent.UNREACHABLE) - MutationResult.Failure(ClientError.Unreachable) - } catch (_: SerializationException) { - onEvent(ClientEvent.INVALID_RESPONSE) - MutationResult.Failure(ClientError.InvalidResponse) - } - } - - private suspend fun refresh(call: ReadCall, request: Request, key: String, group: String, entry: CacheEntry?, saved: Saved?): ReadResult { - return try { - http.newCall(request).awaitResponse().use { response -> - if (response.code == 401) return ReadResult.Failure(revoke()) - if (revoked.get()) return ReadResult.Failure(ClientError.NeedsSignIn) - if (response.code == 304) { - if (entry == null || saved == null) return ReadResult.Failure(ClientError.InvalidResponse) - cacheWrite(key, entry.copy(etag = response.header("ETag") ?: entry.etag, validatedAtMillis = nowMillis()), call.policy) - onEvent(ClientEvent.REVALIDATED) - return ReadResult.Value(saved.copy(fromDisk = false)) - } - if (response.code == 404 && call.policy == CachePolicy.RATINGS) { - val absent = CacheEntry("", null, nowMillis(), nowMillis(), absent = true, group = group) - cacheWrite(key, absent, call.policy) - return ReadResult.Value(Saved(null, absent.savedAtMillis, false, true)) - } - if (!response.isSuccessful) { - onEvent(ClientEvent.HTTP_FAILURE) - return ReadResult.Failure(ClientError.Http(response.code), saved) - } - val body = response.body.string() - val value = WireJson.decodeFromString(call.serializer, body) - val timestamp = nowMillis() - val updated = CacheEntry(body, response.header("ETag"), timestamp, timestamp, feedExpiry(call.policy, body), group = group) - if (call.policy != CachePolicy.NO_CACHE) cacheWrite(key, updated, call.policy) - onEvent(ClientEvent.REFRESHED) - ReadResult.Value(Saved(value, timestamp, false)) - } - } catch (_: IOException) { - onEvent(ClientEvent.UNREACHABLE) - ReadResult.Failure(ClientError.Unreachable, saved) - } catch (_: SerializationException) { - onEvent(ClientEvent.INVALID_RESPONSE) - ReadResult.Failure(ClientError.InvalidResponse, saved) - } - } - - private fun request(url: HttpUrl): Request.Builder = Request.Builder().url(url) - .header("Authorization", "MediaBrowser Token=\"$jellyfinToken\"") - .header("Accept", "application/json") - - private fun url(contract: RouteContract, segments: Map, query: Map): HttpUrl { - val builder = base.newBuilder() - for (segment in contract.path.trimStart('/').split('/')) { - builder.addPathSegment(if (segment.startsWith('{')) segments.getValue(segment.removeSurrounding("{", "}")) else segment) - } - query.toSortedMap().forEach { (name, value) -> builder.addQueryParameter(name, value) } - return builder.build() - } - - private fun decodeSaved(serializer: KSerializer, entry: CacheEntry, fromDisk: Boolean): Saved? = try { - Saved(if (entry.absent) null else WireJson.decodeFromString(serializer, entry.body), entry.savedAtMillis, fromDisk, entry.absent) - } catch (_: SerializationException) { null } - - private fun cacheRead(key: String): CacheEntry? = try { cache.read(scope, key) } catch (_: IOException) { - onEvent(ClientEvent.CACHE_UNAVAILABLE) - null - } - - private fun cacheWrite(key: String, entry: CacheEntry, policy: CachePolicy) = synchronized(cacheIdentityLock) { - if (!revoked.get()) { - try { cache.write(scope, key, entry, policy.capacity) } catch (_: IOException) { onEvent(ClientEvent.CACHE_UNAVAILABLE) } - } - } - - private fun revoke(): ClientError { - synchronized(cacheIdentityLock) { - revoked.set(true) - try { cache.clear(scope) } catch (_: IOException) { onEvent(ClientEvent.CACHE_UNAVAILABLE) } - } - onEvent(ClientEvent.NEEDS_SIGN_IN) - return ClientError.NeedsSignIn - } - - private fun feedExpiry(policy: CachePolicy, body: String): Long? { - if (policy != CachePolicy.FEED) return null - val expiry = WireJson.parseToJsonElement(body).jsonObject["expires_at"]?.jsonPrimitive?.content ?: return null - return try { OffsetDateTime.parse(expiry).toInstant().toEpochMilli() } catch (_: DateTimeParseException) { null } - } - - /** Release this instance's network resources; disk LKG survives. */ - override fun close() { - http.dispatcher.cancelAll() - http.dispatcher.executorService.shutdown() - http.connectionPool.evictAll() - } -} - -private suspend fun Call.awaitResponse(): Response = suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { cancel() } - enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - if (!continuation.isCancelled) continuation.resumeWithException(e) - } - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) { _, value, _ -> value.close() } - } - }) -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/FileCacheStore.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/FileCacheStore.kt deleted file mode 100644 index fe34a9b2..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/FileCacheStore.kt +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr - -import java.io.File -import java.io.IOException -import java.security.MessageDigest -import kotlinx.serialization.SerializationException - -/** App-private disk cache, safe to reopen after process death; one store instance per directory. */ -class FileCacheStore(private val directory: File) : CacheStore { - @Synchronized override fun read(scope: String, key: String): CacheEntry? { - val file = entryFile(scope, key) - if (!file.exists()) return null - return try { - WireJson.decodeFromString(file.readText()) - } catch (_: SerializationException) { - file.delete() - null - } - } - - @Synchronized override fun write(scope: String, key: String, entry: CacheEntry, capacity: Int?) { - val target = entryFile(scope, key) - val parent = target.parentFile!! - if (!parent.isDirectory && !parent.mkdirs()) throw IOException("Cache directory unavailable") - val temporary = File.createTempFile("entry-", ".tmp", parent) - try { - temporary.outputStream().use { output -> - output.write(WireJson.encodeToString(CacheEntry.serializer(), entry).toByteArray(Charsets.UTF_8)) - output.fd.sync() - } - if (!temporary.renameTo(target)) throw IOException("Cache replacement failed") - target.setLastModified(entry.validatedAtMillis) - } finally { - temporary.delete() - } - if (capacity != null) { - parent.listFiles { file -> file.extension == "json" } - ?.sortedByDescending { it.lastModified() }?.drop(capacity)?.forEach { it.delete() } - } - } - - @Synchronized override fun remove(scope: String, key: String) { entryFile(scope, key).delete() } - @Synchronized override fun clear(scope: String) { File(directory, digest(scope)).deleteRecursively() } - - private fun entryFile(scope: String, key: String): File = - File(File(File(directory, digest(scope)), digest(key.substringBefore(':'))), digest(key) + ".json") -} - -/** Opaque storage names never expose credentials, query parameters or household names. */ -internal fun digest(value: String): String { - val bytes = MessageDigest.getInstance("SHA-256").digest(value.toByteArray(Charsets.UTF_8)) - val hex = "0123456789abcdef" - val characters = CharArray(bytes.size * 2) - bytes.forEachIndexed { index, byte -> - val number = byte.toInt() and 0xff - characters[index * 2] = hex[number ushr 4] - characters[index * 2 + 1] = hex[number and 0x0f] - } - return String(characters) -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/TvRoutes.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/TvRoutes.kt deleted file mode 100644 index f567fee7..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/TvRoutes.kt +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr - -import kotlinx.serialization.KSerializer -import org.justinvisser.wolfskin.conductarr.model.* - -/** A TV-used operation and its pinned request/response contract. */ -class RouteContract internal constructor( - val method: String, - val path: String, - val responseSchema: String, - val requestSchema: String? = null, -) - -/** A typed read; only TvRoutes can construct paths and policies. */ -class ReadCall internal constructor( - internal val contract: RouteContract, - internal val serializer: KSerializer, - internal val segments: Map = emptyMap(), - internal val query: Map = emptyMap(), - internal val policy: CachePolicy, -) - -/** A single mutation; failures are returned without automatic retry or replay. */ -class MutationCall internal constructor( - internal val contract: RouteContract, - internal val serializer: KSerializer, - internal val segments: Map, - internal val body: String? = null, -) - -/** The catalogue media type accepted by TV operations. */ -enum class MediaType(internal val wire: String) { MOVIE("movie"), TV("tv") } - -/** Feed filter; the unfiltered feed uses all. */ -enum class MediaFilter(internal val wire: String) { ALL("all"), MOVIE("movie"), TV("tv") } - -/** The four discovery sorts used by the TV. */ -enum class DiscoverSort(internal val wire: String) { POPULAR("popular"), NEW("new"), TOP("top"), UPCOMING("upcoming") } - -/** The complete TV route allowlist; caller identity is never a request body field. */ -object TvRoutes { - private val operator = RouteContract("GET", "/api/v1/operator", "Operator") - private val profiles = RouteContract("GET", "/api/v1/recommendations/profiles", "RecommendationProfiles") - private val feed = RouteContract("GET", "/api/v1/recommendations/profiles/{profile}/feed", "RecommendationFeed") - private val dismissals = RouteContract("GET", "/api/v1/recommendations/profiles/{profile}/dismissals", "RecommendationDismissals") - private val hide = RouteContract("POST", dismissals.path, "RecommendationHideResult", "RecommendationHideInputBody") - private val restore = RouteContract("DELETE", "${dismissals.path}/{type}/{tmdb}", "RecommendationRestoreResult") - private val options = RouteContract("GET", "/catalogue/options", "Options") - private val discover = RouteContract("GET", "/catalogue/discover", "Page") - private val title = RouteContract("GET", "/catalogue/titles/{tmdb}/{type}", "Detail") - private val cast = RouteContract("GET", "${title.path}/cast", "TitleCast") - private val related = RouteContract("GET", "${title.path}/related", "Page") - private val enrichment = RouteContract("GET", "${title.path}/enrichment", "Enrichment") - private val season = RouteContract("GET", "/catalogue/tv/{tmdb}/seasons/{season}", "SeasonDetail") - private val performer = RouteContract("GET", "/catalogue/performers/{performer}", "PerformerDetail") - private val filmography = RouteContract("GET", "${performer.path}/filmography", "FilmographyPage") - private val activity = RouteContract("GET", "/api/v1/activity", "ActivityFeed") - private val readiness = RouteContract("GET", "/api/v1/activity/titles/{tmdb}/{type}", "ViewerReadiness") - private val requests = RouteContract("GET", "/api/v1/requests/titles/{tmdb}/{type}", "TitleRequestsBody") - private val request = RouteContract("POST", "/api/v1/requests", "RequestReceipt", "RequestIntentBody") - private val audiences = RouteContract("GET", "/curation/library-audiences", "LibraryAudiencesBody") - - val contracts: List = listOf( - operator, profiles, feed, dismissals, hide, restore, options, discover, title, cast, - related, enrichment, season, performer, filmography, activity, readiness, requests, request, audiences, - ) - - fun operator() = ReadCall(operator, Operator.serializer(), policy = CachePolicy.LAUNCH) - fun profiles() = ReadCall(profiles, RecommendationProfiles.serializer(), policy = CachePolicy.LAUNCH) - fun audiences() = ReadCall(audiences, LibraryAudiencesBody.serializer(), policy = CachePolicy.DAILY) - - fun feed(profile: String, media: MediaFilter = MediaFilter.ALL) = ReadCall( - feed, RecommendationFeed.serializer(), mapOf("profile" to profile), - mapOf("media_type" to media.wire), CachePolicy.FEED, - ) - - fun dismissals(profile: String) = ReadCall( - dismissals, RecommendationDismissals.serializer(), mapOf("profile" to profile), policy = CachePolicy.ON_OPEN, - ) - - fun options(media: MediaType) = ReadCall( - options, Options.serializer(), query = mapOf("media_type" to media.wire), policy = CachePolicy.FOREGROUND, - ) - - fun discover(media: MediaType, page: Int = 1, genreId: Long? = null, serviceId: Long? = null, sort: DiscoverSort = DiscoverSort.POPULAR): ReadCall { - require(page in 1..500) - val query = buildMap { - put("media_type", media.wire) - put("page", page.toString()) - put("sort", sort.wire) - genreId?.let { put("genre_id", it.toString()) } - serviceId?.let { put("service_id", it.toString()) } - } - return ReadCall(discover, Page.serializer(), query = query, policy = CachePolicy.DISCOVERY) - } - - fun title(tmdb: Long, media: MediaType) = ReadCall(title, Detail.serializer(), titleSegments(tmdb, media), policy = CachePolicy.ON_OPEN) - fun cast(tmdb: Long, media: MediaType) = ReadCall(cast, TitleCast.serializer(), titleSegments(tmdb, media), policy = CachePolicy.ON_OPEN) - fun enrichment(tmdb: Long, media: MediaType) = ReadCall(enrichment, Enrichment.serializer(), titleSegments(tmdb, media), policy = CachePolicy.RATINGS) - fun related(tmdb: Long, media: MediaType, page: Int = 1) = ReadCall( - related, Page.serializer(), titleSegments(tmdb, media), mapOf("page" to page.toString()), CachePolicy.ON_OPEN, - ) - fun season(tmdb: Long, season: Int): ReadCall { - require(tmdb > 0 && season >= 0) - return ReadCall(this.season, SeasonDetail.serializer(), mapOf("tmdb" to tmdb.toString(), "season" to season.toString()), policy = CachePolicy.SEASON) - } - fun performer(performer: Long) = ReadCall( - this.performer, PerformerDetail.serializer(), mapOf("performer" to performer.toString()), policy = CachePolicy.ON_OPEN, - ) - fun filmography(performer: Long, page: Int = 1, pageSize: Int = 20) = ReadCall( - filmography, FilmographyPage.serializer(), mapOf("performer" to performer.toString()), - mapOf("page" to page.toString(), "page_size" to pageSize.toString()), CachePolicy.ON_OPEN, - ) - fun activity() = ReadCall(activity, ActivityFeed.serializer(), query = mapOf("requester" to "all"), policy = CachePolicy.ACTIVITY) - fun readiness(tmdb: Long, media: MediaType, viewer: String) = ReadCall( - readiness, ViewerReadiness.serializer(), titleSegments(tmdb, media), mapOf("viewer" to viewer), CachePolicy.ON_OPEN, - ) - fun requests(tmdb: Long, media: MediaType) = ReadCall( - requests, TitleRequestsBody.serializer(), titleSegments(tmdb, media), policy = CachePolicy.NO_CACHE, - ) - fun request(body: RequestIntentBody) = MutationCall( - request, RequestReceipt.serializer(), emptyMap(), WireJson.encodeToString(RequestIntentBody.serializer(), body), - ) - fun hide(profile: String, body: RecommendationHideInputBody) = MutationCall( - hide, RecommendationHideResult.serializer(), mapOf("profile" to profile), - WireJson.encodeToString(RecommendationHideInputBody.serializer(), body), - ) - fun restore(profile: String, tmdb: Long, media: MediaType) = MutationCall( - restore, RecommendationRestoreResult.serializer(), titleSegments(tmdb, media) + ("profile" to profile), - ) - - private fun titleSegments(tmdb: Long, media: MediaType): Map { - require(tmdb > 0) - return mapOf("tmdb" to tmdb.toString(), "type" to media.wire) - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/WireJson.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/WireJson.kt deleted file mode 100644 index 1e80eb25..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/WireJson.kt +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr - -import kotlinx.serialization.json.Json - -/** Shared wire encoding omits absent optional request fields. */ -internal val WireJson = Json { - ignoreUnknownKeys = true - explicitNulls = false - encodeDefaults = false -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ActingCredit.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ActingCredit.kt deleted file mode 100644 index fe181459..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ActingCredit.kt +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** ActingCredit wire contract from the pinned Conductarr schema. */ -@Serializable -data class ActingCredit( - @SerialName("character") val character: String, - @SerialName("credit_id") val creditId: String, - @SerialName("episode_count") val episodeCount: Long? = null, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ActivityFeed.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ActivityFeed.kt deleted file mode 100644 index 8f36f041..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ActivityFeed.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** ActivityFeed wire contract from the pinned Conductarr schema. */ -@Serializable -data class ActivityFeed( - @SerialName("items") val items: List?, - @SerialName("requester") val requester: String, - @SerialName("sources") val sources: List?, - @SerialName("truncated") val truncated: Boolean, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Actor.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Actor.kt deleted file mode 100644 index 36ff72fd..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Actor.kt +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Actor wire contract from the pinned Conductarr schema. */ -@Serializable -data class Actor( - @SerialName("name") val name: String, - @SerialName("requester") val requester: String, - @SerialName("role") val role: Role, -) { - /** Wire values for role. */ - @Serializable - enum class Role { - @SerialName("admin") ADMIN, - @SerialName("household") HOUSEHOLD, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Candidate.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Candidate.kt deleted file mode 100644 index 81ef5607..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Candidate.kt +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Candidate wire contract from the pinned Conductarr schema. */ -@Serializable -data class Candidate( - @SerialName("backdrop_path") val backdropPath: String? = null, - @SerialName("contributions") val contributions: List?, - @SerialName("genre_ids") val genreIds: List? = null, - @SerialName("media_type") val mediaType: MediaType, - @SerialName("overview") val overview: String, - @SerialName("poster_path") val posterPath: String, - @SerialName("score") val score: Double, - @SerialName("title") val title: String, - @SerialName("tmdb_id") val tmdbId: Long, - @SerialName("tvdb_id") val tvdbId: Long, - @SerialName("year") val year: Long, -) { - /** Wire values for media_type. */ - @Serializable - enum class MediaType { - @SerialName("movie") MOVIE, - @SerialName("tv") TV, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/CastMember.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/CastMember.kt deleted file mode 100644 index 9dca14e1..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/CastMember.kt +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** CastMember wire contract from the pinned Conductarr schema. */ -@Serializable -data class CastMember( - @SerialName("character") val character: String, - @SerialName("credit_id") val creditId: String, - @SerialName("name") val name: String, - @SerialName("order") val order: Long, - @SerialName("performer_id") val performerId: Long, - @SerialName("profile_url") val profileUrl: String? = null, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Detail.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Detail.kt deleted file mode 100644 index 85cd9a88..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Detail.kt +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Detail wire contract from the pinned Conductarr schema. */ -@Serializable -data class Detail( - @SerialName("genres") val genres: List?, - @SerialName("held_title") val heldTitle: HeldTitle? = null, - @SerialName("metadata") val metadata: TitleMetadata, - @SerialName("metadata_state") val metadataState: MetadataState, - @SerialName("observations") val observations: List?, - @SerialName("ratings") val ratings: List? = null, - @SerialName("runtime") val runtime: Long? = null, - @SerialName("seasons") val seasons: List?, - @SerialName("state") val state: State, - @SerialName("title") val title: Title, -) { - /** Wire values for metadata_state. */ - @Serializable - enum class MetadataState { - @SerialName("observed") OBSERVED, - @SerialName("missing") MISSING, - @SerialName("unavailable") UNAVAILABLE, - } - - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("partial") PARTIAL, - @SerialName("unavailable") UNAVAILABLE, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Discovery.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Discovery.kt deleted file mode 100644 index a198abd1..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Discovery.kt +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Discovery wire contract from the pinned Conductarr schema. */ -@Serializable -data class Discovery( - @SerialName("genre_id") val genreId: Long, - @SerialName("next") val next: String? = null, - @SerialName("region") val region: String, - @SerialName("service_id") val serviceId: Long, - @SerialName("sort") val sort: Sort, - @SerialName("state") val state: State, -) { - /** Wire values for sort. */ - @Serializable - enum class Sort { - @SerialName("trending") TRENDING, - @SerialName("popular") POPULAR, - @SerialName("new") NEW, - @SerialName("top") TOP, - @SerialName("upcoming") UPCOMING, - } - - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("empty") EMPTY, - @SerialName("partial") PARTIAL, - @SerialName("exhausted") EXHAUSTED, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Enrichment.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Enrichment.kt deleted file mode 100644 index 4329d346..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Enrichment.kt +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Enrichment wire contract from the pinned Conductarr schema. */ -@Serializable -data class Enrichment( - @SerialName("genres") val genres: List?, - @SerialName("metadata") val metadata: TitleMetadata, - @SerialName("metadata_state") val metadataState: MetadataState, - @SerialName("observations") val observations: List?, - @SerialName("ratings") val ratings: List?, - @SerialName("runtime") val runtime: Long? = null, - @SerialName("seasons") val seasons: List?, - @SerialName("state") val state: State, - @SerialName("title") val title: Title, -) { - /** Wire values for metadata_state. */ - @Serializable - enum class MetadataState { - @SerialName("observed") OBSERVED, - @SerialName("missing") MISSING, - @SerialName("unavailable") UNAVAILABLE, - } - - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("unavailable") UNAVAILABLE, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Episode.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Episode.kt deleted file mode 100644 index 212a0b87..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Episode.kt +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Episode wire contract from the pinned Conductarr schema. */ -@Serializable -data class Episode( - @SerialName("air_date") val airDate: String? = null, - @SerialName("name") val name: String, - @SerialName("number") val number: Long, - @SerialName("overview") val overview: String, - @SerialName("recorded") val recorded: Recorded, -) { - /** Wire values for recorded. */ - @Serializable - enum class Recorded { - @SerialName("recorded") RECORDED, - @SerialName("missing") MISSING, - @SerialName("unknown") UNKNOWN, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeFileChip.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeFileChip.kt deleted file mode 100644 index b0c2a091..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeFileChip.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** EpisodeFileChip wire contract from the pinned Conductarr schema. */ -@Serializable -data class EpisodeFileChip( - @SerialName("quality") val quality: String? = null, - @SerialName("release_name") val releaseName: String? = null, - @SerialName("resolution") val resolution: String? = null, - @SerialName("size_bytes") val sizeBytes: Long, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeRow.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeRow.kt deleted file mode 100644 index 1a56793b..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeRow.kt +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** EpisodeRow wire contract from the pinned Conductarr schema. */ -@Serializable -data class EpisodeRow( - @SerialName("air_date") val airDate: String? = null, - @SerialName("episode") val episode: Long, - @SerialName("file") val file: EpisodeFileChip? = null, - @SerialName("has_file") val hasFile: Boolean, - @SerialName("key") val key: String, - @SerialName("monitored") val monitored: Boolean, - @SerialName("queue_state") val queueState: String? = null, - @SerialName("runtime_minutes") val runtimeMinutes: Long? = null, - @SerialName("season") val season: Long, - @SerialName("title") val title: String? = null, - @SerialName("watched") val watched: Map, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeView.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeView.kt deleted file mode 100644 index aab5e114..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EpisodeView.kt +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** EpisodeView wire contract from the pinned Conductarr schema. */ -@Serializable -data class EpisodeView( - @SerialName("owners") val owners: List?, - @SerialName("seasons") val seasons: List?, - @SerialName("title") val title: String, - @SerialName("tmdb_id") val tmdbId: Long, - @SerialName("users") val users: List?, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ErrorDetail.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ErrorDetail.kt deleted file mode 100644 index 3aef3a95..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ErrorDetail.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement - -/** ErrorDetail wire contract from the pinned Conductarr schema. */ -@Serializable -data class ErrorDetail( - @SerialName("location") val location: String? = null, - @SerialName("message") val message: String? = null, - @SerialName("value") val `value`: JsonElement? = null, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ErrorModel.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ErrorModel.kt deleted file mode 100644 index 1c926492..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ErrorModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** ErrorModel wire contract from the pinned Conductarr schema. */ -@Serializable -data class ErrorModel( - @SerialName("detail") val detail: String? = null, - @SerialName("errors") val errors: List? = null, - @SerialName("instance") val instance: String? = null, - @SerialName("status") val status: Long? = null, - @SerialName("title") val title: String? = null, - @SerialName("type") val type: String? = null, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EvidenceSource.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EvidenceSource.kt deleted file mode 100644 index 3d3fd6e5..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/EvidenceSource.kt +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** EvidenceSource wire contract from the pinned Conductarr schema. */ -@Serializable -data class EvidenceSource( - @SerialName("detail") val detail: String? = null, - @SerialName("name") val name: String, - @SerialName("state") val state: State, -) { - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("unavailable") UNAVAILABLE, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FileDetail.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FileDetail.kt deleted file mode 100644 index 5fc58321..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FileDetail.kt +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** FileDetail wire contract from the pinned Conductarr schema. */ -@Serializable -data class FileDetail( - @SerialName("audio") val audio: String? = null, - @SerialName("container") val container: String? = null, - @SerialName("quality") val quality: String? = null, - @SerialName("release_name") val releaseName: String? = null, - @SerialName("resolution") val resolution: String? = null, - @SerialName("size_bytes") val sizeBytes: Long, - @SerialName("video") val video: String? = null, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FilmographyPage.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FilmographyPage.kt deleted file mode 100644 index 22328337..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FilmographyPage.kt +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** FilmographyPage wire contract from the pinned Conductarr schema. */ -@Serializable -data class FilmographyPage( - @SerialName("completeness") val completeness: Completeness, - @SerialName("items") val items: List?, - @SerialName("metadata_state") val metadataState: MetadataState, - @SerialName("observations") val observations: List?, - @SerialName("page") val page: Long, - @SerialName("page_size") val pageSize: Long, - @SerialName("performer_id") val performerId: Long, - @SerialName("source") val source: Source, - @SerialName("source_paging") val sourcePaging: SourcePaging, - @SerialName("total_pages") val totalPages: Long? = null, - @SerialName("total_results") val totalResults: Long? = null, -) { - /** Wire values for completeness. */ - @Serializable - enum class Completeness { - @SerialName("returned-non-adult-cast") RETURNED_NON_ADULT_CAST, - } - - /** Wire values for metadata_state. */ - @Serializable - enum class MetadataState { - @SerialName("observed") OBSERVED, - @SerialName("missing") MISSING, - @SerialName("unavailable") UNAVAILABLE, - } - - /** Wire values for source. */ - @Serializable - enum class Source { - @SerialName("seerr") SEERR, - } - - /** Wire values for source_paging. */ - @Serializable - enum class SourcePaging { - @SerialName("unpaged") UNPAGED, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FilmographyTitle.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FilmographyTitle.kt deleted file mode 100644 index 69115f23..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/FilmographyTitle.kt +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** FilmographyTitle wire contract from the pinned Conductarr schema. */ -@Serializable -data class FilmographyTitle( - @SerialName("credits") val credits: List?, - @SerialName("title") val title: Title, - @SerialName("users") val users: List?, - @SerialName("visibility_state") val visibilityState: VisibilityState, -) { - /** Wire values for visibility_state. */ - @Serializable - enum class VisibilityState { - @SerialName("configured") CONFIGURED, - @SerialName("unknown") UNKNOWN, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Genre.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Genre.kt deleted file mode 100644 index fff1dbad..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Genre.kt +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Genre wire contract from the pinned Conductarr schema. */ -@Serializable -data class Genre( - @SerialName("id") val id: Long, - @SerialName("name") val name: String, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/HealthDetail.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/HealthDetail.kt deleted file mode 100644 index 76247a3a..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/HealthDetail.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** HealthDetail wire contract from the pinned Conductarr schema. */ -@Serializable -data class HealthDetail( - @SerialName("failed") val failed: Boolean, - @SerialName("has_file") val hasFile: Boolean, - @SerialName("in_queue") val inQueue: Boolean, - @SerialName("missing") val missing: Boolean, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/HeldTitle.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/HeldTitle.kt deleted file mode 100644 index e3e6fbba..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/HeldTitle.kt +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** HeldTitle wire contract from the pinned Conductarr schema. */ -@Serializable -data class HeldTitle( - @SerialName("detail") val detail: ItemDetail, - @SerialName("episodes") val episodes: EpisodeView? = null, - @SerialName("libraries") val libraries: List?, - @SerialName("observations") val observations: List?, - @SerialName("state") val state: State, - @SerialName("versions") val versions: List?, - @SerialName("watched") val watched: Map, -) { - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("partial") PARTIAL, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ItemDetail.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ItemDetail.kt deleted file mode 100644 index 17b7357b..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/ItemDetail.kt +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** ItemDetail wire contract from the pinned Conductarr schema. */ -@Serializable -data class ItemDetail( - @SerialName("backdrop_ref") val backdropRef: String? = null, - @SerialName("file") val file: FileDetail? = null, - @SerialName("health") val health: HealthDetail, - @SerialName("is_anime") val isAnime: Boolean, - @SerialName("media_type") val mediaType: String, - @SerialName("owners") val owners: List?, - @SerialName("poster_ref") val posterRef: String? = null, - @SerialName("runtime") val runtime: Long? = null, - @SerialName("source") val source: SourceDetail, - @SerialName("title") val title: String, - @SerialName("tmdb_id") val tmdbId: Long, - @SerialName("users") val users: List?, - @SerialName("year") val year: Long? = null, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryAudience.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryAudience.kt deleted file mode 100644 index 11c69a33..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryAudience.kt +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** LibraryAudience wire contract from the pinned Conductarr schema. */ -@Serializable -data class LibraryAudience( - @SerialName("is_anime") val isAnime: Boolean, - @SerialName("libraries") val libraries: List?, - @SerialName("media_type") val mediaType: MediaType, - @SerialName("owner") val owner: String, - @SerialName("users") val users: List?, -) { - /** Wire values for media_type. */ - @Serializable - enum class MediaType { - @SerialName("movie") MOVIE, - @SerialName("tv") TV, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryAudiencesBody.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryAudiencesBody.kt deleted file mode 100644 index b3ed0ebf..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryAudiencesBody.kt +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** LibraryAudiencesBody wire contract from the pinned Conductarr schema. */ -@Serializable -data class LibraryAudiencesBody( - @SerialName("audiences") val audiences: List?, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryObservation.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryObservation.kt deleted file mode 100644 index e439b750..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/LibraryObservation.kt +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** LibraryObservation wire contract from the pinned Conductarr schema. */ -@Serializable -data class LibraryObservation( - @SerialName("detail") val detail: String? = null, - @SerialName("observed_at") val observedAt: String? = null, - @SerialName("source") val source: String, - @SerialName("state") val state: State, -) { - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("partial") PARTIAL, - @SerialName("unavailable") UNAVAILABLE, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/OpenLink.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/OpenLink.kt deleted file mode 100644 index 75d1e996..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/OpenLink.kt +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** OpenLink wire contract from the pinned Conductarr schema. */ -@Serializable -data class OpenLink( - @SerialName("detail") val detail: String? = null, - @SerialName("state") val state: State, - @SerialName("url") val url: String? = null, -) { - /** Wire values for state. */ - @Serializable - enum class State { - @SerialName("observed") OBSERVED, - @SerialName("missing") MISSING, - @SerialName("unavailable") UNAVAILABLE, - @SerialName("unconfigured") UNCONFIGURED, - @SerialName("not_permitted") NOT_PERMITTED, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Operator.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Operator.kt deleted file mode 100644 index edb9f0cf..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Operator.kt +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Operator wire contract from the pinned Conductarr schema. */ -@Serializable -data class Operator( - @SerialName("capabilities") val capabilities: OperatorCapabilities, - @SerialName("name") val name: String, - @SerialName("recommendation_profile") val recommendationProfile: String, - @SerialName("requester") val requester: String, - @SerialName("role") val role: Role, - @SerialName("subject") val subject: String, - @SerialName("watch_user") val watchUser: String, -) { - /** Wire values for role. */ - @Serializable - enum class Role { - @SerialName("admin") ADMIN, - @SerialName("household") HOUSEHOLD, - } -} diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/OperatorCapabilities.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/OperatorCapabilities.kt deleted file mode 100644 index 777f1c03..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/OperatorCapabilities.kt +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** OperatorCapabilities wire contract from the pinned Conductarr schema. */ -@Serializable -data class OperatorCapabilities( - @SerialName("administer") val administer: Boolean, - @SerialName("manage_library") val manageLibrary: Boolean, - @SerialName("manage_memberships") val manageMemberships: Boolean, - @SerialName("recommendation_profiles") val recommendationProfiles: List?, - @SerialName("request_media") val requestMedia: Boolean, - @SerialName("requesters") val requesters: List?, - @SerialName("watch_users") val watchUsers: List?, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Option.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Option.kt deleted file mode 100644 index ebd7b132..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Option.kt +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Option wire contract from the pinned Conductarr schema. */ -@Serializable -data class Option( - @SerialName("id") val id: Long, - @SerialName("logo_url") val logoUrl: String? = null, - @SerialName("name") val name: String, -) diff --git a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Options.kt b/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Options.kt deleted file mode 100644 index b0a9839b..00000000 --- a/conductarr-client/src/main/kotlin/org/justinvisser/wolfskin/conductarr/model/Options.kt +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.conductarr.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Options wire contract from the pinned Conductarr schema. */ -@Serializable -data class Options( - @SerialName("genres") val genres: List