diff --git a/src/cli/domain/aggregate/CLI.ts b/src/cli/domain/aggregate/CLI.ts index bb66e49..25f9efa 100644 --- a/src/cli/domain/aggregate/CLI.ts +++ b/src/cli/domain/aggregate/CLI.ts @@ -1,13 +1,15 @@ import type { File } from "../../../common/domain/valueobject/File.ts"; import type { Action } from "../valueobject/Action.ts"; -import { BlueskyCredentials } from "../valueobject/BlueskyCredentials.ts"; +import { BlueskyPublisherAction } from "../valueobject/BlueskyPublisherAction.ts"; +import { PreScannerAction } from "../valueobject/PreScannerAction.ts"; +import { ScannerAction } from "../valueobject/ScannerAction.ts"; export class CLI { constructor( public readonly configuration: File, public readonly action: Action, public readonly databaseFilepath = "./db.autophoto.sqlite3", - public readonly debugDatabase = false, + public readonly debug = false, ) {} static builder() { @@ -15,17 +17,11 @@ export class CLI { } } -class ScannerAction implements Action { - isScan(): boolean { - return true; - } -} - export class CLIBuilder { private configuration: File | undefined; private action: Action | undefined; private databaseFilepath: string | undefined; - private debugDatabase = false; + private debug = false; withConfiguration(configuration: File) { this.configuration = configuration; @@ -43,12 +39,17 @@ export class CLIBuilder { } withBluesky(host: URL, login: string, password: string) { - this.action = new BlueskyCredentials(host, login, password); + this.action = new BlueskyPublisherAction(host, login, password); + return this; + } + + withDebug() { + this.debug = true; return this; } - withDebugDatabase() { - this.debugDatabase = true; + withPreScanner(configuration: File) { + this.action = new PreScannerAction(configuration); return this; } @@ -58,14 +59,14 @@ export class CLIBuilder { } if (!this.action) { - throw new Error("Action is required: scanner or bluesky publisher"); + throw new Error("Action is required: prescanner, publisher or scanner"); } return new CLI( this.configuration, this.action, this.databaseFilepath, - this.debugDatabase, + this.debug, ); } } diff --git a/src/cli/domain/valueobject/Action.ts b/src/cli/domain/valueobject/Action.ts index ceadc44..b05001a 100644 --- a/src/cli/domain/valueobject/Action.ts +++ b/src/cli/domain/valueobject/Action.ts @@ -1,3 +1,9 @@ export interface Action { - isScan(): boolean; + type(): ActionType; +} + +export enum ActionType { + PRESCANNER = "PRESCANNER", + PUBLISHER = "PUBLISHER", + SCANNER = "SCANNER", } diff --git a/src/cli/domain/valueobject/BlueskyCredentials.ts b/src/cli/domain/valueobject/BlueskyPublisherAction.ts similarity index 77% rename from src/cli/domain/valueobject/BlueskyCredentials.ts rename to src/cli/domain/valueobject/BlueskyPublisherAction.ts index b38cf2a..04fcc28 100644 --- a/src/cli/domain/valueobject/BlueskyCredentials.ts +++ b/src/cli/domain/valueobject/BlueskyPublisherAction.ts @@ -1,8 +1,8 @@ import { DomainError } from "../../../common/domain/DomainError.ts"; import type { ValueObject } from "../../../common/domain/ValueObject.ts"; -import type { Action } from "./Action.ts"; +import { type Action, ActionType } from "./Action.ts"; -export class BlueskyCredentials implements ValueObject, Action { +export class BlueskyPublisherAction implements ValueObject, Action { constructor( public readonly host: URL, public readonly login: string, @@ -22,7 +22,7 @@ export class BlueskyCredentials implements ValueObject, Action { } equals(other: unknown): boolean { - if (other instanceof BlueskyCredentials) { + if (other instanceof BlueskyPublisherAction) { return ( this.login === other.login && this.password === other.password && @@ -32,7 +32,7 @@ export class BlueskyCredentials implements ValueObject, Action { return false; } - isScan(): boolean { - return false; + type(): ActionType { + return ActionType.PUBLISHER; } } diff --git a/src/cli/domain/valueobject/PreScannerAction.ts b/src/cli/domain/valueobject/PreScannerAction.ts new file mode 100644 index 0000000..58b5586 --- /dev/null +++ b/src/cli/domain/valueobject/PreScannerAction.ts @@ -0,0 +1,10 @@ +import type { File } from "../../../common/domain/valueobject/File.ts"; +import { type Action, ActionType } from "./Action.ts"; + +export class PreScannerAction implements Action { + constructor(public readonly configuration: File) {} + + type(): ActionType { + return ActionType.PRESCANNER; + } +} diff --git a/src/cli/domain/valueobject/ScannerAction.ts b/src/cli/domain/valueobject/ScannerAction.ts new file mode 100644 index 0000000..7853199 --- /dev/null +++ b/src/cli/domain/valueobject/ScannerAction.ts @@ -0,0 +1,7 @@ +import { type Action, ActionType } from "./Action.ts"; + +export class ScannerAction implements Action { + type(): ActionType { + return ActionType.SCANNER; + } +} diff --git a/src/cli/service/CLIService.ts b/src/cli/service/CLIService.ts index a93220a..f5fc565 100644 --- a/src/cli/service/CLIService.ts +++ b/src/cli/service/CLIService.ts @@ -7,8 +7,14 @@ import { CLI, type CLIBuilder } from "../domain/aggregate/CLI.ts"; export class CLIService { read(cliArgs: string[]): CLI { const args: Args = parseArgs(cliArgs, { - boolean: ["debug-database", "publish", "scan"], - string: ["bluesky_host", "bluesky_login", "bluesky_passord", "database"], + boolean: ["debug", "publish", "scan"], + string: [ + "bluesky_host", + "bluesky_login", + "bluesky_passord", + "database", + "prescan", + ], }); const cliParameters: (string | number)[] = args._; @@ -41,8 +47,8 @@ export class CLIService { .withConfiguration(new File(new Path(filepath))) .withDatabaseFilepath(databaseFilepath); - if (args["debug-database"] === true) { - cliBuilder.withDebugDatabase(); + if (args.debug === true) { + cliBuilder.withDebug(); } if (args.publish === true) { @@ -55,8 +61,10 @@ export class CLIService { ); } else if (args.scan === true) { cliBuilder.withScanner(); + } else if (args.prescan) { + cliBuilder.withPreScanner(new File(new Path(args.prescan))); } else { - throw new Error('Missing option: "--scan" or "--publish"'); + throw new Error('Missing option: "--prescan" or "--publish" or "--scan"'); } return cliBuilder.build(); diff --git a/src/common/domain/valueobject/Directory.ts b/src/common/domain/valueobject/Directory.ts index e76c262..f636d8b 100644 --- a/src/common/domain/valueobject/Directory.ts +++ b/src/common/domain/valueobject/Directory.ts @@ -1,6 +1,7 @@ import { DomainError } from "../../../common/domain/DomainError.ts"; import type { ValueObject } from "../../../common/domain/ValueObject.ts"; import { isDirectory } from "../../../utils/file.ts"; +import { scanDirectory } from "../../../utils/scan-directory.ts"; import { File } from "./File.ts"; import { Path } from "./Path.ts"; @@ -22,43 +23,11 @@ export class Directory implements ValueObject { return false; } - public async scanDirectories(pattern: RegExp): Promise { + public scanDirectories(pattern: RegExp): File[] { const files: File[] = []; - await Directory.scanDirectory(this.path.value, pattern, (file: File) => - files.push(file), + scanDirectory(this.path.value, pattern, (file: string) => + files.push(new File(new Path(file))), ); return files; } - - private static async scanDirectory( - directory: string, - pattern: RegExp, - onFileAdded: (file: File) => void, - ): Promise { - for await (const dirEntry of Deno.readDir(directory)) { - if (dirEntry.isDirectory) { - if (dirEntry.name === "@eaDir") { - continue; - } - - await Directory.scanDirectory( - `${directory}/${dirEntry.name}`, - pattern, - onFileAdded, - ); - } else if (dirEntry.isFile) { - if (dirEntry.name === ".DS_Store") { - continue; - } - - const fullPath = `${directory}/${dirEntry.name}`; - if (!pattern.test(fullPath)) { - continue; - } - - const file = new File(new Path(fullPath)); - onFileAdded(file); - } - } - } } diff --git a/src/main-cli.ts b/src/main-cli.ts new file mode 100644 index 0000000..e13873b --- /dev/null +++ b/src/main-cli.ts @@ -0,0 +1,3 @@ +import { main } from "./main"; + +Deno.exit((await main(Deno.args)) ? 0 : 1); diff --git a/src/main.ts b/src/main.ts index 2c450f5..b22583c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,37 +1,47 @@ import type { CLI } from "./cli/domain/aggregate/CLI.ts"; -import type { BlueskyCredentials } from "./cli/domain/valueobject/BlueskyCredentials.ts"; +import { ActionType } from "./cli/domain/valueobject/Action.ts"; +import type { BlueskyPublisherAction } from "./cli/domain/valueobject/BlueskyPublisherAction.ts"; +import type { PreScannerAction } from "./cli/domain/valueobject/PreScannerAction.ts"; import { CLIService } from "./cli/service/CLIService.ts"; import { KvDriver } from "./common/dbdriver/KvDriver.ts"; import type { Configuration } from "./configuration/domain/aggregate/Configuration.ts"; import { ConfigurationService } from "./configuration/service/ConfigurationService.ts"; +import { preScan } from "./prescan.ts"; import { publish } from "./publish.ts"; import { runScanner } from "./scan.ts"; -await main(Deno.args); - -export async function main(cliArgs: string[]): Promise { +export async function main(cliArgs: string[]): Promise { console.log("Starting Autophoto..."); const cli: CLI = new CLIService().read(cliArgs); const kvDriver = new KvDriver(cli.databaseFilepath); try { - const configuration: Configuration = new ConfigurationService().loadFile( - cli.configuration.path.value, - ); - - if (cli.action.isScan()) { + if (cli.action.type() === ActionType.SCANNER) { console.log("Scanning..."); - await runScanner(configuration, kvDriver, cli.debugDatabase); - } else { + const configuration: Configuration = new ConfigurationService().loadFile( + cli.configuration.path.value, + ); + await runScanner(configuration, kvDriver, cli.debug); + return true; + } + + if (cli.action.type() === ActionType.PUBLISHER) { console.log("Publishing..."); const result: string | undefined = await publish( - cli.action as BlueskyCredentials, + cli.action as BlueskyPublisherAction, kvDriver, - cli.debugDatabase, + cli.debug, ); console.log("Publication result:", result ?? "Nothing to publish."); + return true; } + + console.log("Pre-scanning..."); + const configuration: Configuration = new ConfigurationService().loadFile( + (cli.action as PreScannerAction).configuration.path.value, + ); + return preScan(configuration); } finally { kvDriver.close(); console.log("Autophoto finished."); diff --git a/src/prescan.ts b/src/prescan.ts new file mode 100644 index 0000000..25be709 --- /dev/null +++ b/src/prescan.ts @@ -0,0 +1,43 @@ +import type { Configuration } from "./configuration/domain/aggregate/Configuration.ts"; +import { VideoGamePlatform } from "./scanner/domain/valueobject/VideoGamePlatform.ts"; +import { scanDirectory } from "./utils/scan-directory.ts"; + +export const preScan = (configuration: Configuration): boolean => { + let filesCount = 0; + let errorsCount = 0; + + for (const scan of configuration.scans) { + const directory: string = scan.directory.path.value; + console.log(`Pre-scanning ${directory}...`); + + const platIndex: number = scan.pattern.groups.indexOf("platform"); + if (platIndex === -1) { + console.error( + ` - The pattern "${scan.pattern.regex.source}" does not have a "platform" group.`, + ); + errorsCount++; + continue; + } + + scanDirectory(directory, scan.pattern.regex, (filepath) => { + const regexResult: RegExpExecArray = scan.pattern.regex.exec( + filepath, + ) as RegExpExecArray; + + const group3: string = regexResult[3]; + try { + new VideoGamePlatform(group3); + filesCount++; + } catch (_) { + console.error(` - "${filepath}" has an invalid platform: ${group3}`); + errorsCount++; + } + }); + } + + console.log("Pre-scan completed!"); + console.log(`Found ${filesCount} files.`); + console.log(`Had ${errorsCount} errors.`); + + return errorsCount > 0; +}; diff --git a/src/publish.ts b/src/publish.ts index 33ddbaf..4cae84d 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -1,5 +1,5 @@ import { AtpAgent } from "@atproto/api"; -import type { BlueskyCredentials } from "./cli/domain/valueobject/BlueskyCredentials.ts"; +import type { BlueskyPublisherAction } from "./cli/domain/valueobject/BlueskyPublisherAction.ts"; import type { KvDriver } from "./common/dbdriver/KvDriver.ts"; import { File } from "./common/domain/valueobject/File.ts"; import { Path } from "./common/domain/valueobject/Path.ts"; @@ -19,7 +19,7 @@ import { BlueskyPublisherService } from "./publisher/service/BlueskyPublisherSer import { pluralFinalS } from "./utils/plural-final-s.ts"; export const publish = async ( - blueskyCredentials: BlueskyCredentials, + blueskyAction: BlueskyPublisherAction, kvDriver: KvDriver, debugDatabase: boolean, ): Promise => { @@ -41,9 +41,9 @@ export const publish = async ( const resultPublication: string = await new BlueskyPublisherService().publish( new BlueskyPublication( new AtpAgent({ - service: blueskyCredentials.host.toString(), + service: blueskyAction.host.toString(), }), - new Credentials(blueskyCredentials.login, blueskyCredentials.password), + new Credentials(blueskyAction.login, blueskyAction.password), new Publication( `${pluralFinalS(pickedVideoGameScreeshots.screenshots.length, "Screenshot", false)} from video game "${pickedVideoGameScreeshots.title}" (${pickedVideoGameScreeshots.releaseYear}) taken on ${pickedVideoGameScreeshots.platform}`, pickedVideoGameScreeshots.screenshots.map( diff --git a/src/scan.ts b/src/scan.ts index 16287f6..5dd6c9f 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -4,7 +4,6 @@ import type { Configuration } from "./configuration/domain/aggregate/Configurati import type { ConfigurationScanWithPattern } from "./configuration/domain/valueobject/ConfigurationScanWithPattern.ts"; import { ImageDirectory } from "./scanner/domain/aggregate/ImageDirectory.ts"; import type { VideoGame } from "./scanner/domain/entity/VideoGame.ts"; -import { VideoGamePlatform } from "./scanner/domain/valueobject/VideoGamePlatform.ts"; import { KvImageRepository } from "./scanner/repository/ImageRepository.ts"; import { KvRelationRepository, @@ -22,12 +21,6 @@ export const runScanner = async ( kvDriver: KvDriver, debugDatabase: boolean, ): Promise => { - const hasError: boolean = await preScan(configuration); - if (hasError) { - console.error("An error occurred while pre-scanning."); - return; - } - const videoGameRepository = new KvVideoGameRepository(kvDriver); const relationRepository = new KvRelationRepository(kvDriver); @@ -92,7 +85,6 @@ export async function scan( let hasError = false; try { - console.log("Scanning..."); // TODO Alerting for (const scan of scanData) { console.log(`Scanning ${scan.directory.path.value}...`); @@ -115,56 +107,3 @@ export async function scan( throw new Error("An error occurred while scanning."); } } - -async function preScan(configuration: Configuration): Promise { - console.log("Pre-scan..."); - - let filesCount = 0; - let warningCount = 0; - let errorsCount = 0; - - for (const scan of configuration.scans) { - const directory: string = scan.directory.path.value; - console.log(`Pre-scanning ${directory}...`); - - await scanDirectory(directory, scan.pattern.regex, (filepath) => { - const regexResult: RegExpExecArray | null = - scan.pattern.regex.exec(filepath); - - if (regexResult) { - const group3: string = regexResult[3]; - try { - new VideoGamePlatform(group3); - filesCount++; - } catch (_) { - console.error(` - "${filepath}" has an invalid platform: ${group3}`); - errorsCount++; - } - } else { - warningCount++; - console.warn(` - "${filepath}" does not match the pattern.`); - } - }); - } - - console.log("Pre-scan completed!"); - console.log(`Found ${filesCount} files.`); - console.log(`Had ${errorsCount} errors and ${warningCount} warnings.`); - - return errorsCount > 0; -} - -async function scanDirectory( - directory: string, - pattern: RegExp, - onFile: (filepath: string) => void, -): Promise { - for await (const dirEntry of Deno.readDir(directory)) { - if (dirEntry.isDirectory && dirEntry.name !== "@eaDir") { - await scanDirectory(`${directory}/${dirEntry.name}`, pattern, onFile); - } else if (dirEntry.isFile && dirEntry.name !== ".DS_Store") { - const fullPath = `${directory}/${dirEntry.name}`; - onFile(fullPath); - } - } -} diff --git a/src/utils/scan-directory.ts b/src/utils/scan-directory.ts new file mode 100644 index 0000000..6792fce --- /dev/null +++ b/src/utils/scan-directory.ts @@ -0,0 +1,16 @@ +export function scanDirectory( + directory: string, + pattern: RegExp, + onFile: (filepath: string) => void, +): void { + for (const dirEntry of Deno.readDirSync(directory)) { + if (dirEntry.isDirectory && dirEntry.name !== "@eaDir") { + scanDirectory(`${directory}/${dirEntry.name}`, pattern, onFile); + } else if (dirEntry.isFile && dirEntry.name !== ".DS_Store") { + const fullPath = `${directory}/${dirEntry.name}`; + if (pattern.test(fullPath)) { + onFile(fullPath); + } + } + } +} diff --git a/test/cli/domain/aggregate/CLI.test.ts b/test/cli/domain/aggregate/CLI.test.ts index e3404ed..6c30751 100644 --- a/test/cli/domain/aggregate/CLI.test.ts +++ b/test/cli/domain/aggregate/CLI.test.ts @@ -18,6 +18,6 @@ Deno.test(function buildNoAction() { assert(error instanceof Error); assertEquals( error.message, - "Action is required: scanner or bluesky publisher", + "Action is required: prescanner, publisher or scanner", ); }); diff --git a/test/cli/domain/valueobject/BlueskyCredentials.test.ts b/test/cli/domain/valueobject/BlueskyPublisherAction.test.ts similarity index 68% rename from test/cli/domain/valueobject/BlueskyCredentials.test.ts rename to test/cli/domain/valueobject/BlueskyPublisherAction.test.ts index 6db04bc..e0d0a78 100644 --- a/test/cli/domain/valueobject/BlueskyCredentials.test.ts +++ b/test/cli/domain/valueobject/BlueskyPublisherAction.test.ts @@ -1,14 +1,14 @@ import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert"; -import { BlueskyCredentials } from "../../../../src/cli/domain/valueobject/BlueskyCredentials.ts"; +import { BlueskyPublisherAction } from "../../../../src/cli/domain/valueobject/BlueskyPublisherAction.ts"; import { DomainError } from "../../../../src/common/domain/DomainError.ts"; Deno.test(function equals() { - const obj1 = new BlueskyCredentials( + const obj1 = new BlueskyPublisherAction( new URL("http://zhykos.fr"), "login", "password", ); - const obj2 = new BlueskyCredentials( + const obj2 = new BlueskyPublisherAction( new URL("http://zhykos.fr"), "login", "password", @@ -17,7 +17,7 @@ Deno.test(function equals() { }); Deno.test(function notEquals() { - const obj1 = new BlueskyCredentials( + const obj1 = new BlueskyPublisherAction( new URL("http://zhykos.fr"), "login", "password", @@ -28,7 +28,8 @@ Deno.test(function notEquals() { Deno.test(function noLogin() { const error = assertThrows( - () => new BlueskyCredentials(new URL("http://zhykos.fr"), "", "password"), + () => + new BlueskyPublisherAction(new URL("http://zhykos.fr"), "", "password"), ); assert(error instanceof DomainError); assertEquals(error.message, "Login is required"); @@ -36,7 +37,7 @@ Deno.test(function noLogin() { Deno.test(function noPassword() { const error = assertThrows( - () => new BlueskyCredentials(new URL("http://zhykos.fr"), "login", ""), + () => new BlueskyPublisherAction(new URL("http://zhykos.fr"), "login", ""), ); assert(error instanceof DomainError); assertEquals(error.message, "Password is required"); diff --git a/test/cli/service/CLIService.test.ts b/test/cli/service/CLIService.test.ts index e21dcb6..f96f134 100644 --- a/test/cli/service/CLIService.test.ts +++ b/test/cli/service/CLIService.test.ts @@ -1,5 +1,7 @@ import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert"; import type { CLI } from "../../../src/cli/domain/aggregate/CLI.ts"; +import { BlueskyPublisherAction } from "../../../src/cli/domain/valueobject/BlueskyPublisherAction.ts"; +import { ScannerAction } from "../../../src/cli/domain/valueobject/ScannerAction.ts"; import { CLIService } from "../../../src/cli/service/CLIService.ts"; Deno.test(function noArgs() { @@ -37,14 +39,17 @@ Deno.test(function argMustBeFile() { Deno.test(function argMissingAction() { const error = assertThrows(() => new CLIService().read(["README.md"])); assert(error instanceof Error); - assertEquals(error.message, 'Missing option: "--scan" or "--publish"'); + assertEquals( + error.message, + 'Missing option: "--prescan" or "--publish" or "--scan"', + ); }); Deno.test(function readScanOK() { const cliResult: CLI = new CLIService().read(["--scan", "README.md"]); assertEquals(cliResult.configuration.path.value, "README.md"); - assert(cliResult.action.isScan()); - assertFalse(cliResult.debugDatabase); + assert(cliResult.action instanceof ScannerAction); + assertFalse(cliResult.debug); }); Deno.test(function readPublishOK() { @@ -55,7 +60,7 @@ Deno.test(function readPublishOK() { "README.md", ]); assertEquals(cliResult.configuration.path.value, "README.md"); - assertFalse(cliResult.action.isScan()); + assert(cliResult.action instanceof BlueskyPublisherAction); }); Deno.test(function newDatabaseFile() { @@ -70,9 +75,9 @@ Deno.test(function newDatabaseFile() { Deno.test(function debug() { const cliResult: CLI = new CLIService().read([ - "--debug-database", + "--debug", "--scan", "README.md", ]); - assert(cliResult.debugDatabase); + assert(cliResult.debug); }); diff --git a/test/main.test.ts b/test/main.test.ts index 9949574..fcc62f4 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -19,7 +19,7 @@ import { getAllVideoGamesFromRepository } from "./test-utils/getAllVideoGamesFro const tempDatabaseFilePath = "./test/it-database.sqlite3"; -describe("main publish", () => { +describe("main", () => { let mockedBlueskyServer: MockBlueskyServer; beforeAll(() => { diff --git a/test/publish.test.ts b/test/publish.test.ts index a8cb001..c814d93 100644 --- a/test/publish.test.ts +++ b/test/publish.test.ts @@ -6,7 +6,7 @@ import { describe, it, } from "@std/testing/bdd"; -import { BlueskyCredentials } from "../src/cli/domain/valueobject/BlueskyCredentials.ts"; +import { BlueskyPublisherAction } from "../src/cli/domain/valueobject/BlueskyPublisherAction.ts"; import { KvDriver } from "../src/common/dbdriver/KvDriver.ts"; import type { VideoGameRelationImageRepositoryEntity } from "../src/common/repository/entity/VideoGameRelationImageRepositoryEntity.ts"; import { main } from "../src/main.ts"; @@ -64,7 +64,7 @@ describe("main publish", () => { try { await publish( - new BlueskyCredentials( + new BlueskyPublisherAction( new URL(mockedBlueskyServer.host), "login", "password",