🔨 Fixes for import statuses
This commit is contained in:
@@ -6,18 +6,6 @@ import ApiGateway from "moleculer-web";
|
||||
import debounce from "lodash/debounce";
|
||||
import { IFolderData } from "threetwo-ui-typings";
|
||||
|
||||
/**
|
||||
* Import statistics cache for real-time updates
|
||||
*/
|
||||
interface ImportStatisticsCache {
|
||||
totalLocalFiles: number;
|
||||
alreadyImported: number;
|
||||
newFiles: number;
|
||||
percentageImported: string;
|
||||
lastUpdated: Date;
|
||||
pendingFiles: Set<string>; // Files in stabilization period
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiService exposes REST endpoints and watches the comics directory for changes.
|
||||
* It uses chokidar to monitor filesystem events and broadcasts them via the Moleculer broker.
|
||||
@@ -30,18 +18,6 @@ export default class ApiService extends Service {
|
||||
*/
|
||||
private fileWatcher?: any;
|
||||
|
||||
/**
|
||||
* Import statistics cache for real-time updates
|
||||
* @private
|
||||
*/
|
||||
private statsCache: ImportStatisticsCache | null = null;
|
||||
|
||||
/**
|
||||
* Debounced function to broadcast statistics updates
|
||||
* @private
|
||||
*/
|
||||
private broadcastStatsUpdate?: ReturnType<typeof debounce>;
|
||||
|
||||
/**
|
||||
* Creates an instance of ApiService.
|
||||
* @param {ServiceBroker} broker - The Moleculer service broker instance.
|
||||
@@ -134,61 +110,6 @@ export default class ApiService extends Service {
|
||||
assets: { folder: "public", options: {} },
|
||||
},
|
||||
events: {
|
||||
/**
|
||||
* Listen for import session completion to refresh statistics
|
||||
*/
|
||||
"IMPORT_SESSION_COMPLETED": {
|
||||
async handler(ctx: Context<{
|
||||
sessionId: string;
|
||||
type: string;
|
||||
success: boolean;
|
||||
stats: any;
|
||||
}>) {
|
||||
const { sessionId, type, success } = ctx.params;
|
||||
this.logger.info(
|
||||
`[Stats Cache] Import session completed: ${sessionId} (${type}, success: ${success})`
|
||||
);
|
||||
|
||||
// Invalidate and refresh statistics cache
|
||||
await this.actions.invalidateStatsCache();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen for import progress to update cache incrementally
|
||||
*/
|
||||
"IMPORT_PROGRESS": {
|
||||
async handler(ctx: Context<{
|
||||
sessionId: string;
|
||||
stats: any;
|
||||
}>) {
|
||||
// Update cache with current progress
|
||||
if (this.statsCache) {
|
||||
const { stats } = ctx.params;
|
||||
// Update alreadyImported count based on files succeeded
|
||||
if (stats.filesSucceeded) {
|
||||
this.statsCache.alreadyImported += 1;
|
||||
this.statsCache.newFiles = Math.max(0, this.statsCache.newFiles - 1);
|
||||
|
||||
// Recalculate percentage
|
||||
if (this.statsCache.totalLocalFiles > 0) {
|
||||
const percentage = (
|
||||
(this.statsCache.alreadyImported / this.statsCache.totalLocalFiles) * 100
|
||||
).toFixed(2);
|
||||
this.statsCache.percentageImported = `${percentage}%`;
|
||||
}
|
||||
|
||||
this.statsCache.lastUpdated = new Date();
|
||||
|
||||
// Trigger debounced broadcast
|
||||
if (this.broadcastStatsUpdate) {
|
||||
this.broadcastStatsUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen for watcher disable events
|
||||
*/
|
||||
@@ -230,192 +151,8 @@ export default class ApiService extends Service {
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* Get cached import statistics (fast, no filesystem scan)
|
||||
* @returns Cached statistics or null if not initialized
|
||||
*/
|
||||
getCachedImportStatistics: {
|
||||
rest: "GET /cachedImportStatistics",
|
||||
async handler() {
|
||||
// If cache not initialized, try to initialize it now
|
||||
if (!this.statsCache) {
|
||||
this.logger.info("[Stats Cache] Cache not initialized, initializing now...");
|
||||
try {
|
||||
await this.initializeStatsCache();
|
||||
} catch (error) {
|
||||
this.logger.error("[Stats Cache] Failed to initialize:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to initialize statistics cache",
|
||||
stats: null,
|
||||
lastUpdated: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check again after initialization attempt
|
||||
if (!this.statsCache) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Statistics cache not initialized yet",
|
||||
stats: null,
|
||||
lastUpdated: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats: {
|
||||
totalLocalFiles: this.statsCache.totalLocalFiles,
|
||||
alreadyImported: this.statsCache.alreadyImported,
|
||||
newFiles: this.statsCache.newFiles,
|
||||
percentageImported: this.statsCache.percentageImported,
|
||||
pendingFiles: this.statsCache.pendingFiles.size,
|
||||
},
|
||||
lastUpdated: this.statsCache.lastUpdated.toISOString(),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate statistics cache (force refresh on next request)
|
||||
*/
|
||||
invalidateStatsCache: {
|
||||
async handler() {
|
||||
this.logger.info("[Stats Cache] Invalidating cache...");
|
||||
await this.initializeStatsCache();
|
||||
return { success: true, message: "Cache invalidated and refreshed" };
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Initialize statistics cache by fetching current import statistics
|
||||
* @private
|
||||
*/
|
||||
initializeStatsCache: async function() {
|
||||
try {
|
||||
this.logger.info("[Stats Cache] Initializing import statistics cache...");
|
||||
const stats = await this.broker.call("library.getImportStatistics", {});
|
||||
|
||||
if (stats && stats.success) {
|
||||
this.statsCache = {
|
||||
totalLocalFiles: stats.stats.totalLocalFiles,
|
||||
alreadyImported: stats.stats.alreadyImported,
|
||||
newFiles: stats.stats.newFiles,
|
||||
percentageImported: stats.stats.percentageImported,
|
||||
lastUpdated: new Date(),
|
||||
pendingFiles: new Set<string>(),
|
||||
};
|
||||
this.logger.info("[Stats Cache] Cache initialized successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("[Stats Cache] Failed to initialize cache:", error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update statistics cache when files are added or removed
|
||||
* @param event - File event type ('add' or 'unlink')
|
||||
* @param filePath - Path to the file
|
||||
* @private
|
||||
*/
|
||||
updateStatsCache: function(event: string, filePath: string) {
|
||||
if (!this.statsCache) return;
|
||||
|
||||
const fileExtension = path.extname(filePath);
|
||||
const isComicFile = [".cbz", ".cbr", ".cb7"].includes(fileExtension);
|
||||
|
||||
if (!isComicFile) return;
|
||||
|
||||
if (event === "add") {
|
||||
// Add to pending files (in stabilization period)
|
||||
this.statsCache.pendingFiles.add(filePath);
|
||||
this.statsCache.totalLocalFiles++;
|
||||
this.statsCache.newFiles++;
|
||||
} else if (event === "unlink") {
|
||||
// Remove from pending if it was there
|
||||
this.statsCache.pendingFiles.delete(filePath);
|
||||
this.statsCache.totalLocalFiles--;
|
||||
// Could be either new or already imported, but we'll decrement newFiles for safety
|
||||
if (this.statsCache.newFiles > 0) {
|
||||
this.statsCache.newFiles--;
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate percentage
|
||||
if (this.statsCache.totalLocalFiles > 0) {
|
||||
const percentage = ((this.statsCache.alreadyImported / this.statsCache.totalLocalFiles) * 100).toFixed(2);
|
||||
this.statsCache.percentageImported = `${percentage}%`;
|
||||
} else {
|
||||
this.statsCache.percentageImported = "0.00%";
|
||||
}
|
||||
|
||||
this.statsCache.lastUpdated = new Date();
|
||||
|
||||
// Trigger debounced broadcast
|
||||
if (this.broadcastStatsUpdate) {
|
||||
this.broadcastStatsUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast statistics update via Socket.IO
|
||||
* @private
|
||||
*/
|
||||
broadcastStats: async function() {
|
||||
if (!this.statsCache) return;
|
||||
|
||||
try {
|
||||
await this.broker.call("socket.broadcast", {
|
||||
namespace: "/",
|
||||
event: "IMPORT_STATISTICS_UPDATED",
|
||||
args: [{
|
||||
stats: {
|
||||
totalLocalFiles: this.statsCache.totalLocalFiles,
|
||||
alreadyImported: this.statsCache.alreadyImported,
|
||||
newFiles: this.statsCache.newFiles,
|
||||
percentageImported: this.statsCache.percentageImported,
|
||||
pendingFiles: this.statsCache.pendingFiles.size,
|
||||
},
|
||||
lastUpdated: this.statsCache.lastUpdated.toISOString(),
|
||||
}],
|
||||
});
|
||||
this.logger.debug("[Stats Cache] Broadcasted statistics update");
|
||||
} catch (error) {
|
||||
this.logger.error("[Stats Cache] Failed to broadcast statistics:", error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark a file as imported (moved from pending to imported)
|
||||
* @param filePath - Path to the imported file
|
||||
* @private
|
||||
*/
|
||||
markFileAsImported: function(filePath: string) {
|
||||
if (!this.statsCache) return;
|
||||
|
||||
this.statsCache.pendingFiles.delete(filePath);
|
||||
this.statsCache.alreadyImported++;
|
||||
if (this.statsCache.newFiles > 0) {
|
||||
this.statsCache.newFiles--;
|
||||
}
|
||||
|
||||
// Recalculate percentage
|
||||
if (this.statsCache.totalLocalFiles > 0) {
|
||||
const percentage = ((this.statsCache.alreadyImported / this.statsCache.totalLocalFiles) * 100).toFixed(2);
|
||||
this.statsCache.percentageImported = `${percentage}%`;
|
||||
}
|
||||
|
||||
this.statsCache.lastUpdated = new Date();
|
||||
|
||||
// Trigger debounced broadcast
|
||||
if (this.broadcastStatsUpdate) {
|
||||
this.broadcastStatsUpdate();
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
methods: {},
|
||||
started: this.startWatcher,
|
||||
stopped: this.stopWatcher,
|
||||
});
|
||||
@@ -439,20 +176,6 @@ export default class ApiService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize debounced broadcast function (2 second debounce for statistics updates)
|
||||
this.broadcastStatsUpdate = debounce(
|
||||
() => {
|
||||
this.broadcastStats();
|
||||
},
|
||||
2000,
|
||||
{ leading: false, trailing: true }
|
||||
);
|
||||
|
||||
// Initialize statistics cache (async, but don't block watcher startup)
|
||||
this.initializeStatsCache().catch(err => {
|
||||
this.logger.error("[Stats Cache] Failed to initialize on startup:", err);
|
||||
});
|
||||
|
||||
this.fileWatcher = chokidar.watch(watchDir, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
@@ -534,11 +257,6 @@ export default class ApiService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// Update statistics cache for add/unlink events
|
||||
if (event === "add" || event === "unlink") {
|
||||
this.updateStatsCache(event, filePath);
|
||||
}
|
||||
|
||||
if (event === "add" && stats) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
@@ -620,9 +338,6 @@ export default class ApiService extends Service {
|
||||
|
||||
this.logger.info(`Successfully imported: ${filePath}`);
|
||||
|
||||
// Mark file as imported in statistics cache
|
||||
this.markFileAsImported(filePath);
|
||||
|
||||
// Complete watcher session
|
||||
await this.broker.call("importstate.completeSession", {
|
||||
sessionId,
|
||||
|
||||
Reference in New Issue
Block a user