398 lines
9.3 KiB
TypeScript
398 lines
9.3 KiB
TypeScript
const paginate = require("mongoose-paginate-v2");
|
|
const { Client } = require("@elastic/elasticsearch");
|
|
import { mongoosastic } from "mongoosastic-ts";
|
|
const mongoose = require("mongoose");
|
|
import {
|
|
MongoosasticDocument,
|
|
MongoosasticModel,
|
|
MongoosasticPluginOpts,
|
|
} from "mongoosastic-ts/dist/types";
|
|
const ELASTICSEARCH_HOST =
|
|
process.env.ELASTICSEARCH_URI || "http://localhost:9200";
|
|
console.log(`ELASTICSEARCH -> ${ELASTICSEARCH_HOST}`);
|
|
export const eSClient = new Client({
|
|
node: ELASTICSEARCH_HOST,
|
|
auth: {
|
|
username: "elastic",
|
|
password: "password",
|
|
},
|
|
});
|
|
|
|
// Metadata source enumeration
|
|
export enum MetadataSource {
|
|
COMICVINE = "comicvine",
|
|
METRON = "metron",
|
|
GRAND_COMICS_DATABASE = "gcd",
|
|
LOCG = "locg",
|
|
COMICINFO_XML = "comicinfo",
|
|
MANUAL = "manual",
|
|
}
|
|
|
|
// Provenance schema - tracks where each piece of metadata came from
|
|
const ProvenanceSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
source: {
|
|
type: String,
|
|
enum: Object.values(MetadataSource),
|
|
required: true,
|
|
},
|
|
sourceId: String, // External ID from the source (e.g., ComicVine ID)
|
|
confidence: { type: Number, min: 0, max: 1, default: 1 }, // 0-1 confidence score
|
|
fetchedAt: { type: Date, default: Date.now },
|
|
url: String, // Source URL if applicable
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Individual metadata field with provenance
|
|
const MetadataFieldSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
value: mongoose.Schema.Types.Mixed, // The actual value
|
|
provenance: ProvenanceSchema, // Where it came from
|
|
userOverride: { type: Boolean, default: false }, // User manually set this
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Creator with provenance
|
|
const CreatorSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
name: String,
|
|
role: String, // writer, artist, colorist, letterer, etc.
|
|
id: String, // External ID from source (e.g., Metron creator ID)
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Story Arc with provenance
|
|
const StoryArcSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
name: String,
|
|
number: Number, // Issue's position in the arc
|
|
id: String, // External ID from source
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Universe schema for multiverse/alternate reality tracking
|
|
const UniverseSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
name: String,
|
|
designation: String, // e.g., "Earth-616", "Earth-25"
|
|
id: String, // External ID from source
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Price information with country codes
|
|
const PriceSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
country: String, // ISO country code (e.g., "US", "GB")
|
|
amount: Number,
|
|
currency: String, // ISO currency code (e.g., "USD", "GBP")
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// External IDs from various sources
|
|
const ExternalIDSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
source: String, // e.g., "Metron", "Comic Vine", "Grand Comics Database", "MangaDex"
|
|
id: String,
|
|
primary: { type: Boolean, default: false },
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// GTIN (Global Trade Item Number) - includes ISBN, UPC, etc.
|
|
const GTINSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
isbn: String,
|
|
upc: String,
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Reprint information
|
|
const ReprintSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
description: String, // e.g., "Foo Bar #001 (2002)"
|
|
id: String, // External ID from source
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// URL with primary flag
|
|
const URLSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
url: String,
|
|
primary: { type: Boolean, default: false },
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
// Canonical metadata - resolved from multiple sources
|
|
const CanonicalMetadataSchema = new mongoose.Schema(
|
|
{
|
|
_id: false,
|
|
// Core identifiers
|
|
title: MetadataFieldSchema,
|
|
series: MetadataFieldSchema,
|
|
volume: MetadataFieldSchema,
|
|
issueNumber: MetadataFieldSchema,
|
|
|
|
// External IDs from various sources (Metron, ComicVine, GCD, MangaDex, etc.)
|
|
externalIDs: [ExternalIDSchema],
|
|
|
|
// Publication info
|
|
publisher: MetadataFieldSchema,
|
|
imprint: MetadataFieldSchema, // Publisher imprint (e.g., Vertigo for DC Comics)
|
|
publicationDate: MetadataFieldSchema, // Store/release date
|
|
coverDate: MetadataFieldSchema, // Cover date (often different from store date)
|
|
|
|
// Series information
|
|
seriesInfo: {
|
|
type: {
|
|
_id: false,
|
|
id: String, // External series ID
|
|
language: String, // ISO language code (e.g., "en", "de")
|
|
sortName: String, // Alternative sort name
|
|
startYear: Number,
|
|
issueCount: Number, // Total issues in series
|
|
volumeCount: Number, // Total volumes/collections
|
|
alternativeNames: [MetadataFieldSchema], // Alternative series names
|
|
provenance: ProvenanceSchema,
|
|
},
|
|
default: null,
|
|
},
|
|
|
|
// Content
|
|
description: MetadataFieldSchema, // Summary/synopsis
|
|
notes: MetadataFieldSchema, // Additional notes about the issue
|
|
stories: [MetadataFieldSchema], // Story titles within the issue
|
|
storyArcs: [StoryArcSchema], // Story arcs with position tracking
|
|
characters: [MetadataFieldSchema],
|
|
teams: [MetadataFieldSchema],
|
|
locations: [MetadataFieldSchema],
|
|
universes: [UniverseSchema], // Multiverse/alternate reality information
|
|
|
|
// Creators
|
|
creators: [CreatorSchema],
|
|
|
|
// Classification
|
|
genres: [MetadataFieldSchema],
|
|
tags: [MetadataFieldSchema],
|
|
ageRating: MetadataFieldSchema,
|
|
|
|
// Physical/Digital properties
|
|
pageCount: MetadataFieldSchema,
|
|
format: MetadataFieldSchema, // Single Issue, TPB, HC, etc.
|
|
|
|
// Commercial information
|
|
prices: [PriceSchema], // Prices in different countries/currencies
|
|
gtin: GTINSchema, // ISBN, UPC, etc.
|
|
|
|
// Reprints
|
|
reprints: [ReprintSchema], // Information about reprinted content
|
|
|
|
// URLs
|
|
urls: [URLSchema], // External URLs (ComicVine, Metron, etc.)
|
|
|
|
// Ratings and popularity
|
|
communityRating: MetadataFieldSchema,
|
|
|
|
// Cover image
|
|
coverImage: MetadataFieldSchema,
|
|
|
|
// Metadata tracking
|
|
lastModified: MetadataFieldSchema, // Last modification timestamp from source
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
const RawFileDetailsSchema = mongoose.Schema({
|
|
_id: false,
|
|
name: String,
|
|
filePath: String,
|
|
fileSize: Number,
|
|
extension: String,
|
|
mimeType: String,
|
|
containedIn: String,
|
|
pageCount: Number,
|
|
archive: {
|
|
uncompressed: Boolean,
|
|
expandedPath: String,
|
|
},
|
|
cover: {
|
|
filePath: String,
|
|
stats: Object,
|
|
},
|
|
});
|
|
|
|
const LOCGSchema = mongoose.Schema({
|
|
_id: false,
|
|
name: String,
|
|
publisher: String,
|
|
url: String,
|
|
cover: String,
|
|
description: String,
|
|
price: String,
|
|
rating: Number,
|
|
pulls: Number,
|
|
potw: Number,
|
|
});
|
|
|
|
const DirectConnectBundleSchema = mongoose.Schema({
|
|
bundleId: Number,
|
|
name: String,
|
|
size: String,
|
|
type: {},
|
|
_id: false,
|
|
});
|
|
|
|
const wantedSchema = mongoose.Schema(
|
|
{
|
|
source: { type: String, default: null },
|
|
markEntireVolumeWanted: Boolean,
|
|
issues: {
|
|
type: [
|
|
{
|
|
_id: false,
|
|
id: Number,
|
|
url: String,
|
|
image: { type: Array, default: [] },
|
|
coverDate: String,
|
|
issueNumber: String,
|
|
},
|
|
],
|
|
default: null,
|
|
},
|
|
volume: {
|
|
type: {
|
|
_id: false,
|
|
id: Number,
|
|
url: String,
|
|
image: { type: Array, default: [] },
|
|
name: String,
|
|
},
|
|
default: null,
|
|
},
|
|
},
|
|
{ _id: false }
|
|
);
|
|
|
|
const ComicSchema = mongoose.Schema(
|
|
{
|
|
importStatus: {
|
|
isImported: Boolean,
|
|
tagged: Boolean,
|
|
isRawFileMissing: { type: Boolean, default: false },
|
|
matchedResult: {
|
|
score: String,
|
|
},
|
|
},
|
|
userAddedMetadata: {
|
|
tags: [String],
|
|
},
|
|
|
|
// NEW: Canonical metadata with provenance
|
|
canonicalMetadata: {
|
|
type: CanonicalMetadataSchema,
|
|
es_indexed: true,
|
|
default: {},
|
|
},
|
|
|
|
// LEGACY: Keep existing sourced metadata for backward compatibility
|
|
sourcedMetadata: {
|
|
comicInfo: { type: mongoose.Schema.Types.Mixed, default: {} },
|
|
comicvine: { type: mongoose.Schema.Types.Mixed, default: {} },
|
|
metron: { type: mongoose.Schema.Types.Mixed, default: {} },
|
|
gcd: { type: mongoose.Schema.Types.Mixed, default: {} }, // Grand Comics Database
|
|
locg: {
|
|
type: LOCGSchema,
|
|
es_indexed: true,
|
|
default: {},
|
|
},
|
|
},
|
|
|
|
rawFileDetails: {
|
|
type: RawFileDetailsSchema,
|
|
es_indexed: true,
|
|
default: {},
|
|
},
|
|
inferredMetadata: {
|
|
issue: {
|
|
name: { type: String, es_indexed: true },
|
|
number: {
|
|
type: Number,
|
|
es_indexed: true,
|
|
required: false,
|
|
default: 0,
|
|
},
|
|
year: String,
|
|
subtitle: { type: String, es_indexed: true },
|
|
},
|
|
},
|
|
wanted: wantedSchema,
|
|
|
|
acquisition: {
|
|
source: {
|
|
wanted: { type: Boolean, default: false },
|
|
name: { type: String, default: null },
|
|
},
|
|
release: {},
|
|
directconnect: {
|
|
downloads: {
|
|
type: [DirectConnectBundleSchema],
|
|
es_indexed: true,
|
|
default: [],
|
|
},
|
|
},
|
|
torrent: [
|
|
{
|
|
infoHash: String,
|
|
name: String,
|
|
announce: [String],
|
|
},
|
|
],
|
|
usenet: {
|
|
sourceApplication: String,
|
|
},
|
|
},
|
|
},
|
|
{ timestamps: true, minimize: false }
|
|
);
|
|
|
|
ComicSchema.plugin(mongoosastic, {
|
|
index: "comics",
|
|
type: "comic",
|
|
esClient: eSClient,
|
|
} as MongoosasticPluginOpts);
|
|
ComicSchema.plugin(paginate);
|
|
|
|
// Add indexes for performance
|
|
ComicSchema.index({ "rawFileDetails.filePath": 1 }); // For import statistics queries
|
|
ComicSchema.index({ "rawFileDetails.name": 1 }); // For duplicate detection
|
|
ComicSchema.index({ "wanted.volume.id": 1 }); // For wanted comics queries
|
|
|
|
const Comic = mongoose.model("Comic", ComicSchema);
|
|
export default Comic;
|