🛠 graphql changes

This commit is contained in:
2026-02-24 16:29:48 -05:00
parent cd446a9ca3
commit f7804ee3f0
11 changed files with 3317 additions and 40 deletions

813
models/graphql/resolvers.ts Normal file
View File

@@ -0,0 +1,813 @@
import Comic, { MetadataSource } from "../comic.model";
import UserPreferences, {
ConflictResolutionStrategy,
} from "../userpreferences.model";
import {
resolveMetadataField,
buildCanonicalMetadata,
MetadataField,
ResolutionPreferences,
} from "../../utils/metadata.resolution.utils";
/**
* GraphQL Resolvers for canonical metadata queries and mutations
*/
export const resolvers = {
Query: {
/**
* Get a single comic by ID
*/
comic: async (_: any, { id }: { id: string }) => {
try {
const comic = await Comic.findById(id);
return comic;
} catch (error) {
console.error("Error fetching comic:", error);
throw new Error("Failed to fetch comic");
}
},
/**
* List comics with pagination and filtering
*/
comics: async (
_: any,
{
limit = 10,
page = 1,
search,
publisher,
series,
}: {
limit?: number;
page?: number;
search?: string;
publisher?: string;
series?: string;
}
) => {
try {
const query: any = {};
// Build search query
if (search) {
query.$or = [
{ "canonicalMetadata.title.value": new RegExp(search, "i") },
{ "canonicalMetadata.series.value": new RegExp(search, "i") },
{ "rawFileDetails.name": new RegExp(search, "i") },
];
}
if (publisher) {
query["canonicalMetadata.publisher.value"] = new RegExp(
publisher,
"i"
);
}
if (series) {
query["canonicalMetadata.series.value"] = new RegExp(series, "i");
}
const options = {
page,
limit,
sort: { createdAt: -1 },
};
const result = await Comic.paginate(query, options);
return {
comics: result.docs,
totalCount: result.totalDocs,
pageInfo: {
hasNextPage: result.hasNextPage,
hasPreviousPage: result.hasPrevPage,
currentPage: result.page,
totalPages: result.totalPages,
},
};
} catch (error) {
console.error("Error fetching comics:", error);
throw new Error("Failed to fetch comics");
}
},
/**
* Get user preferences
*/
userPreferences: async (
_: any,
{ userId = "default" }: { userId?: string }
) => {
try {
let preferences = await UserPreferences.findOne({ userId });
// Create default preferences if none exist
if (!preferences) {
preferences = await UserPreferences.create({ userId });
}
return preferences;
} catch (error) {
console.error("Error fetching user preferences:", error);
throw new Error("Failed to fetch user preferences");
}
},
/**
* Analyze metadata conflicts for a comic
*/
analyzeMetadataConflicts: async (
_: any,
{ comicId }: { comicId: string }
) => {
try {
const comic = await Comic.findById(comicId);
if (!comic) {
throw new Error("Comic not found");
}
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (!preferences) {
throw new Error("User preferences not found");
}
const conflicts: any[] = [];
// Analyze each field for conflicts
const fields = [
"title",
"series",
"issueNumber",
"description",
"publisher",
];
for (const field of fields) {
const candidates = extractCandidatesForField(
field,
comic.sourcedMetadata
);
if (candidates.length > 1) {
const resolved = resolveMetadataField(
field,
candidates,
convertPreferences(preferences)
);
conflicts.push({
field,
candidates,
resolved,
resolutionReason: getResolutionReason(
resolved,
candidates,
preferences
),
});
}
}
return conflicts;
} catch (error) {
console.error("Error analyzing metadata conflicts:", error);
throw new Error("Failed to analyze metadata conflicts");
}
},
/**
* Preview canonical metadata resolution without saving
*/
previewCanonicalMetadata: async (
_: any,
{
comicId,
preferences: preferencesInput,
}: { comicId: string; preferences?: any }
) => {
try {
const comic = await Comic.findById(comicId);
if (!comic) {
throw new Error("Comic not found");
}
let preferences = await UserPreferences.findOne({
userId: "default",
});
// Use provided preferences or default
if (preferencesInput) {
preferences = applyPreferencesInput(preferences, preferencesInput);
}
if (!preferences) {
throw new Error("User preferences not found");
}
const canonical = buildCanonicalMetadata(
comic.sourcedMetadata,
convertPreferences(preferences)
);
return canonical;
} catch (error) {
console.error("Error previewing canonical metadata:", error);
throw new Error("Failed to preview canonical metadata");
}
},
},
Mutation: {
/**
* Update user preferences
*/
updateUserPreferences: async (
_: any,
{
userId = "default",
preferences: preferencesInput,
}: { userId?: string; preferences: any }
) => {
try {
let preferences = await UserPreferences.findOne({ userId });
if (!preferences) {
preferences = new UserPreferences({ userId });
}
// Update preferences
if (preferencesInput.sourcePriorities) {
preferences.sourcePriorities = preferencesInput.sourcePriorities.map(
(sp: any) => ({
source: sp.source,
priority: sp.priority,
enabled: sp.enabled,
fieldOverrides: sp.fieldOverrides
? new Map(
sp.fieldOverrides.map((fo: any) => [fo.field, fo.priority])
)
: new Map(),
})
);
}
if (preferencesInput.conflictResolution) {
preferences.conflictResolution = preferencesInput.conflictResolution;
}
if (preferencesInput.minConfidenceThreshold !== undefined) {
preferences.minConfidenceThreshold =
preferencesInput.minConfidenceThreshold;
}
if (preferencesInput.preferRecent !== undefined) {
preferences.preferRecent = preferencesInput.preferRecent;
}
if (preferencesInput.fieldPreferences) {
preferences.fieldPreferences = new Map(
preferencesInput.fieldPreferences.map((fp: any) => [
fp.field,
fp.preferredSource,
])
);
}
if (preferencesInput.autoMerge) {
preferences.autoMerge = {
...preferences.autoMerge,
...preferencesInput.autoMerge,
};
}
await preferences.save();
return preferences;
} catch (error) {
console.error("Error updating user preferences:", error);
throw new Error("Failed to update user preferences");
}
},
/**
* Manually set a metadata field (creates user override)
*/
setMetadataField: async (
_: any,
{ comicId, field, value }: { comicId: string; field: string; value: any }
) => {
try {
const comic = await Comic.findById(comicId);
if (!comic) {
throw new Error("Comic not found");
}
// Set the field with user override
const fieldPath = `canonicalMetadata.${field}`;
const update = {
[fieldPath]: {
value,
provenance: {
source: MetadataSource.MANUAL,
confidence: 1.0,
fetchedAt: new Date(),
},
userOverride: true,
},
};
const updatedComic = await Comic.findByIdAndUpdate(
comicId,
{ $set: update },
{ new: true }
);
return updatedComic;
} catch (error) {
console.error("Error setting metadata field:", error);
throw new Error("Failed to set metadata field");
}
},
/**
* Trigger metadata resolution for a comic
*/
resolveMetadata: async (_: any, { comicId }: { comicId: string }) => {
try {
const comic = await Comic.findById(comicId);
if (!comic) {
throw new Error("Comic not found");
}
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (!preferences) {
throw new Error("User preferences not found");
}
// Build canonical metadata
const canonical = buildCanonicalMetadata(
comic.sourcedMetadata,
convertPreferences(preferences)
);
// Update comic with canonical metadata
comic.canonicalMetadata = canonical;
await comic.save();
return comic;
} catch (error) {
console.error("Error resolving metadata:", error);
throw new Error("Failed to resolve metadata");
}
},
/**
* Bulk resolve metadata for multiple comics
*/
bulkResolveMetadata: async (
_: any,
{ comicIds }: { comicIds: string[] }
) => {
try {
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (!preferences) {
throw new Error("User preferences not found");
}
const resolvedComics = [];
for (const comicId of comicIds) {
const comic = await Comic.findById(comicId);
if (comic) {
const canonical = buildCanonicalMetadata(
comic.sourcedMetadata,
convertPreferences(preferences)
);
comic.canonicalMetadata = canonical;
await comic.save();
resolvedComics.push(comic);
}
}
return resolvedComics;
} catch (error) {
console.error("Error bulk resolving metadata:", error);
throw new Error("Failed to bulk resolve metadata");
}
},
/**
* Remove user override for a field
*/
removeMetadataOverride: async (
_: any,
{ comicId, field }: { comicId: string; field: string }
) => {
try {
const comic = await Comic.findById(comicId);
if (!comic) {
throw new Error("Comic not found");
}
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (!preferences) {
throw new Error("User preferences not found");
}
// Re-resolve the field without user override
const candidates = extractCandidatesForField(
field,
comic.sourcedMetadata
).filter((c) => !c.userOverride);
const resolved = resolveMetadataField(
field,
candidates,
convertPreferences(preferences)
);
if (resolved) {
const fieldPath = `canonicalMetadata.${field}`;
await Comic.findByIdAndUpdate(comicId, {
$set: { [fieldPath]: resolved },
});
}
const updatedComic = await Comic.findById(comicId);
return updatedComic;
} catch (error) {
console.error("Error removing metadata override:", error);
throw new Error("Failed to remove metadata override");
}
},
/**
* Refresh metadata from a specific source
*/
refreshMetadataFromSource: async (
_: any,
{ comicId, source }: { comicId: string; source: MetadataSource }
) => {
try {
// This would trigger a re-fetch from the external source
// Implementation depends on your existing metadata fetching services
throw new Error("Not implemented - requires integration with metadata services");
} catch (error) {
console.error("Error refreshing metadata from source:", error);
throw new Error("Failed to refresh metadata from source");
}
},
/**
* Import a new comic with automatic metadata resolution
*/
importComic: async (_: any, { input }: { input: any }) => {
try {
console.log("Importing comic via GraphQL:", input.filePath);
// 1. Check if comic already exists
const existingComic = await Comic.findOne({
"rawFileDetails.name": input.rawFileDetails?.name,
});
if (existingComic) {
return {
success: false,
comic: existingComic,
message: "Comic already exists in the library",
canonicalMetadataResolved: false,
};
}
// 2. Prepare comic data
const comicData: any = {
importStatus: {
isImported: true,
tagged: false,
},
};
// Add raw file details
if (input.rawFileDetails) {
comicData.rawFileDetails = input.rawFileDetails;
}
// Add inferred metadata
if (input.inferredMetadata) {
comicData.inferredMetadata = input.inferredMetadata;
}
// Add sourced metadata
if (input.sourcedMetadata) {
comicData.sourcedMetadata = {};
if (input.sourcedMetadata.comicInfo) {
comicData.sourcedMetadata.comicInfo = JSON.parse(
input.sourcedMetadata.comicInfo
);
}
if (input.sourcedMetadata.comicvine) {
comicData.sourcedMetadata.comicvine = JSON.parse(
input.sourcedMetadata.comicvine
);
}
if (input.sourcedMetadata.metron) {
comicData.sourcedMetadata.metron = JSON.parse(
input.sourcedMetadata.metron
);
}
if (input.sourcedMetadata.gcd) {
comicData.sourcedMetadata.gcd = JSON.parse(
input.sourcedMetadata.gcd
);
}
if (input.sourcedMetadata.locg) {
comicData.sourcedMetadata.locg = input.sourcedMetadata.locg;
}
}
// Add wanted information
if (input.wanted) {
comicData.wanted = input.wanted;
}
// Add acquisition information
if (input.acquisition) {
comicData.acquisition = input.acquisition;
}
// 3. Create the comic document
const comic = await Comic.create(comicData);
console.log(`Comic created with ID: ${comic._id}`);
// 4. Check if auto-resolution is enabled
const preferences = await UserPreferences.findOne({
userId: "default",
});
let canonicalMetadataResolved = false;
if (
preferences?.autoMerge?.enabled &&
preferences?.autoMerge?.onImport
) {
console.log("Auto-resolving canonical metadata...");
// Build canonical metadata
const canonical = buildCanonicalMetadata(
comic.sourcedMetadata,
convertPreferences(preferences)
);
// Update comic with canonical metadata
comic.canonicalMetadata = canonical;
await comic.save();
canonicalMetadataResolved = true;
console.log("Canonical metadata resolved successfully");
}
return {
success: true,
comic,
message: "Comic imported successfully",
canonicalMetadataResolved,
};
} catch (error) {
console.error("Error importing comic:", error);
throw new Error(`Failed to import comic: ${error.message}`);
}
},
/**
* Update sourced metadata and trigger resolution
*/
updateSourcedMetadata: async (
_: any,
{
comicId,
source,
metadata,
}: { comicId: string; source: MetadataSource; metadata: string }
) => {
try {
const comic = await Comic.findById(comicId);
if (!comic) {
throw new Error("Comic not found");
}
// Parse and update the sourced metadata
const parsedMetadata = JSON.parse(metadata);
const sourceKey = source.toLowerCase();
if (!comic.sourcedMetadata) {
comic.sourcedMetadata = {};
}
comic.sourcedMetadata[sourceKey] = parsedMetadata;
await comic.save();
console.log(
`Updated ${source} metadata for comic ${comicId}`
);
// Check if auto-resolution is enabled
const preferences = await UserPreferences.findOne({
userId: "default",
});
if (
preferences?.autoMerge?.enabled &&
preferences?.autoMerge?.onMetadataUpdate
) {
console.log("Auto-resolving canonical metadata after update...");
// Build canonical metadata
const canonical = buildCanonicalMetadata(
comic.sourcedMetadata,
convertPreferences(preferences)
);
// Update comic with canonical metadata
comic.canonicalMetadata = canonical;
await comic.save();
console.log("Canonical metadata resolved after update");
}
return comic;
} catch (error) {
console.error("Error updating sourced metadata:", error);
throw new Error(`Failed to update sourced metadata: ${error.message}`);
}
},
},
// Field resolvers
Comic: {
id: (comic: any) => comic._id.toString(),
sourcedMetadata: (comic: any) => ({
comicInfo: JSON.stringify(comic.sourcedMetadata?.comicInfo || {}),
comicvine: JSON.stringify(comic.sourcedMetadata?.comicvine || {}),
metron: JSON.stringify(comic.sourcedMetadata?.metron || {}),
gcd: JSON.stringify(comic.sourcedMetadata?.gcd || {}),
locg: comic.sourcedMetadata?.locg || null,
}),
},
UserPreferences: {
id: (prefs: any) => prefs._id.toString(),
fieldPreferences: (prefs: any) => {
if (!prefs.fieldPreferences) return [];
return Array.from(prefs.fieldPreferences.entries()).map(
([field, preferredSource]) => ({
field,
preferredSource,
})
);
},
sourcePriorities: (prefs: any) => {
return prefs.sourcePriorities.map((sp: any) => ({
...sp,
fieldOverrides: sp.fieldOverrides
? Array.from(sp.fieldOverrides.entries()).map(([field, priority]) => ({
field,
priority,
}))
: [],
}));
},
},
};
/**
* Helper: Extract candidates for a field from sourced metadata
*/
function extractCandidatesForField(
field: string,
sourcedMetadata: any
): MetadataField[] {
const candidates: MetadataField[] = [];
// Map field names to source paths
const mappings: Record<string, any> = {
title: [
{ source: MetadataSource.COMICVINE, path: "name", data: sourcedMetadata.comicvine },
{ source: MetadataSource.COMICINFO_XML, path: "Title", data: sourcedMetadata.comicInfo },
{ source: MetadataSource.LOCG, path: "name", data: sourcedMetadata.locg },
],
series: [
{ source: MetadataSource.COMICVINE, path: "volumeInformation.name", data: sourcedMetadata.comicvine },
{ source: MetadataSource.COMICINFO_XML, path: "Series", data: sourcedMetadata.comicInfo },
],
issueNumber: [
{ source: MetadataSource.COMICVINE, path: "issue_number", data: sourcedMetadata.comicvine },
{ source: MetadataSource.COMICINFO_XML, path: "Number", data: sourcedMetadata.comicInfo },
],
description: [
{ source: MetadataSource.COMICVINE, path: "description", data: sourcedMetadata.comicvine },
{ source: MetadataSource.LOCG, path: "description", data: sourcedMetadata.locg },
{ source: MetadataSource.COMICINFO_XML, path: "Summary", data: sourcedMetadata.comicInfo },
],
publisher: [
{ source: MetadataSource.COMICVINE, path: "volumeInformation.publisher.name", data: sourcedMetadata.comicvine },
{ source: MetadataSource.LOCG, path: "publisher", data: sourcedMetadata.locg },
{ source: MetadataSource.COMICINFO_XML, path: "Publisher", data: sourcedMetadata.comicInfo },
],
};
const fieldMappings = mappings[field] || [];
for (const mapping of fieldMappings) {
if (!mapping.data) continue;
const value = getNestedValue(mapping.data, mapping.path);
if (value !== null && value !== undefined) {
candidates.push({
value,
provenance: {
source: mapping.source,
confidence: 0.9,
fetchedAt: new Date(),
},
});
}
}
return candidates;
}
/**
* Helper: Get nested value from object
*/
function getNestedValue(obj: any, path: string): any {
return path.split(".").reduce((current, key) => current?.[key], obj);
}
/**
* Helper: Convert UserPreferences model to ResolutionPreferences
*/
function convertPreferences(prefs: any): ResolutionPreferences {
return {
sourcePriorities: prefs.sourcePriorities.map((sp: any) => ({
source: sp.source,
priority: sp.priority,
enabled: sp.enabled,
fieldOverrides: sp.fieldOverrides,
})),
conflictResolution: prefs.conflictResolution,
minConfidenceThreshold: prefs.minConfidenceThreshold,
preferRecent: prefs.preferRecent,
fieldPreferences: prefs.fieldPreferences,
};
}
/**
* Helper: Get resolution reason for display
*/
function getResolutionReason(
resolved: MetadataField | null,
candidates: MetadataField[],
preferences: any
): string {
if (!resolved) return "No valid candidates";
if (resolved.userOverride) {
return "User override";
}
const priority = preferences.getSourcePriority(resolved.provenance.source);
return `Resolved using ${resolved.provenance.source} (priority: ${priority}, confidence: ${resolved.provenance.confidence})`;
}
/**
* Helper: Apply preferences input to existing preferences
*/
function applyPreferencesInput(prefs: any, input: any): any {
const updated = { ...prefs.toObject() };
if (input.sourcePriorities) {
updated.sourcePriorities = input.sourcePriorities;
}
if (input.conflictResolution) {
updated.conflictResolution = input.conflictResolution;
}
if (input.minConfidenceThreshold !== undefined) {
updated.minConfidenceThreshold = input.minConfidenceThreshold;
}
if (input.preferRecent !== undefined) {
updated.preferRecent = input.preferRecent;
}
return updated;
}

View File

@@ -1,24 +1,442 @@
import { gql } from "graphql-tag";
export const typeDefs = gql`
type Query {
comic(id: ID!): Comic
comics(limit: Int = 10): [Comic]
}
# Metadata source enumeration
enum MetadataSource {
COMICVINE
METRON
GRAND_COMICS_DATABASE
LOCG
COMICINFO_XML
MANUAL
}
type Comic {
id: ID!
title: String
volume: Int
issueNumber: String
publicationDate: String
coverUrl: String
creators: [Creator]
source: String
}
# Conflict resolution strategy
enum ConflictResolutionStrategy {
PRIORITY
CONFIDENCE
RECENCY
MANUAL
HYBRID
}
type Creator {
name: String
role: String
}
# Provenance information for metadata
type Provenance {
source: MetadataSource!
sourceId: String
confidence: Float!
fetchedAt: String!
url: String
}
# Metadata field with provenance
type MetadataField {
value: String
provenance: Provenance!
userOverride: Boolean
}
# Array metadata field with provenance
type MetadataArrayField {
values: [String!]!
provenance: Provenance!
userOverride: Boolean
}
# Creator with role and provenance
type Creator {
name: String!
role: String!
provenance: Provenance!
}
# Canonical metadata - resolved from multiple sources
type CanonicalMetadata {
# Core identifiers
title: MetadataField
series: MetadataField
volume: MetadataField
issueNumber: MetadataField
# Publication info
publisher: MetadataField
publicationDate: MetadataField
coverDate: MetadataField
# Content
description: MetadataField
storyArcs: [MetadataField!]
characters: [MetadataField!]
teams: [MetadataField!]
locations: [MetadataField!]
# Creators
creators: [Creator!]
# Classification
genres: [MetadataField!]
tags: [MetadataField!]
ageRating: MetadataField
# Physical/Digital properties
pageCount: MetadataField
format: MetadataField
# Ratings
communityRating: MetadataField
# Cover
coverImage: MetadataField
}
# Raw file details
type RawFileDetails {
name: String
filePath: String
fileSize: Int
extension: String
mimeType: String
containedIn: String
pageCount: Int
archive: Archive
cover: Cover
}
type Archive {
uncompressed: Boolean
expandedPath: String
}
type Cover {
filePath: String
stats: String
}
# Import status
type ImportStatus {
isImported: Boolean
tagged: Boolean
matchedResult: MatchedResult
}
type MatchedResult {
score: String
}
# Main Comic type with canonical metadata
type Comic {
id: ID!
# Canonical metadata (resolved from all sources)
canonicalMetadata: CanonicalMetadata
# Raw sourced metadata (for transparency)
sourcedMetadata: SourcedMetadata
# File information
rawFileDetails: RawFileDetails
# Import status
importStatus: ImportStatus
# Timestamps
createdAt: String
updatedAt: String
}
# Sourced metadata (raw data from each source)
type SourcedMetadata {
comicInfo: String # JSON string
comicvine: String # JSON string
metron: String # JSON string
gcd: String # JSON string
locg: LOCGMetadata
}
type LOCGMetadata {
name: String
publisher: String
url: String
cover: String
description: String
price: String
rating: Float
pulls: Int
potw: Int
}
# Source priority configuration
type SourcePriority {
source: MetadataSource!
priority: Int!
enabled: Boolean!
fieldOverrides: [FieldOverride!]
}
type FieldOverride {
field: String!
priority: Int!
}
# User preferences for metadata resolution
type UserPreferences {
id: ID!
userId: String!
sourcePriorities: [SourcePriority!]!
conflictResolution: ConflictResolutionStrategy!
minConfidenceThreshold: Float!
preferRecent: Boolean!
fieldPreferences: [FieldPreference!]
autoMerge: AutoMergeSettings!
createdAt: String
updatedAt: String
}
type FieldPreference {
field: String!
preferredSource: MetadataSource!
}
type AutoMergeSettings {
enabled: Boolean!
onImport: Boolean!
onMetadataUpdate: Boolean!
}
# Pagination
type ComicConnection {
comics: [Comic!]!
totalCount: Int!
pageInfo: PageInfo!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
currentPage: Int!
totalPages: Int!
}
# Metadata conflict information
type MetadataConflict {
field: String!
candidates: [MetadataField!]!
resolved: MetadataField
resolutionReason: String!
}
# Queries
type Query {
# Get a single comic by ID
comic(id: ID!): Comic
# List comics with pagination and filtering
comics(
limit: Int = 10
page: Int = 1
search: String
publisher: String
series: String
): ComicConnection!
# Get user preferences
userPreferences(userId: String = "default"): UserPreferences
# Analyze metadata conflicts for a comic
analyzeMetadataConflicts(comicId: ID!): [MetadataConflict!]!
# Preview canonical metadata resolution without saving
previewCanonicalMetadata(
comicId: ID!
preferences: UserPreferencesInput
): CanonicalMetadata
}
# Mutations
type Mutation {
# Update user preferences
updateUserPreferences(
userId: String = "default"
preferences: UserPreferencesInput!
): UserPreferences!
# Manually set a metadata field (creates user override)
setMetadataField(
comicId: ID!
field: String!
value: String!
): Comic!
# Trigger metadata resolution for a comic
resolveMetadata(comicId: ID!): Comic!
# Bulk resolve metadata for multiple comics
bulkResolveMetadata(comicIds: [ID!]!): [Comic!]!
# Remove user override for a field
removeMetadataOverride(comicId: ID!, field: String!): Comic!
# Refresh metadata from a specific source
refreshMetadataFromSource(
comicId: ID!
source: MetadataSource!
): Comic!
# Import a new comic with automatic metadata resolution
importComic(input: ImportComicInput!): ImportComicResult!
# Update sourced metadata and trigger resolution
updateSourcedMetadata(
comicId: ID!
source: MetadataSource!
metadata: String!
): Comic!
}
# Input types
input UserPreferencesInput {
sourcePriorities: [SourcePriorityInput!]
conflictResolution: ConflictResolutionStrategy
minConfidenceThreshold: Float
preferRecent: Boolean
fieldPreferences: [FieldPreferenceInput!]
autoMerge: AutoMergeSettingsInput
}
input SourcePriorityInput {
source: MetadataSource!
priority: Int!
enabled: Boolean!
fieldOverrides: [FieldOverrideInput!]
}
input FieldOverrideInput {
field: String!
priority: Int!
}
input FieldPreferenceInput {
field: String!
preferredSource: MetadataSource!
}
input AutoMergeSettingsInput {
enabled: Boolean
onImport: Boolean
onMetadataUpdate: Boolean
}
# Import comic input
input ImportComicInput {
filePath: String!
fileSize: Int
sourcedMetadata: SourcedMetadataInput
inferredMetadata: InferredMetadataInput
rawFileDetails: RawFileDetailsInput
wanted: WantedInput
acquisition: AcquisitionInput
}
input SourcedMetadataInput {
comicInfo: String
comicvine: String
metron: String
gcd: String
locg: LOCGMetadataInput
}
input LOCGMetadataInput {
name: String
publisher: String
url: String
cover: String
description: String
price: String
rating: Float
pulls: Int
potw: Int
}
input InferredMetadataInput {
issue: IssueInput
}
input IssueInput {
name: String
number: Int
year: String
subtitle: String
}
input RawFileDetailsInput {
name: String!
filePath: String!
fileSize: Int
extension: String
mimeType: String
containedIn: String
pageCount: Int
archive: ArchiveInput
cover: CoverInput
}
input ArchiveInput {
uncompressed: Boolean
expandedPath: String
}
input CoverInput {
filePath: String
stats: String
}
input WantedInput {
source: String
markEntireVolumeWanted: Boolean
issues: [WantedIssueInput!]
volume: WantedVolumeInput
}
input WantedIssueInput {
id: Int
url: String
image: [String!]
coverDate: String
issueNumber: String
}
input WantedVolumeInput {
id: Int
url: String
image: [String!]
name: String
}
input AcquisitionInput {
source: AcquisitionSourceInput
directconnect: DirectConnectInput
}
input AcquisitionSourceInput {
wanted: Boolean
name: String
}
input DirectConnectInput {
downloads: [DirectConnectBundleInput!]
}
input DirectConnectBundleInput {
bundleId: Int
name: String
size: String
}
# Import result
type ImportComicResult {
success: Boolean!
comic: Comic
message: String
canonicalMetadataResolved: Boolean!
}
`;