Repository: IgnisDa/ryot (language: rust)

Issue:
[Feature Request] - Add external_identifiers to GraphQL metadataDetails query.
Hi!
First of all, thanks for the hard work, I have been trying Ryot for a couple weeks and I am absolutely loving it.

I have been integrating it with a bunch of tooling and automated systems I have in my own self-hosted homelab.
But one of the issues I have been having, is the lack of `external_identifiers` in the GraphQL queries. I have to match data using the title of an Anime/Show/Movie, and thats just not very accurate.

I looked pretty extensively through the schema and could not find a way to pull the field.

I was wondering if you were willing to add this field to the GraphQL query `metadataDetails` or provide another query which I could use to pull that data from.

I could also look into opening a PR, but I am not very well versed with Rust. 😅 


Discussion / hints:

Fix patch:
diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs
index 4555aa7067..20c7fdac3d 100644
--- a/crates/models/media/src/lib.rs
+++ b/crates/models/media/src/lib.rs
@@ -1220,33 +1220,34 @@ pub struct GraphqlMediaAssets {
 pub struct GraphqlMetadataDetails {
     pub id: String,
     pub title: String,
+    pub lot: MediaLot,
     pub identifier: String,
+    pub source: MediaSource,
     pub is_nsfw: Option<bool>,
     pub is_partial: Option<bool>,
+    pub suggestions: Vec<String>,
+    pub publish_year: Option<i32>,
+    pub source_url: Option<String>,
+    pub genres: Vec<GenreListItem>,
+    pub assets: GraphqlMediaAssets,
     pub description: Option<String>,
-    pub original_language: Option<String>,
+    pub publish_date: Option<NaiveDate>,
     pub provider_rating: Option<Decimal>,
+    pub original_language: Option<String>,
     pub production_status: Option<String>,
-    pub lot: MediaLot,
-    pub source: MediaSource,
-    pub creators: Vec<MetadataCreatorGroupedByRole>,
+    pub group: Option<GraphqlMetadataGroup>,
     pub watch_providers: Vec<WatchProvider>,
-    pub genres: Vec<GenreListItem>,
-    pub assets: GraphqlMediaAssets,
-    pub publish_year: Option<i32>,
-    pub publish_date: Option<NaiveDate>,
+    pub show_specifics: Option<ShowSpecifics>,
     pub book_specifics: Option<BookSpecifics>,
     pub movie_specifics: Option<MovieSpecifics>,
-    pub show_specifics: Option<ShowSpecifics>,
-    pub video_game_specifics: Option<VideoGameSpecifics>,
-    pub visual_novel_specifics: Option<VisualNovelSpecifics>,
-    pub audio_book_specifics: Option<AudioBookSpecifics>,
     pub podcast_specifics: Option<PodcastSpecifics>,
     pub manga_specifics: Option<MangaSpecifics>,
     pub anime_specifics: Option<AnimeSpecifics>,
-    pub source_url: Option<String>,
-    pub suggestions: Vec<String>,
-    pub group: Option<GraphqlMetadataGroup>,
+    pub creators: Vec<MetadataCreatorGroupedByRole>,
+    pub audio_book_specifics: Option<AudioBookSpecifics>,
+    pub video_game_specifics: Option<VideoGameSpecifics>,
+    pub external_identifiers: Option<ExternalIdentifiers>,
+    pub visual_novel_specifics: Option<VisualNovelSpecifics>,
 }
 
 #[derive(Debug, Serialize, Deserialize, Enum, Clone, PartialEq, Eq, Copy, Default)]
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 1c736e2cef..2dab99d625 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -507,8 +507,15 @@ ORDER BY RANDOM() LIMIT 10;
         let watch_providers = model.watch_providers.unwrap_or_default();
 
         let resp = GraphqlMetadataDetails {
+            group,
+            assets,
+            genres,
+            creators,
+            source_url,
+            suggestions,
             id: model.id,
             lot: model.lot,
+            watch_providers,
             title: model.title,
             source: model.source,
             is_nsfw: model.is_nsfw,
@@ -517,25 +524,19 @@ ORDER BY RANDOM() LIMIT 10;
             description: model.description,
             publish_date: model.publish_date,
             publish_year: model.publish_year,
-            provider_rating: model.provider_rating,
-            production_status: model.production_status,
-            original_language: model.original_language,
             book_specifics: model.book_specifics,
             show_specifics: model.show_specifics,
             movie_specifics: model.movie_specifics,
             manga_specifics: model.manga_specifics,
             anime_specifics: model.anime_specifics,
+            provider_rating: model.provider_rating,
+            production_status: model.production_status,
+            original_language: model.original_language,
             podcast_specifics: model.podcast_specifics,
+            external_identifiers: model.external_identifiers,
             video_game_specifics: model.video_game_specifics,
             audio_book_specifics: model.audio_book_specifics,
             visual_novel_specifics: model.visual_novel_specifics,
-            group,
-            assets,
-            genres,
-            creators,
-            source_url,
-            suggestions,
-            watch_providers,
         };
         Ok(resp)
     }
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index 67bf4c17d5..c789ce8269 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -11,6 +11,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
  * 3. It does not support dead code elimination, so it will add unused operations.
  *
  * Therefore it is highly recommended to use the babel or swc plugin for production.
+ * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
  */
 const documents = {
     "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateComplexJsonInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}": types.RegisterUserDocument,
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index 7853c79a57..4247093d64 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -641,6 +641,10 @@ export type ExportJob = {
   url: Scalars['String']['output'];
 };
 
+export type ExternalIdentifiers = {
+  tvdbId?: Maybe<Scalars['Int']['output']>;
+};
+
 export type FrontendConfig = {
   /** A message to be displayed on the dashboard. */
   dashboardMessage: Scalars['String']['output'];
@@ -711,6 +715,7 @@ export type GraphqlMetadataDetails = {
   bookSpecifics?: Maybe<BookSpecifics>;
   creators: Array<MetadataCreatorGroupedByRole>;
   description?: Maybe<Scalars['String']['output']>;
+  externalIdentifiers?: Maybe<ExternalIdentifiers>;
   genres: Array<GenreListItem>;
   group?: Maybe<GraphqlMetadataGroup>;
   id: Scalars['String']['output'];


Test patch: