🔨 graphql refactor for missingFiles flag
This commit is contained in:
@@ -767,7 +767,7 @@ export const resolvers = {
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const broker = context?.broker;
|
const broker = context?.broker;
|
||||||
|
|
||||||
if (!broker) {
|
if (!broker) {
|
||||||
throw new Error("Broker not available in context");
|
throw new Error("Broker not available in context");
|
||||||
}
|
}
|
||||||
@@ -780,6 +780,111 @@ export const resolvers = {
|
|||||||
throw new Error(`Failed to fetch active import session: ${error.message}`);
|
throw new Error(`Failed to fetch active import session: ${error.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
searchComicVine: async (
|
||||||
|
_: any,
|
||||||
|
{ searchTerms, exactMatch }: { searchTerms: string; exactMatch?: boolean },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("library.volumeBasedSearch", { searchTerms, exactMatch });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching ComicVine:", error);
|
||||||
|
throw new Error(`Failed to search ComicVine: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: async (
|
||||||
|
_: any,
|
||||||
|
{ settingsKey }: { settingsKey?: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("settings.getSettings", settingsKey ? { settingsKey } : {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching settings:", error);
|
||||||
|
throw new Error(`Failed to fetch settings: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hubs: async (
|
||||||
|
_: any,
|
||||||
|
{ host }: { host: any },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("airdcpp.getHubs", { host });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching hubs:", error);
|
||||||
|
throw new Error(`Failed to fetch hubs: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
bundles: async (
|
||||||
|
_: any,
|
||||||
|
{ comicObjectId, config }: { comicObjectId: string; config?: any },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("library.getBundles", { comicObjectId, config });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching bundles:", error);
|
||||||
|
throw new Error(`Failed to fetch bundles: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
torrentJobs: async (
|
||||||
|
_: any,
|
||||||
|
{ trigger }: { trigger: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("torrentjobs.getTorrentData", { trigger });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching torrent jobs:", error);
|
||||||
|
throw new Error(`Failed to fetch torrent jobs: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
searchTorrents: async (
|
||||||
|
_: any,
|
||||||
|
{ query }: { query: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("prowlarr.search", { query });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching torrents:", error);
|
||||||
|
throw new Error(`Failed to search torrents: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
walkFolders: async (
|
||||||
|
_: any,
|
||||||
|
{ basePathToWalk, extensions }: { basePathToWalk: string; extensions?: string[] },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("library.walkFolders", { basePathToWalk, extensions });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error walking folders:", error);
|
||||||
|
throw new Error(`Failed to walk folders: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@@ -1547,7 +1652,7 @@ export const resolvers = {
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const broker = context?.broker;
|
const broker = context?.broker;
|
||||||
|
|
||||||
if (!broker) {
|
if (!broker) {
|
||||||
throw new Error("Broker not available in context");
|
throw new Error("Broker not available in context");
|
||||||
}
|
}
|
||||||
@@ -1567,6 +1672,52 @@ export const resolvers = {
|
|||||||
throw new Error(`Failed to force complete session: ${error.message}`);
|
throw new Error(`Failed to force complete session: ${error.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applyComicVineMatch: async (
|
||||||
|
_: any,
|
||||||
|
{ comicObjectId, match }: { comicObjectId: string; match: any },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("library.applyComicVineMetadata", { comicObjectId, match });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error applying ComicVine match:", error);
|
||||||
|
throw new Error(`Failed to apply ComicVine match: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
analyzeImage: async (
|
||||||
|
_: any,
|
||||||
|
{ imageFilePath }: { imageFilePath: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
return await broker.call("imagetransformation.analyze", { imageFilePath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error analyzing image:", error);
|
||||||
|
throw new Error(`Failed to analyze image: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uncompressArchive: async (
|
||||||
|
_: any,
|
||||||
|
{ filePath, comicObjectId, options }: { filePath: string; comicObjectId: string; options?: any },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const broker = context?.broker;
|
||||||
|
if (!broker) throw new Error("Broker not available in context");
|
||||||
|
await broker.call("library.uncompressFullArchive", { filePath, comicObjectId, options });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uncompressing archive:", error);
|
||||||
|
throw new Error(`Failed to uncompress archive: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1681,6 +1832,29 @@ export const resolvers = {
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Custom scalars
|
||||||
|
JSON: {
|
||||||
|
serialize: (value: any) => value,
|
||||||
|
parseValue: (value: any) => value,
|
||||||
|
parseLiteral: (ast: any) => {
|
||||||
|
// Handle basic scalar literals; complex objects are passed as variables
|
||||||
|
switch (ast.kind) {
|
||||||
|
case "StringValue": return ast.value;
|
||||||
|
case "IntValue": return parseInt(ast.value, 10);
|
||||||
|
case "FloatValue": return parseFloat(ast.value);
|
||||||
|
case "BooleanValue": return ast.value;
|
||||||
|
case "NullValue": return null;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
PredicateInput: {
|
||||||
|
serialize: (value: any) => value,
|
||||||
|
parseValue: (value: any) => value,
|
||||||
|
parseLiteral: (ast: any) => ast.value ?? null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ import { gql } from "graphql-tag";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
|
# Arbitrary JSON scalar
|
||||||
|
scalar JSON
|
||||||
|
|
||||||
# Metadata source enumeration
|
# Metadata source enumeration
|
||||||
enum MetadataSource {
|
enum MetadataSource {
|
||||||
COMICVINE
|
COMICVINE
|
||||||
@@ -353,6 +356,27 @@ export const typeDefs = gql`
|
|||||||
|
|
||||||
# Get active import session (if any)
|
# Get active import session (if any)
|
||||||
getActiveImportSession: ImportSession
|
getActiveImportSession: ImportSession
|
||||||
|
|
||||||
|
# Search ComicVine for volumes by name
|
||||||
|
searchComicVine(searchTerms: String!, exactMatch: Boolean): ComicVineSearchResult!
|
||||||
|
|
||||||
|
# Get all app settings (optionally filtered by key)
|
||||||
|
settings(settingsKey: String): AppSettings
|
||||||
|
|
||||||
|
# Get AirDC++ hubs for a given host
|
||||||
|
hubs(host: HostInput!): [Hub!]!
|
||||||
|
|
||||||
|
# Get AirDC++ bundles for a comic object
|
||||||
|
bundles(comicObjectId: ID!, config: JSON): [Bundle!]!
|
||||||
|
|
||||||
|
# Enqueue a repeating torrent data polling job
|
||||||
|
torrentJobs(trigger: String!): TorrentJob
|
||||||
|
|
||||||
|
# Search Prowlarr for torrents
|
||||||
|
searchTorrents(query: String!): [TorrentSearchResult!]!
|
||||||
|
|
||||||
|
# Walk a folder and return matching comic file paths
|
||||||
|
walkFolders(basePathToWalk: String!, extensions: [String!]): [String!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mutations
|
# Mutations
|
||||||
@@ -406,6 +430,15 @@ export const typeDefs = gql`
|
|||||||
|
|
||||||
# Force complete a stuck import session
|
# Force complete a stuck import session
|
||||||
forceCompleteSession(sessionId: String!): ForceCompleteResult!
|
forceCompleteSession(sessionId: String!): ForceCompleteResult!
|
||||||
|
|
||||||
|
# Apply a ComicVine volume match to a comic
|
||||||
|
applyComicVineMatch(comicObjectId: ID!, match: ComicVineMatchInput!): Comic!
|
||||||
|
|
||||||
|
# Analyze an image file for color and metadata
|
||||||
|
analyzeImage(imageFilePath: String!): ImageAnalysisResult!
|
||||||
|
|
||||||
|
# Uncompress an archive (enqueues background job)
|
||||||
|
uncompressArchive(filePath: String!, comicObjectId: ID!, options: JSON): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input types
|
# Input types
|
||||||
@@ -793,4 +826,128 @@ export const typeDefs = gql`
|
|||||||
filesSucceeded: Int!
|
filesSucceeded: Int!
|
||||||
filesFailed: Int!
|
filesFailed: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Host configuration (used by AirDC++, bittorrent, prowlarr)
|
||||||
|
type HostConfig {
|
||||||
|
hostname: String
|
||||||
|
port: String
|
||||||
|
protocol: String
|
||||||
|
username: String
|
||||||
|
password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input HostInput {
|
||||||
|
hostname: String!
|
||||||
|
port: String!
|
||||||
|
protocol: String!
|
||||||
|
username: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# App settings
|
||||||
|
type DirectConnectClient {
|
||||||
|
host: HostConfig
|
||||||
|
airDCPPUserSettings: JSON
|
||||||
|
hubs: [JSON]
|
||||||
|
}
|
||||||
|
|
||||||
|
type DirectConnectSettings {
|
||||||
|
client: DirectConnectClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type BittorrentClient {
|
||||||
|
name: String
|
||||||
|
host: HostConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type BittorrentSettings {
|
||||||
|
client: BittorrentClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProwlarrClient {
|
||||||
|
host: HostConfig
|
||||||
|
apiKey: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProwlarrSettings {
|
||||||
|
client: ProwlarrClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppSettings {
|
||||||
|
directConnect: DirectConnectSettings
|
||||||
|
bittorrent: BittorrentSettings
|
||||||
|
prowlarr: ProwlarrSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
# AirDC++ Hub
|
||||||
|
type Hub {
|
||||||
|
id: Int
|
||||||
|
name: String
|
||||||
|
description: String
|
||||||
|
userCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
# AirDC++ Bundle
|
||||||
|
type Bundle {
|
||||||
|
id: Int
|
||||||
|
name: String
|
||||||
|
size: String
|
||||||
|
status: String
|
||||||
|
speed: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# Torrent search result (from Prowlarr)
|
||||||
|
type TorrentSearchResult {
|
||||||
|
title: String
|
||||||
|
size: Float
|
||||||
|
seeders: Int
|
||||||
|
leechers: Int
|
||||||
|
downloadUrl: String
|
||||||
|
guid: String
|
||||||
|
publishDate: String
|
||||||
|
indexer: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# Torrent job reference
|
||||||
|
type TorrentJob {
|
||||||
|
id: String
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# Image analysis result
|
||||||
|
type ImageAnalysisResult {
|
||||||
|
analyzedData: JSON
|
||||||
|
colorHistogramData: JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
# ComicVine volume search result
|
||||||
|
type ComicVineVolume {
|
||||||
|
id: Int
|
||||||
|
name: String
|
||||||
|
publisher: Publisher
|
||||||
|
start_year: String
|
||||||
|
count_of_issues: Int
|
||||||
|
image: VolumeImage
|
||||||
|
api_detail_url: String
|
||||||
|
site_detail_url: String
|
||||||
|
description: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComicVineSearchResult {
|
||||||
|
results: [ComicVineVolume!]!
|
||||||
|
total: Int!
|
||||||
|
limit: Int
|
||||||
|
offset: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for applying a ComicVine match
|
||||||
|
input ComicVineMatchInput {
|
||||||
|
volume: ComicVineVolumeRefInput!
|
||||||
|
volumeInformation: JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
input ComicVineVolumeRefInput {
|
||||||
|
api_detail_url: String!
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export default {
|
|||||||
settings: {
|
settings: {
|
||||||
/** Remote metadata GraphQL endpoint URL */
|
/** Remote metadata GraphQL endpoint URL */
|
||||||
metadataGraphqlUrl: process.env.METADATA_GRAPHQL_URL || "http://localhost:3080/metadata-graphql",
|
metadataGraphqlUrl: process.env.METADATA_GRAPHQL_URL || "http://localhost:3080/metadata-graphql",
|
||||||
|
/** Remote acquisition GraphQL endpoint URL */
|
||||||
|
acquisitionGraphqlUrl: process.env.ACQUISITION_GRAPHQL_URL || "http://localhost:3060/acquisition-graphql",
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -222,61 +224,34 @@ export default {
|
|||||||
|
|
||||||
const localSchema = makeExecutableSchema({ typeDefs, resolvers });
|
const localSchema = makeExecutableSchema({ typeDefs, resolvers });
|
||||||
|
|
||||||
|
const subschemas: any[] = [{ schema: localSchema }];
|
||||||
|
|
||||||
|
// Stitch metadata schema
|
||||||
try {
|
try {
|
||||||
this.logger.info(`Attempting to introspect remote schema at ${this.settings.metadataGraphqlUrl}`);
|
this.logger.info(`Attempting to introspect remote schema at ${this.settings.metadataGraphqlUrl}`);
|
||||||
|
const metadataSchema = await fetchRemoteSchema(this.settings.metadataGraphqlUrl);
|
||||||
const remoteSchema = await fetchRemoteSchema(this.settings.metadataGraphqlUrl);
|
subschemas.push({ schema: metadataSchema, executor: createRemoteExecutor(this.settings.metadataGraphqlUrl) });
|
||||||
this.logger.info("✓ Successfully introspected remote metadata schema");
|
this.logger.info("✓ Successfully introspected remote metadata schema");
|
||||||
|
} catch (error: any) {
|
||||||
const remoteQueryType = remoteSchema.getQueryType();
|
this.logger.warn(`⚠ Metadata schema unavailable: ${error.message}`);
|
||||||
const remoteMutationType = remoteSchema.getMutationType();
|
}
|
||||||
|
|
||||||
if (remoteQueryType) {
|
// Stitch acquisition schema
|
||||||
const remoteQueryFields = Object.keys(remoteQueryType.getFields());
|
try {
|
||||||
this.logger.info(`✓ Remote schema has ${remoteQueryFields.length} Query fields: ${remoteQueryFields.join(', ')}`);
|
this.logger.info(`Attempting to introspect remote schema at ${this.settings.acquisitionGraphqlUrl}`);
|
||||||
}
|
const acquisitionSchema = await fetchRemoteSchema(this.settings.acquisitionGraphqlUrl);
|
||||||
|
subschemas.push({ schema: acquisitionSchema, executor: createRemoteExecutor(this.settings.acquisitionGraphqlUrl) });
|
||||||
if (remoteMutationType) {
|
this.logger.info("✓ Successfully introspected remote acquisition schema");
|
||||||
const remoteMutationFields = Object.keys(remoteMutationType.getFields());
|
} catch (error: any) {
|
||||||
this.logger.info(`✓ Remote schema has ${remoteMutationFields.length} Mutation fields: ${remoteMutationFields.join(', ')}`);
|
this.logger.warn(`⚠ Acquisition schema unavailable: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.schema = stitchSchemas({
|
if (subschemas.length > 1) {
|
||||||
subschemas: [
|
this.schema = stitchSchemas({ subschemas, mergeTypes: true });
|
||||||
{ schema: localSchema },
|
this.logger.info(`✓ Stitched ${subschemas.length} schemas`);
|
||||||
{ schema: remoteSchema, executor: createRemoteExecutor(this.settings.metadataGraphqlUrl) },
|
|
||||||
],
|
|
||||||
mergeTypes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stitchedQueryType = this.schema.getQueryType();
|
|
||||||
const stitchedMutationType = this.schema.getMutationType();
|
|
||||||
|
|
||||||
if (stitchedQueryType) {
|
|
||||||
const stitchedQueryFields = Object.keys(stitchedQueryType.getFields());
|
|
||||||
this.logger.info(`✓ Stitched schema has ${stitchedQueryFields.length} Query fields`);
|
|
||||||
|
|
||||||
// Verify critical remote fields are present
|
|
||||||
const criticalFields = ['getWeeklyPullList'];
|
|
||||||
const missingFields = criticalFields.filter(field => !stitchedQueryFields.includes(field));
|
|
||||||
if (missingFields.length > 0) {
|
|
||||||
this.logger.warn(`⚠ Missing expected remote fields: ${missingFields.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stitchedMutationType) {
|
|
||||||
const stitchedMutationFields = Object.keys(stitchedMutationType.getFields());
|
|
||||||
this.logger.info(`✓ Stitched schema has ${stitchedMutationFields.length} Mutation fields`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info("✓ Successfully stitched local and remote schemas");
|
|
||||||
this.remoteSchemaAvailable = true;
|
this.remoteSchemaAvailable = true;
|
||||||
} catch (remoteError: any) {
|
} else {
|
||||||
this.logger.error(`✗ Failed to connect to remote metadata GraphQL at ${this.settings.metadataGraphqlUrl}`);
|
|
||||||
this.logger.error(`✗ Error: ${remoteError.message}`);
|
|
||||||
this.logger.warn("⚠ FALLING BACK TO LOCAL SCHEMA ONLY");
|
this.logger.warn("⚠ FALLING BACK TO LOCAL SCHEMA ONLY");
|
||||||
this.logger.warn("⚠ Remote queries like 'getWeeklyPullList' will NOT be available");
|
|
||||||
this.logger.warn(`⚠ To fix: Ensure metadata-graphql service is running at ${this.settings.metadataGraphqlUrl}`);
|
|
||||||
this.schema = localSchema;
|
this.schema = localSchema;
|
||||||
this.remoteSchemaAvailable = false;
|
this.remoteSchemaAvailable = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -832,6 +832,77 @@ export default class ImportService extends Service {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reconcileLibrary: {
|
||||||
|
rest: "POST /reconcileLibrary",
|
||||||
|
timeout: 120000,
|
||||||
|
async handler(ctx: Context<{ directoryPath?: string }>) {
|
||||||
|
const resolvedPath = path.resolve(
|
||||||
|
ctx.params.directoryPath || COMICS_DIRECTORY
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Collect all comic file paths currently on disk
|
||||||
|
const localPaths = new Set<string>();
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
klaw(resolvedPath)
|
||||||
|
.on("error", reject)
|
||||||
|
.pipe(
|
||||||
|
through2.obj(function (item, enc, next) {
|
||||||
|
if (
|
||||||
|
item.stats.isFile() &&
|
||||||
|
[".cbz", ".cbr", ".cb7"].includes(
|
||||||
|
path.extname(item.path)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.push(item.path);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.on("data", (p: string) => localPaths.add(p))
|
||||||
|
.on("end", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Get every DB record that has a stored filePath
|
||||||
|
const comics = await Comic.find(
|
||||||
|
{ "rawFileDetails.filePath": { $exists: true, $ne: null } },
|
||||||
|
{ _id: 1, "rawFileDetails.filePath": 1 }
|
||||||
|
).lean();
|
||||||
|
|
||||||
|
const nowMissing: any[] = [];
|
||||||
|
const nowPresent: any[] = [];
|
||||||
|
|
||||||
|
for (const comic of comics) {
|
||||||
|
const stored = comic.rawFileDetails?.filePath;
|
||||||
|
if (!stored) continue;
|
||||||
|
if (localPaths.has(stored)) {
|
||||||
|
nowPresent.push(comic._id);
|
||||||
|
} else {
|
||||||
|
nowMissing.push(comic._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Apply updates in bulk
|
||||||
|
if (nowMissing.length > 0) {
|
||||||
|
await Comic.updateMany(
|
||||||
|
{ _id: { $in: nowMissing } },
|
||||||
|
{ $set: { "importStatus.isRawFileMissing": true } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nowPresent.length > 0) {
|
||||||
|
await Comic.updateMany(
|
||||||
|
{ _id: { $in: nowPresent } },
|
||||||
|
{ $set: { "importStatus.isRawFileMissing": false } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scanned: localPaths.size,
|
||||||
|
markedMissing: nowMissing.length,
|
||||||
|
cleared: nowPresent.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
getComicsMarkedAsWanted: {
|
getComicsMarkedAsWanted: {
|
||||||
|
|
||||||
rest: "GET /getComicsMarkedAsWanted",
|
rest: "GET /getComicsMarkedAsWanted",
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ export default class SettingsService extends Service {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "missingFiles":
|
||||||
|
Object.assign(eSQuery, {
|
||||||
|
term: query,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"Searching ElasticSearch index with this query -> "
|
"Searching ElasticSearch index with this query -> "
|
||||||
|
|||||||
Reference in New Issue
Block a user