Repository: IgnisDa/ryot (language: rust)

Issue:
[FEATURE REQUEST] - Tracking actors and directors upcoming projects
I am not sure if this is already a feature and i just cant find it.
I do use the "monitor a person" feature. But it would be great to have an option to also see upcoming projects. Maybe even show those in the calendar. 
Also i really like the way "letterboxd" visualizes projects a director or actor worked on. 
A nice little feature is their percentage/number of watched projects per actor/director.

![grafik](https://github.com/IgnisDa/ryot/assets/83821430/e033bcd4-f74a-4600-876f-a5ce32db6cf3)


Discussion / hints:
@IO-B On a different note, did you try importing data from letterboxd?
no, i have not. I was just looking through different tools.
Not sure if I should open a new issue but I would wish something similar. Some days ago I got this notification:

> Jackie Chan has been associated with Police Story 2 as Theme Song Performance
> Jackie Chan has been associated with Police Story 3: Super Cop as Presenter

That is information I don't really need as the movies are from the 90s and of course Jackie Chan is included but it seems that somebody added some info to TVDB or IMDB and triggered this if I understand it correctly.

It would be nice to have the option to only watch for upcoming projects or at least of the same year and older. It makes no sense for such old movies.

Fix patch:
diff --git a/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
index 60bcbb64bd..961c7a12b6 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
@@ -1,10 +1,10 @@
 import {
 	Anchor,
-	Box,
 	Button,
 	Container,
 	Group,
 	Menu,
+	Select,
 	SimpleGrid,
 	Stack,
 	Tabs,
@@ -18,12 +18,14 @@ import {
 	PersonDetailsDocument,
 	UserPersonDetailsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
+import { sum } from "@ryot/ts-utils";
 import {
 	IconDeviceTv,
 	IconInfoCircle,
 	IconMessageCircle2,
 	IconUser,
 } from "@tabler/icons-react";
+import { useLocalStorage } from "usehooks-ts";
 import { z } from "zod";
 import { zx } from "zodix";
 import {
@@ -41,7 +43,7 @@ import { useAddEntityToCollection, useReviewEntity } from "~/lib/state/media";
 import { serverGqlService } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	defaultTab: z.string().optional().default("media"),
+	defaultTab: z.string().optional(),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
@@ -67,6 +69,14 @@ export default function Page() {
 	const userPreferences = useUserPreferences();
 	const [_r, setEntityToReview] = useReviewEntity();
 	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
+	const [roleFilter, setRoleFilter] = useLocalStorage(
+		"MediaTabRoleFilter",
+		loaderData.personDetails.contents.map((c) => c.name).at(0) || null,
+	);
+
+	const totalMetadata = sum(
+		loaderData.personDetails.contents.map((c) => c.count),
+	);
 
 	return (
 		<Container>
@@ -87,9 +97,7 @@ export default function Page() {
 				</Title>
 				<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
 					{[
-						`${
-							loaderData.personDetails.contents.flatMap((c) => c.items).length
-						} media items`,
+						`${totalMetadata} media items`,
 						loaderData.personDetails.details.birthDate &&
 							`Birth: ${loaderData.personDetails.details.birthDate}`,
 						loaderData.personDetails.details.deathDate &&
@@ -126,11 +134,19 @@ export default function Page() {
 						))}
 					</Group>
 				) : null}
-				<Tabs variant="outline" defaultValue={loaderData.query.defaultTab}>
+				<Tabs
+					variant="outline"
+					defaultValue={
+						loaderData.query.defaultTab ||
+						(totalMetadata > 0 ? "media" : "actions")
+					}
+				>
 					<Tabs.List mb="xs">
-						<Tabs.Tab value="media" leftSection={<IconDeviceTv size={16} />}>
-							Media
-						</Tabs.Tab>
+						{totalMetadata > 0 ? (
+							<Tabs.Tab value="media" leftSection={<IconDeviceTv size={16} />}>
+								Media
+							</Tabs.Tab>
+						) : null}
 						{loaderData.personDetails.details.description ? (
 							<Tabs.Tab
 								value="overview"
@@ -153,25 +169,32 @@ export default function Page() {
 					</Tabs.List>
 					<Tabs.Panel value="media">
 						<MediaScrollArea>
-							<Stack>
-								{loaderData.personDetails.contents.map((role) => (
-									<Box key={role.name}>
-										<Title order={3} mb="xs" ta="center">
-											{role.name}
-										</Title>
-										<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
-											{role.items.map((item) => (
-												<PartialMetadataDisplay
-													key={item.mediaId}
-													metadataId={item.mediaId}
-													extraText={
-														item.character ? `as ${item.character}` : undefined
-													}
-												/>
-											))}
-										</SimpleGrid>
-									</Box>
-								))}
+							<Stack gap="xl">
+								<Group justify="center">
+									<Text size="sm" c="dimmed">
+										Role:
+									</Text>
+									<Select
+										size="xs"
+										value={roleFilter}
+										onChange={(value) => setRoleFilter(value)}
+										data={loaderData.personDetails.contents.map((c) => ({
+											value: c.name,
+											label: `${c.name} (${c.count})`,
+										}))}
+									/>
+								</Group>
+								<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
+									{loaderData.personDetails.contents
+										.find((c) => c.name === roleFilter)
+										?.items.map((item) => (
+											<MetadataDisplay
+												key={item.metadataId}
+												character={item.character}
+												metadataId={item.metadataId}
+											/>
+										))}
+								</SimpleGrid>
 							</Stack>
 						</MediaScrollArea>
 					</Tabs.Panel>
@@ -261,3 +284,15 @@ export default function Page() {
 		</Container>
 	);
 }
+
+const MetadataDisplay = (props: {
+	metadataId: string;
+	character?: string | null;
+}) => {
+	return (
+		<PartialMetadataDisplay
+			metadataId={props.metadataId}
+			extraText={props.character ? `as ${props.character}` : undefined}
+		/>
+	);
+};
diff --git a/crates/migrations/src/lib.rs b/crates/migrations/src/lib.rs
index d98570a109..00116d8251 100644
--- a/crates/migrations/src/lib.rs
+++ b/crates/migrations/src/lib.rs
@@ -42,6 +42,7 @@ mod m20240928_add_grid_packing_to_general_preferences;
 mod m20241002_add_columns_for_associating_seen_with_reviews;
 mod m20241004_create_application_cache;
 mod m20241006_changes_for_issue_1056;
+mod m20241010_changes_for_issue_708;
 
 pub use m20230410_create_metadata::Metadata as AliasedMetadata;
 pub use m20230413_create_person::Person as AliasedPerson;
@@ -104,6 +105,7 @@ impl MigratorTrait for Migrator {
             Box::new(m20241002_add_columns_for_associating_seen_with_reviews::Migration),
             Box::new(m20241004_create_application_cache::Migration),
             Box::new(m20241006_changes_for_issue_1056::Migration),
+            Box::new(m20241010_changes_for_issue_708::Migration),
         ]
     }
 }
diff --git a/crates/migrations/src/m20241010_changes_for_issue_708.rs b/crates/migrations/src/m20241010_changes_for_issue_708.rs
new file mode 100644
index 0000000000..16305d058c
--- /dev/null
+++ b/crates/migrations/src/m20241010_changes_for_issue_708.rs
@@ -0,0 +1,29 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        db.execute_unprepared(
+            r#"
+UPDATE "person"
+SET "is_partial" = true
+WHERE EXISTS (
+    SELECT 1
+    FROM "metadata_to_person" mp
+    WHERE mp."person_id" = "person"."id"
+);
+DELETE FROM "metadata_to_person";
+"#,
+        )
+        .await?;
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs
index 5d29d22939..97bd84e98c 100644
--- a/crates/models/media/src/lib.rs
+++ b/crates/models/media/src/lib.rs
@@ -471,19 +471,26 @@ pub struct PartialMetadataPerson {
     pub source_specifics: Option<PersonSourceSpecifics>,
 }
 
+#[derive(Debug, Serialize, Deserialize, Clone, Hash)]
+pub struct MetadataPersonRelated {
+    pub role: String,
+    pub character: Option<String>,
+    pub metadata: PartialMetadataWithoutId,
+}
+
 #[derive(Debug, Serialize, Deserialize, Clone, Hash)]
 pub struct MetadataPerson {
+    pub name: String,
     pub identifier: String,
     pub source: MediaSource,
-    pub name: String,
+    pub gender: Option<String>,
+    pub place: Option<String>,
+    pub website: Option<String>,
     pub description: Option<String>,
     pub images: Option<Vec<String>>,
-    pub gender: Option<String>,
     pub death_date: Option<NaiveDate>,
     pub birth_date: Option<NaiveDate>,
-    pub place: Option<String>,
-    pub website: Option<String>,
-    pub related: Vec<(String, PartialMetadataWithoutId)>,
+    pub related: Vec<MetadataPersonRelated>,
     pub source_specifics: Option<PersonSourceSpecifics>,
 }
 
@@ -1269,12 +1276,14 @@ pub struct MetadataCreatorGroupedByRole {
 
 #[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
 pub struct PersonDetailsItemWithCharacter {
-    pub media_id: String,
+    pub metadata_id: String,
     pub character: Option<String>,
 }
 
 #[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
 pub struct PersonDetailsGroupedByRole {
+    /// The number of items in this role.
+    pub count: usize,
     /// The name of the role performed.
     pub name: String,
     /// The media items in which this role was performed.
diff --git a/crates/providers/src/anilist/mod.rs b/crates/providers/src/anilist/mod.rs
index e3ec8a7bc4..5c47599ed2 100644
--- a/crates/providers/src/anilist/mod.rs
+++ b/crates/providers/src/anilist/mod.rs
@@ -10,9 +10,9 @@ use graphql_client::{GraphQLQuery, Response};
 use itertools::Itertools;
 use media_models::{
     AnimeAiringScheduleSpecifics, AnimeSpecifics, MangaSpecifics, MediaDetails,
-    MetadataImageForMediaDetails, MetadataPerson, MetadataSearchItem, MetadataVideo,
-    MetadataVideoSource, PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem,
-    PersonSourceSpecifics,
+    MetadataImageForMediaDetails, MetadataPerson, MetadataPersonRelated, MetadataSearchItem,
+    MetadataVideo, MetadataVideoSource, PartialMetadataPerson, PartialMetadataWithoutId,
+    PeopleSearchItem, PersonSourceSpecifics,
 };
 use reqwest::Client;
 use rust_decimal::Decimal;
@@ -281,9 +281,10 @@ impl MediaProvider for NonMediaAnilistService {
                 .into_iter()
                 .map(|r| {
                     let data = r.unwrap().node.unwrap();
-                    (
-                        STUDIO_ROLE.to_owned(),
-                        PartialMetadataWithoutId {
+                    MetadataPersonRelated {
+                        character: None,
+                        role: STUDIO_ROLE.to_owned(),
+                        metadata: PartialMetadataWithoutId {
                             title: data.title.unwrap().native.unwrap(),
                             identifier: data.id.to_string(),
                             source: MediaSource::Anilist,
@@ -295,7 +296,7 @@ impl MediaProvider for NonMediaAnilistService {
                             image: data.cover_image.unwrap().extra_large,
                             is_recommendation: None,
                         },
-                    )
+                    }
                 })
                 .collect();
             MetadataPerson {
@@ -355,14 +356,17 @@ impl MediaProvider for NonMediaAnilistService {
                     None
                 }
             });
-            let mut related = details
+            let mut related = vec![];
+            details
                 .character_media
                 .unwrap()
                 .edges
                 .unwrap()
                 .into_iter()
-                .map(|r| {
-                    let data = r.unwrap().node.unwrap();
+                .for_each(|r| {
+                    let edge = r.unwrap();
+                    let characters = edge.characters.unwrap_or_default();
+                    let data = edge.node.unwrap();
                     let title = data.title.unwrap();
                     let title = get_in_preferred_language(
                         title.native,
@@ -370,23 +374,27 @@ impl MediaProvider for NonMediaAnilistService {
                         title.romaji,
                         &self.base.preferred_language,
                     );
-                    (
-                        "Voicing".to_owned(),
-                        PartialMetadataWithoutId {
-                            title,
-                            identifier: data.id.to_string(),
-                            source: MediaSource::Anilist,
-                            lot: match data.type_.unwrap() {
-                                staff_query::MediaType::ANIME => MediaLot::Anime,
-                                staff_query::MediaType::MANGA => MediaLot::Manga,
-                                staff_query::MediaType::Other(_) => unreachable!(),
-                            },
-                            image: data.cover_image.unwrap().extra_large,
-                            is_recommendation: None,
-                        },
-                    )
-                })
-                .collect_vec();
+                    for character in characters {
+                        if let Some(character) = character.and_then(|c| c.name) {
+                            related.push(MetadataPersonRelated {
+                                character: character.full,
+                                role: "Voicing".to_owned(),
+                                metadata: PartialMetadataWithoutId {
+                                    title: title.clone(),
+                                    is_recommendation: None,
+                                    identifier: data.id.to_string(),
+                                    source: MediaSource::Anilist,
+                                    lot: match data.type_.clone().unwrap() {
+                                        staff_query::MediaType::ANIME => MediaLot::Anime,
+                                        staff_query::MediaType::MANGA => MediaLot::Manga,
+                                        staff_query::MediaType::Other(_) => unreachable!(),
+                                    },
+                                    image: data.cover_image.clone().unwrap().extra_large,
+                                },
+                            })
+                        }
+                    }
+                });
             related.extend(
                 details
                     .staff_media
@@ -395,7 +403,8 @@ impl MediaProvider for NonMediaAnilistService {
                     .unwrap()
                     .into_iter()
                     .map(|r| {
-                        let data = r.unwrap().node.unwrap();
+                        let edge = r.unwrap();
+                        let data = edge.node.unwrap();
                         let title = data.title.unwrap();
                         let title = get_in_preferred_language(
                             title.native,
@@ -403,9 +412,10 @@ impl MediaProvider for NonMediaAnilistService {
                             title.romaji,
                             &self.base.preferred_language,
                         );
-                        (
-                            "Production".to_owned(),
-                            PartialMetadataWithoutId {
+                        MetadataPersonRelated {
+                            character: None,
+                            role: edge.staff_role.unwrap_or_else(|| "Production".to_owned()),
+                            metadata: PartialMetadataWithoutId {
                                 title,
                                 identifier: data.id.to_string(),
                                 source: MediaSource::Anilist,
@@ -417,7 +427,7 @@ impl MediaProvider for NonMediaAnilistService {
                                 image: data.cover_image.unwrap().extra_large,
                                 is_recommendation: None,
                             },
-                        )
+                        }
                     }),
             );
             MetadataPerson {
diff --git a/crates/providers/src/anilist/staff_details.graphql b/crates/providers/src/anilist/staff_details.graphql
index 1b23bc1f6e..1cffd10884 100644
--- a/crates/providers/src/anilist/staff_details.graphql
+++ b/crates/providers/src/anilist/staff_details.graphql
@@ -22,6 +22,11 @@ query StaffQuery($id: Int!) {
     homeTown
     characterMedia {
       edges {
+				characters {
+          name {
+            full
+          }
+        }
         node {
           id
           type
diff --git a/crates/providers/src/audible.rs b/crates/providers/src/audible.rs
index 77cd391c32..56d79a155e 100644
--- a/crates/providers/src/audible.rs
+++ b/crates/providers/src/audible.rs
@@ -252,18 +252,18 @@ impl MediaProvider for AudibleService {
             .await
             .map_err(|e| anyhow!(e))?;
         Ok(MetadataPerson {
-            identifier: data.asin,
-            name: data.name,
-            description: data.description,
-            images: Some(Vec::from_iter(data.image)),
-            source: MediaSource::Audible,
-            gender: None,
-            death_date: None,
-            birth_date: None,
             place: None,
+            gender: None,
             website: None,
+            name: data.name,
             related: vec![],
+            death_date: None,
+            birth_date: None,
+            identifier: data.asin,
             source_specifics: None,
+            description: data.description,
+            source: MediaSource::Audible,
+            images: Some(Vec::from_iter(data.image)),
         })
     }
 
diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs
index 86dd214e51..66054d5bf3 100644
--- a/crates/providers/src/igdb.rs
+++ b/crates/providers/src/igdb.rs
@@ -12,8 +12,9 @@ use enums::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     MediaDetails, MetadataGroupSearchItem, MetadataImageForMediaDetails, MetadataPerson,
-    MetadataSearchItem, MetadataVideo, MetadataVideoSource, PartialMetadataPerson,
-    PartialMetadataWithoutId, PeopleSearchItem, PersonSourceSpecifics, VideoGameSpecifics,
+    MetadataPersonRelated, MetadataSearchItem, MetadataVideo, MetadataVideoSource,
+    PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem, PersonSourceSpecifics,
+    VideoGameSpecifics,
 };
 use reqwest::{
     header::{HeaderName, HeaderValue, AUTHORIZATION},
@@ -369,32 +370,34 @@ where id = {id};
             .into_iter()
             .map(|r| {
                 let image = r.cover.map(|a| self.get_cover_image_url(a.image_id));
-                (
-                    "Publishing".to_owned(),
-                    PartialMetadataWithoutId {
-                        title: r.name.unwrap(),
-                        identifier: r.id.to_string(),
-                        source: MediaSource::Igdb,
-                        lot: MediaLot::VideoGame,
+                MetadataPersonRelated {
+                    character: None,
+                    role: "Publishing".to_owned(),
+                    metadata: PartialMetadataWithoutId {
                         image,
+                        title: r.name.unwrap(),
                         is_recommendation: None,
+                        lot: MediaLot::VideoGame,
+                        source: MediaSource::Igdb,
+                        identifier: r.id.to_string(),
                     },
-                )
+                }
             })
             .collect_vec();
         related.extend(detail.developed.unwrap_or_default().into_iter().map(|r| {
             let image = r.cover.map(|a| self.get_cover_image_url(a.image_id));
-            (
-                "Development".to_owned(),
-                PartialMetadataWithoutId {
-                    title: r.name.unwrap(),
-                    identifier: r.id.to_string(),
-                    source: MediaSource::Igdb,
-                    lot: MediaLot::VideoGame,
+            MetadataPersonRelated {
+                character: None,
+                role: "Development".to_owned(),
+                metadata: PartialMetadataWithoutId {
                     image,
+                    title: r.name.unwrap(),
                     is_recommendation: None,
+                    lot: MediaLot::VideoGame,
+                    source: MediaSource::Igdb,
+                    identifier: r.id.to_string(),
                 },
-            )
+            }
         }));
         Ok(MetadataPerson {
             identifier: detail.id.to_string(),
diff --git a/crates/providers/src/manga_updates.rs b/crates/providers/src/manga_updates.rs
index e89bf34f17..27f0b5db1c 100644
--- a/crates/providers/src/manga_updates.rs
+++ b/crates/providers/src/manga_updates.rs
@@ -7,8 +7,9 @@ use dependent_models::SearchResults;
 use enums::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
-    MangaSpecifics, MediaDetails, MetadataImageForMediaDetails, MetadataPerson, MetadataSearchItem,
-    PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem, PersonSourceSpecifics,
+    MangaSpecifics, MediaDetails, MetadataImageForMediaDetails, MetadataPerson,
+    MetadataPersonRelated, MetadataSearchItem, PartialMetadataPerson, PartialMetadataWithoutId,
+    PeopleSearchItem, PersonSourceSpecifics,
 };
 use reqwest::Client;
 use rust_decimal::Decimal;
@@ -237,18 +238,17 @@ impl MediaProvider for MangaUpdatesService {
         let related = related_data
             .series_list
             .into_iter()
-            .map(|r| {
-                (
-                    "Author".to_owned(),
-                    PartialMetadataWithoutId {
-                        title: r.title,
-                        identifier: r.series_id.to_string(),
-                        source: MediaSource::MangaUpdates,
-                        lot: MediaLot::Manga,
-                        image: None,
-                        is_recommendation: None,
-                    },
-                )
+            .map(|r| MetadataPersonRelated {
+                character: None,
+                role: "Author".to_owned(),
+                metadata: PartialMetadataWithoutId {
+                    image: None,
+                    title: r.title,
+                    lot: MediaLot::Manga,
+                    is_recommendation: None,
+                    source: MediaSource::MangaUpdates,
+                    identifier: r.series_id.to_string(),
+                },
             })
             .collect_vec();
         let resp = MetadataPerson {
diff --git a/crates/providers/src/openlibrary.rs b/crates/providers/src/openlibrary.rs
index a0049d92e3..f600e626e0 100644
--- a/crates/providers/src/openlibrary.rs
+++ b/crates/providers/src/openlibrary.rs
@@ -9,8 +9,9 @@ use dependent_models::SearchResults;
 use enums::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
-    BookSpecifics, MediaDetails, MetadataImageForMediaDetails, MetadataPerson, MetadataSearchItem,
-    PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem, PersonSourceSpecifics,
+    BookSpecifics, MediaDetails, MetadataImageForMediaDetails, MetadataPerson,
+    MetadataPersonRelated, MetadataSearchItem, PartialMetadataPerson, PartialMetadataWithoutId,
+    PeopleSearchItem, PersonSourceSpecifics,
 };
 use reqwest::Client;
 use scraper::{Html, Selector};
@@ -261,17 +262,18 @@ impl MediaProvider for OpenlibraryService {
                     .collect_vec()
                     .first()
                     .cloned();
-                related.push((
-                    "Author".to_owned(),
-                    PartialMetadataWithoutId {
-                        identifier: get_key(&entry.key),
+                related.push(MetadataPersonRelated {
+                    character: None,
+                    role: "Author".to_owned(),
+                    metadata: PartialMetadataWithoutId {
                         title,
+                        image,
                         lot: MediaLot::Book,
-                        source: MediaSource::Openlibrary,
                         is_recommendation: None,
-                        image,
+                        identifier: get_key(&entry.key),
+                        source: MediaSource::Openlibrary,
                     },
-                ))
+                })
             }
         }
         ryot_log!(debug, "Found {} related works.", related.len());
diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs
index 3aae9098e8..0d8bb3ab9a 100644
--- a/crates/providers/src/tmdb.rs
+++ b/crates/providers/src/tmdb.rs
@@ -20,9 +20,10 @@ use hashbag::HashBag;
 use itertools::Itertools;
 use media_models::{
     ExternalIdentifiers, MediaDetails, MetadataGroupSearchItem, MetadataImage,
-    MetadataImageForMediaDetails, MetadataPerson, MetadataSearchItem, MetadataVideo,
-    MetadataVideoSource, MovieSpecifics, PartialMetadataPerson, PartialMetadataWithoutId,
-    PeopleSearchItem, PersonSourceSpecifics, ShowEpisode, ShowSeason, ShowSpecifics, WatchProvider,
+    MetadataImageForMediaDetails, MetadataPerson, MetadataPersonRelated, MetadataSearchItem,
+    MetadataVideo, MetadataVideoSource, MovieSpecifics, PartialMetadataPerson,
+    PartialMetadataWithoutId, PeopleSearchItem, PersonSourceSpecifics, ShowEpisode, ShowSeason,
+    ShowSpecifics, WatchProvider,
 };
 use reqwest::{
     header::{HeaderValue, AUTHORIZATION},
@@ -37,7 +38,6 @@ use traits::{MediaProvider, MediaProviderLanguages};
 
 static URL: &str = "https://api.themoviedb.org/3";
 static FILE: &str = "tmdb.json";
-static POSSIBLE_ROLES: [&str; 5] = ["Acting", "Directing", "Director", "Production", "Writer"];
 
 #[derive(Debug, Serialize, Deserialize, Clone)]
 struct Settings {
@@ -54,14 +54,14 @@ struct TmdbLanguage {
 #[derive(Debug, Serialize, Deserialize, Clone)]
 struct TmdbCredit {
     id: Option<i32>,
+    job: Option<String>,
     name: Option<String>,
     title: Option<String>,
-    media_type: Option<String>,
-    known_for_department: Option<String>,
-    job: Option<String>,
     character: Option<String>,
-    profile_path: Option<String>,
+    media_type: Option<String>,
     poster_path: Option<String>,
+    profile_path: Option<String>,
+    known_for_department: Option<String>,
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -165,6 +165,20 @@ struct TmdbFindByExternalSourceResponse {
     tv_results: Vec<TmdbEntry>,
 }
 
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct TmdbNonMediaEntity {
+    id: i32,
+    name: String,
+    biography: Option<String>,
+    description: Option<String>,
+    birthday: Option<NaiveDate>,
+    deathday: Option<NaiveDate>,
+    homepage: Option<String>,
+    gender: Option<u8>,
+    origin_country: Option<String>,
+    place_of_birth: Option<String>,
+}
+
 #[derive(Debug, Clone)]
 pub struct TmdbService {
     client: Client,
@@ -503,23 +517,24 @@ impl MediaProvider for NonMediaTmdbService {
                 .await
                 .map_err(|e| anyhow!(e))?;
             for media in cred_det.crew.into_iter().chain(cred_det.cast.into_iter()) {
-                if let Some(job) = media.job {
-                    related.push((
-                        job,
-                        PartialMetadataWithoutId {
-                            identifier: media.id.unwrap().to_string(),
-                            title: media.title.or(media.name).unwrap_or_default(),
-                            image: media.poster_path.map(|p| self.base.get_image_url(p)),
-                            lot: match media.media_type.unwrap().as_ref() {
-                                "movie" => MediaLot::Movie,
-                                "tv" => MediaLot::Show,
-                                _ => continue,
-                            },
-                            source: MediaSource::Tmdb,
-                            is_recommendation: None,
-                        },
-                    ));
-                }
+                let role = media.job.unwrap_or_else(|| "Actor".to_owned());
+                let metadata = PartialMetadataWithoutId {
+                    is_recommendation: None,
+                    source: MediaSource::Tmdb,
+                    identifier: media.id.unwrap().to_string(),
+                    title: media.title.or(media.name).unwrap_or_default(),
+                    image: media.poster_path.map(|p| self.base.get_image_url(p)),
+                    lot: match media.media_type.unwrap().as_ref() {
+                        "movie" => MediaLot::Movie,
+                        "tv" => MediaLot::Show,
+                        _ => continue,
+                    },
+                };
+                related.push(MetadataPersonRelated {
+                    role,
+                    metadata,
+                    character: media.character,
+                });
             }
         } else {
             for m_typ in ["movie", "tv"] {
@@ -536,22 +551,21 @@ impl MediaProvider for NonMediaTmdbService {
                         .json()
                         .await
                         .map_err(|e| anyhow!(e))?;
-                    related.extend(cred_det.results.into_iter().map(|m| {
-                        (
-                            "Production Company".to_owned(),
-                            PartialMetadataWithoutId {
-                                identifier: m.id.to_string(),
-                                title: m.title.unwrap_or_default(),
-                                image: m.poster_path.map(|p| self.base.get_image_url(p)),
-                                lot: match m_typ {
-                                    "movie" => MediaLot::Movie,
-                                    "tv" => MediaLot::Show,
-                                    _ => unreachable!(),
-                                },
-                                source: MediaSource::Tmdb,
-                                is_recommendation: None,
+                    related.extend(cred_det.results.into_iter().map(|m| MetadataPersonRelated {
+                        character: None,
+                        role: "Production Company".to_owned(),
+                        metadata: PartialMetadataWithoutId {
+                            is_recommendation: None,
+                            source: MediaSource::Tmdb,
+                            identifier: m.id.to_string(),
+                            title: m.title.unwrap_or_default(),
+                            image: m.poster_path.map(|p| self.base.get_image_url(p)),
+                            lot: match m_typ {
+                                "movie" => MediaLot::Movie,
+                                "tv" => MediaLot::Show,
+                                _ => unreachable!(),
                             },
-                        )
+                        },
                     }));
                     if cred_det.page == cred_det.total_pages {
                         break;
@@ -719,16 +733,14 @@ impl MediaProvider for TmdbMovieService {
                 .into_iter()
                 .flat_map(|g| {
                     g.id.and_then(|id| {
-                        g.known_for_department
-                            .filter(|r| POSSIBLE_ROLES.contains(&r.as_str()))
-                            .map(|r| PartialMetadataPerson {
-                                identifier: id.to_string(),
-                                name: g.name.unwrap_or_default(),
-                                role: r,
-                                source: MediaSource::Tmdb,
-                                character: g.character,
-                                source_specifics: None,
-                            })
+                        g.known_for_department.map(|r| PartialMetadataPerson {
+                            identifier: id.to_string(),
+                            name: g.name.unwrap_or_default(),
+                            role: r,
+                            source: MediaSource::Tmdb,
+                            character: g.character,
+                            source_specifics: None,
+                        })
                     })
                 })
                 .unique()
@@ -741,16 +753,14 @@ impl MediaProvider for TmdbMovieService {
                 .into_iter()
                 .flat_map(|g| {
                     g.id.and_then(|id| {
-                        g.known_for_department
-                            .filter(|r| POSSIBLE_ROLES.contains(&r.as_str()))
-                            .map(|r| PartialMetadataPerson {
-                                identifier: id.to_string(),
-                                name: g.name.unwrap_or_default(),
-                                role: r,
-                                source: MediaSource::Tmdb,
-                                character: g.character,
-                                source_specifics: None,
-                            })
+                        g.known_for_department.map(|r| PartialMetadataPerson {
+                            identifier: id.to_string(),
+                            name: g.name.unwrap_or_default(),
+                            role: r,
+                            source: MediaSource::Tmdb,
+                            character: g.character,
+                            source_specifics: None,
+                        })
                     })
                 })
                 .unique()
@@ -1136,7 +1146,6 @@ impl MediaProvider for TmdbShowService {
             .sorted_by_key(|c| c.1)
             .rev()
             .map(|c| c.0)
-            .filter(|c| POSSIBLE_ROLES.contains(&c.role.as_str()))
             .cloned()
             .collect_vec();
         let seasons_without_specials = seasons
@@ -1355,20 +1364,6 @@ async fn get_client_config(access_token: &str) -> (Client, Settings) {
     (client, tmdb_settings)
 }
 
-#[derive(Debug, Serialize, Deserialize, Clone)]
-struct TmdbNonMediaEntity {
-    id: i32,
-    name: String,
-    biography: Option<String>,
-    description: Option<String>,
-    birthday: Option<NaiveDate>,
-    deathday: Option<NaiveDate>,
-    homepage: Option<String>,
-    gender: Option<u8>,
-    origin_country: Option<String>,
-    place_of_birth: Option<String>,
-}
-
 fn replace_from_end(input_string: String, search_string: &str, replace_string: &str) -> String {
     if let Some(last_index) = input_string.rfind(search_string) {
         let mut modified_string = input_string.clone();
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 26ed613b61..3658b6b67f 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -2254,7 +2254,7 @@ ORDER BY RANDOM() LIMIT 10;
         for assoc in associations {
             let to_push = PersonDetailsItemWithCharacter {
                 character: assoc.character,
-                media_id: assoc.metadata_id,
+                metadata_id: assoc.metadata_id,
             };
             contents
                 .entry(assoc.role)
@@ -2266,7 +2266,11 @@ ORDER BY RANDOM() LIMIT 10;
         let contents = contents
             .into_iter()
             .sorted_by_key(|(role, _)| role.clone())
-            .map(|(name, items)| PersonDetailsGroupedByRole { name, items })
+            .map(|(name, items)| PersonDetailsGroupedByRole {
+                count: items.len(),
+                name,
+                items,
+            })
             .collect_vec();
         let slug = slug::slugify(&details.name);
         let identifier = &details.identifier;
@@ -2697,20 +2701,21 @@ ORDER BY RANDOM() LIMIT 10;
         to_update_person.images = ActiveValue::Set(images);
         to_update_person.is_partial = ActiveValue::Set(Some(false));
         to_update_person.name = ActiveValue::Set(provider_person.name);
-        for (role, media) in provider_person.related.clone() {
-            let title = media.title.clone();
-            let pm = create_partial_metadata(media, &self.0.db).await?;
+        for data in provider_person.related.clone() {
+            let title = data.metadata.title.clone();
+            let pm = create_partial_metadata(data.metadata, &self.0.db).await?;
             let already_intermediate = MetadataToPerson::find()
                 .filter(metadata_to_person::Column::MetadataId.eq(&pm.id))
                 .filter(metadata_to_person::Column::PersonId.eq(&person_id))
-                .filter(metadata_to_person::Column::Role.eq(&role))
+                .filter(metadata_to_person::Column::Role.eq(&data.role))
                 .one(&self.0.db)
                 .await?;
             if already_intermediate.is_none() {
                 let intermediate = metadata_to_person::ActiveModel {
-                    person_id: ActiveValue::Set(person.id.clone()),
+                    role: ActiveValue::Set(data.role.clone()),
                     metadata_id: ActiveValue::Set(pm.id.clone()),
-                    role: ActiveValue::Set(role.clone()),
+                    person_id: ActiveValue::Set(person.id.clone()),
+                    character: ActiveValue::Set(data.character.clone()),
                     ..Default::default()
                 };
                 intermediate.insert(&self.0.db).await.unwrap();
@@ -2722,13 +2727,13 @@ ORDER BY RANDOM() LIMIT 10;
                     source: pm.source,
                     ..Default::default()
                 },
-                role: role.clone(),
+                role: data.role.clone(),
             };
             if !default_state_changes.media_associated.contains(&search_for) {
                 notifications.push((
                     format!(
                         "{} has been associated with {} as {}",
-                        person.name, title, role
+                        person.name, title, data.role
                     ),
                     MediaStateChanged::PersonMediaAssociated,
                 ));
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index 52410a41f3..c8cef59dd1 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -29,7 +29,7 @@ const documents = {
     "query MetadataList($input: MetadataListInput!) {\n  metadataList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}": types.MetadataListDocument,
     "query MetadataSearch($input: MetadataSearchInput!) {\n  metadataSearch(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      databaseId\n      hasInteracted\n      item {\n        identifier\n        title\n        image\n        publishYear\n      }\n    }\n  }\n}": types.MetadataSearchDocument,
     "query PeopleSearch($input: PeopleSearchInput!) {\n  peopleSearch(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      identifier\n      name\n      image\n      birthYear\n    }\n  }\n}": types.PeopleSearchDocument,
-    "query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      items {\n        mediaId\n        character\n      }\n    }\n  }\n}": types.PersonDetailsDocument,
+    "query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      count\n      items {\n        character\n        metadataId\n      }\n    }\n  }\n}": types.PersonDetailsDocument,
     "query UserDetails {\n  userDetails {\n    __typename\n    ... on User {\n      id\n      lot\n      name\n      oidcIssuerId\n    }\n  }\n}": types.UserDetailsDocument,
     "query UserExerciseDetails($exerciseId: String!) {\n  userExerciseDetails(exerciseId: $exerciseId) {\n    collections {\n      ...CollectionPart\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    history {\n      idx\n      workoutId\n      workoutEndOn\n      bestSet {\n        ...WorkoutSetRecordPart\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            exerciseIdx\n            setIdx\n          }\n        }\n      }\n    }\n  }\n}": types.UserExerciseDetailsDocument,
     "query UserMeasurementsList($input: UserMeasurementsListInput!) {\n  userMeasurementsList(input: $input) {\n    timestamp\n    name\n    comment\n    stats {\n      weight\n      bodyMassIndex\n      totalBodyWater\n      muscle\n      leanBodyMass\n      bodyFat\n      boneMass\n      visceralFat\n      waistCircumference\n      waistToHeightRatio\n      hipCircumference\n      waistToHipRatio\n      chestCircumference\n      thighCircumference\n      bicepsCircumference\n      neckCircumference\n      bodyFatCaliper\n      chestSkinfold\n      abdominalSkinfold\n      thighSkinfold\n      basalMetabolicRate\n      totalDailyEnergyExpenditure\n      calories\n      custom\n    }\n  }\n}": types.UserMeasurementsListDocument,
@@ -126,7 +126,7 @@ export function graphql(source: "query PeopleSearch($input: PeopleSearchInput!)
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      items {\n        mediaId\n        character\n      }\n    }\n  }\n}"): (typeof documents)["query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      items {\n        mediaId\n        character\n      }\n    }\n  }\n}"];
+export function graphql(source: "query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      count\n      items {\n        character\n        metadataId\n      }\n    }\n  }\n}"): (typeof documents)["query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      count\n      items {\n        character\n        metadataId\n      }\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index ab485c518e..e4976d3742 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -1517,6 +1517,8 @@ export type PersonDetails = {
 };
 
 export type PersonDetailsGroupedByRole = {
+  /** The number of items in this role. */
+  count: Scalars['Int']['output'];
   /** The media items in which this role was performed. */
   items: Array<PersonDetailsItemWithCharacter>;
   /** The name of the role performed. */
@@ -1525,7 +1527,7 @@ export type PersonDetailsGroupedByRole = {
 
 export type PersonDetailsItemWithCharacter = {
   character?: Maybe<Scalars['String']['output']>;
-  mediaId: Scalars['String']['output'];
+  metadataId: Scalars['String']['output'];
 };
 
 export type PersonSortInput = {
@@ -3085,7 +3087,7 @@ export type PersonDetailsQueryVariables = Exact<{
 }>;
 
 
-export type PersonDetailsQuery = { personDetails: { sourceUrl?: string | null, details: { id: string, name: string, source: MediaSource, identifier: string, isPartial?: boolean | null, description?: string | null, birthDate?: string | null, deathDate?: string | null, place?: string | null, website?: string | null, gender?: string | null, displayImages: Array<string> }, contents: Array<{ name: string, items: Array<{ mediaId: string, character?: string | null }> }> } };
+export type PersonDetailsQuery = { personDetails: { sourceUrl?: string | null, details: { id: string, name: string, source: MediaSource, identifier: string, isPartial?: boolean | null, description?: string | null, birthDate?: string | null, deathDate?: string | null, place?: string | null, website?: string | null, gender?: string | null, displayImages: Array<string> }, contents: Array<{ name: string, count: number, items: Array<{ character?: string | null, metadataId: string }> }> } };
 
 export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>;
 
@@ -3381,7 +3383,7 @@ export const MetadataGroupSearchDocument = {"kind":"Document","definitions":[{"k
 export const MetadataListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode<MetadataListQuery, MetadataListQueryVariables>;
 export const MetadataSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataSearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"databaseId"}},{"kind":"Field","name":{"kind":"Name","value":"hasInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"item"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}}]}}]}}]}}]}}]} as unknown as DocumentNode<MetadataSearchQuery, MetadataSearchQueryVariables>;
 export const PeopleSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleSearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"birthYear"}}]}}]}}]}}]} as unknown as DocumentNode<PeopleSearchQuery, PeopleSearchQueryVariables>;
-export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"birthDate"}},{"kind":"Field","name":{"kind":"Name","value":"deathDate"}},{"kind":"Field","name":{"kind":"Name","value":"place"}},{"kind":"Field","name":{"kind":"Name","value":"website"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mediaId"}},{"kind":"Field","name":{"kind":"Name","value":"character"}}]}}]}}]}}]}}]} as unknown as DocumentNode<PersonDetailsQuery, PersonDetailsQueryVariables>;
+export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"birthDate"}},{"kind":"Field","name":{"kind":"Name","value":"deathDate"}},{"kind":"Field","name":{"kind":"Name","value":"place"}},{"kind":"Field","name":{"kind":"Name","value":"website"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"character"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}}]}}]}}]}}]}}]} as unknown as DocumentNode<PersonDetailsQuery, PersonDetailsQueryVariables>;
 export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}}]}}]}}]}}]} as unknown as DocumentNode<UserDetailsQuery, UserDetailsQueryVariables>;
 export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"idx"}},{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEndOn"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}},{"kind":"Field","name":{"kind":"Name","value":"setIdx"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}}]} as unknown as DocumentNode<UserExerciseDetailsQuery, UserExerciseDetailsQueryVariables>;
 export const UserMeasurementsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMeasurementsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserMeasurementsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMeasurementsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}}]}}]}}]} as unknown as DocumentNode<UserMeasurementsListQuery, UserMeasurementsListQueryVariables>;
diff --git a/libs/graphql/src/backend/queries/PersonDetails.gql b/libs/graphql/src/backend/queries/PersonDetails.gql
index a4e354fec5..c663285f6e 100644
--- a/libs/graphql/src/backend/queries/PersonDetails.gql
+++ b/libs/graphql/src/backend/queries/PersonDetails.gql
@@ -17,9 +17,10 @@ query PersonDetails($personId: String!) {
     }
     contents {
       name
+      count
       items {
-        mediaId
         character
+        metadataId
       }
     }
   }


Test patch: