commit
0f3f0134b8
@ -0,0 +1,2 @@ |
||||
/node_modules/ |
||||
/build/ |
@ -0,0 +1,8 @@ |
||||
# Default ignored files |
||||
/shelf/ |
||||
/workspace.xml |
||||
# Datasource local storage ignored files |
||||
/dataSources/ |
||||
/dataSources.local.xml |
||||
# Editor-based HTTP Client requests |
||||
/httpRequests/ |
@ -0,0 +1,5 @@ |
||||
<component name="ProjectCodeStyleConfiguration"> |
||||
<state> |
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Prisma" /> |
||||
</state> |
||||
</component> |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="JavaScriptLibraryMappings"> |
||||
<includedPredefinedLibrary name="Node.js Core" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="ProjectModuleManager"> |
||||
<modules> |
||||
<module fileurl="file://$PROJECT_DIR$/.idea/smva-indexer.iml" filepath="$PROJECT_DIR$/.idea/smva-indexer.iml" /> |
||||
</modules> |
||||
</component> |
||||
</project> |
@ -0,0 +1,5 @@ |
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="indexer" type="NodeJSConfigurationType" path-to-js-file="$PROJECT_DIR$/indexer.mjs" working-dir="$PROJECT_DIR$"> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,12 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<module type="WEB_MODULE" version="4"> |
||||
<component name="NewModuleRootManager"> |
||||
<content url="file://$MODULE_DIR$"> |
||||
<excludeFolder url="file://$MODULE_DIR$/temp" /> |
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" /> |
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" /> |
||||
</content> |
||||
<orderEntry type="inheritedJdk" /> |
||||
<orderEntry type="sourceFolder" forTests="false" /> |
||||
</component> |
||||
</module> |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="VcsDirectoryMappings"> |
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,7 @@ |
||||
Copyright 2022 Mari Lyndon |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,309 @@ |
||||
import enquirer from "enquirer" |
||||
import {watch} from "fs" |
||||
import {access, readFile, rename, stat, writeFile} from "fs/promises" |
||||
import glob from "glob" |
||||
import ini from "ini" |
||||
import envPaths from "env-paths" |
||||
import chalk from "chalk" |
||||
import {isAbsolute, join, normalize, basename} from "path" |
||||
import makeDir from "make-dir" |
||||
import homeDir from "home-dir" |
||||
|
||||
const {Toggle, Select, Input} = enquirer |
||||
|
||||
function log(message) { |
||||
process.stderr.write(message + "\n") |
||||
} |
||||
|
||||
async function getDirectoryFromUser(message, initial) { |
||||
while (true) { |
||||
const newDir = normalize(await new Input({ |
||||
message, initial |
||||
}).run()) |
||||
if (!isAbsolute(newDir)) { |
||||
log(chalk.red("Can't use that folder: This path must be absolute. Try again.")) |
||||
continue |
||||
} |
||||
try { |
||||
const statResult = await stat(newDir) |
||||
if (!statResult.isDirectory()) { |
||||
log(chalk.red("Can't use that folder: This path must be a directory. Try again.")) |
||||
} |
||||
return newDir |
||||
} catch (ex) { |
||||
if (ex.code === "ENOENT") { |
||||
const create = await new Toggle({ |
||||
message: "That folder doesn't exist. Want me to create it?", |
||||
enabled: "Create it", |
||||
disabled: "No, I'll enter the path again", |
||||
}).run() |
||||
if (!create) { |
||||
continue |
||||
} |
||||
try { |
||||
await makeDir(newDir) |
||||
return newDir |
||||
} catch (ex) { |
||||
log(chalk.red(`Couldn't create that folder: ${chalk.redBright.bold(ex)}. Try again.`)) |
||||
} |
||||
} else { |
||||
log(chalk.red(`Couldn't check on that folder: ${chalk.redBright.bold(ex)}. Try again.`)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
async function loadPaths(config, home) { |
||||
let sourceDir = process.env.SMVA_SOURCE_DIR || null |
||||
let destDir = process.env.SMVA_DEST_DIR || null |
||||
const configPath = join(config, "smva-indexer.ini") |
||||
if (!(sourceDir && destDir)) { |
||||
try { |
||||
const configText = await readFile(configPath, {encoding: "utf-8"}) |
||||
log(chalk.cyan(`Read configuration from ${chalk.cyanBright.bold(configPath)}`)) |
||||
const configIni = ini.parse(configText) |
||||
if (!sourceDir && configIni.paths && configIni.paths.source) { |
||||
sourceDir = configIni.paths.source |
||||
} |
||||
if (!destDir && configIni.paths && configIni.paths.dest) { |
||||
destDir = configIni.paths.dest |
||||
} |
||||
} catch (ex) { |
||||
if (ex.code !== "ENOENT") { |
||||
log(chalk.red(`Failed to read configuration from ${chalk.redBright.bold(configPath)}: ${chalk.redBright.bold(ex)}`)) |
||||
} |
||||
} |
||||
} |
||||
if (!(sourceDir && destDir)) { |
||||
log(chalk.yellow(`Configuration at ${chalk.yellowBright.bold(configPath)} is missing or incomplete. Configure now.`)) |
||||
sourceDir = await getDirectoryFromUser( |
||||
"Where are SMVA saves downloaded to? (e.g., your Downloads folder)", |
||||
sourceDir || join(home, "Downloads")) |
||||
destDir = await getDirectoryFromUser( |
||||
"Where do you want to store your organized SMVA saves? (e.g., your Documents folder)", |
||||
destDir || join(home, "Documents", "SMVA Saves") |
||||
) |
||||
await makeDir(config) |
||||
await writeFile(configPath, ini.encode({paths: {source: sourceDir, dest: destDir}}), {encoding: "utf-8"}) |
||||
log(chalk.cyan(`Wrote configuration to ${chalk.cyanBright.bold(configPath)}`)) |
||||
} |
||||
return {sourceDir, destDir} |
||||
} |
||||
|
||||
async function* watchNewSaves(sourceDir) { |
||||
const sourceRe = /^saint-miluinas-vore-academy-.*\.save$/ |
||||
let resolveNext |
||||
let nextPromise |
||||
|
||||
function makeNewPromise() { |
||||
nextPromise = new Promise((resolve) => { |
||||
resolveNext = (...args) => { |
||||
makeNewPromise() |
||||
resolve(...args) |
||||
} |
||||
}) |
||||
} |
||||
makeNewPromise() |
||||
|
||||
watch(sourceDir, {}, (eventType, filename) => { |
||||
if (eventType === "rename" && sourceRe.test(filename)) { |
||||
resolveNext(join(sourceDir, filename)) |
||||
} |
||||
}) |
||||
|
||||
log(chalk.cyan(`Waiting for new saves to appear in ${chalk.cyanBright.bold(sourceDir)}`)) |
||||
|
||||
while (true) { |
||||
yield await nextPromise |
||||
} |
||||
} |
||||
|
||||
async function findExistingSaves(sourceDir) { |
||||
const sourceGlob = `${sourceDir}/saint-miluinas-vore-academy-*.save` |
||||
return new Promise((resolve, reject) => { |
||||
glob(sourceGlob, {"nodir": true}, (err, results) => { |
||||
if (err !== null) { |
||||
reject(err) |
||||
} else if (results.length > 0) { |
||||
resolve(results) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
async function promptForFilename() { |
||||
const saveType = await new Select({ |
||||
message: "What type of save is this?", |
||||
choices: ["Useful Save", "Vore Scene", "Skip This Save"], |
||||
}).run() |
||||
const dayPrompt = new Select({ |
||||
message: "When is this save from?", |
||||
choices: ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6"] |
||||
}) |
||||
if (saveType === "Vore Scene") { |
||||
const voreType = await new Select({ |
||||
message: "What type of vore is involved in this save?", |
||||
choices: ["Oral", "Anal", "Unbirth"] |
||||
}).run() |
||||
const voreRole = await new Select({ |
||||
message: "What role does the player play in this scene?", |
||||
choices: ["Predator", "Prey", "Observer"] |
||||
}).run() |
||||
const voreResult = await new Select({ |
||||
message: "What is the result of the vore?", |
||||
choices: ["Fatal", "Always Fatal", "Non-fatal"], |
||||
}).run() |
||||
const day = await dayPrompt.run() |
||||
const vorePartner = await new Input({ |
||||
message: "Who is/are the partner(s) the player is with in this scene?", |
||||
}).run() |
||||
const voreVariant = await new Input({ |
||||
message: "What is the variant name/comment for this scene? (leave blank to skip)", |
||||
}).run() |
||||
return `${voreType} ${voreRole} - ${voreResult} - ${day} ${vorePartner}${voreVariant ? ` (${voreVariant})` : ""}.save` |
||||
} else if (saveType === "Useful Save") { |
||||
const day = await dayPrompt.run() |
||||
const title = await new Input({ |
||||
message: "What is the title of this useful save?" |
||||
}).run() |
||||
return `Useful Saves - ${day} - ${title}.save` |
||||
} else { |
||||
return null |
||||
} |
||||
} |
||||
|
||||
async function shouldDoRename(destPath) { |
||||
try { |
||||
await access(destPath) |
||||
console.log(chalk.yellow(`A file named ${chalk.yellowBright.bold(destPath)} already exists.`)) |
||||
} catch (ex) { |
||||
if (ex.code === "ENOENT") { |
||||
return true |
||||
} else { |
||||
console.log(chalk.red(`The file named ${chalk.redBright.bold(destPath)} could not be accessed: ${chalk.redBright.bold(ex)}`)) |
||||
} |
||||
} |
||||
const result = await new Select({message: "What should we do?", choices: ["Skip it", "Rename it", "Overwrite it"]}).run(); |
||||
switch (result) { |
||||
case "Skip it": |
||||
return false; |
||||
case "Overwrite it": |
||||
return true; |
||||
case "Rename it": |
||||
return (await new Input({message: "What would you like to name the file?", initial: basename(destPath, ".save")}).run()) + ".save" |
||||
} |
||||
} |
||||
|
||||
function queueMovingTo(destDir) { |
||||
let resolvePromise, rejectPromise |
||||
const completionPromise = new Promise((resolve, reject) => { |
||||
resolvePromise = resolve |
||||
rejectPromise = reject |
||||
}) |
||||
const filequeue = [] |
||||
let processingFiles = false |
||||
|
||||
async function processFiles() { |
||||
if (processingFiles) { |
||||
return |
||||
} |
||||
processingFiles = true |
||||
while (filequeue.length > 0) { |
||||
const nextFile = filequeue[0] |
||||
try { |
||||
access(nextFile) |
||||
} catch (ex) { |
||||
if (ex.code !== "ENOENT") { |
||||
log(chalk.red(`failed accessing ${chalk.redBright.bold(nextFile)}: ${chalk.redBright.bold(ex)}`)) |
||||
} |
||||
filequeue.shift() |
||||
continue |
||||
} |
||||
log(chalk.green(`For save ${chalk.greenBright.bold(nextFile)}:`)) |
||||
const destFilename = await promptForFilename() |
||||
if (!destFilename) { |
||||
log(chalk.yellow("Skipped.")) |
||||
filequeue.shift() |
||||
continue |
||||
} |
||||
let destPath = join(destDir, destFilename) |
||||
let confirmed = false; |
||||
while (!confirmed) { |
||||
const promptResult = await shouldDoRename(destPath) |
||||
if (typeof promptResult === "string") { |
||||
destPath = join(destDir, promptResult) |
||||
} else if (!promptResult) { |
||||
destPath = null |
||||
confirmed = true |
||||
} else { |
||||
confirmed = true |
||||
} |
||||
} |
||||
if (!destPath) { |
||||
log(chalk.yellow("Skipped.")) |
||||
filequeue.shift() |
||||
continue |
||||
} |
||||
await rename(nextFile, destPath) |
||||
log(chalk.cyan(`Moved to ${chalk.cyanBright.bold(destPath)}`)) |
||||
filequeue.shift() |
||||
} |
||||
processingFiles = false |
||||
} |
||||
|
||||
return { |
||||
push(...files) { |
||||
if (!rejectPromise) { |
||||
throw Error("Already stopped the queue") |
||||
} |
||||
for (const file of files) { |
||||
if (!filequeue.includes(file)) { |
||||
filequeue.push(file) |
||||
} |
||||
} |
||||
processFiles().catch(() => { |
||||
if (!rejectPromise) { |
||||
return |
||||
} |
||||
rejectPromise() |
||||
resolvePromise = null |
||||
rejectPromise = null |
||||
}) |
||||
}, |
||||
complete() { |
||||
if (!resolvePromise) { |
||||
throw Error("Already stopped the queue") |
||||
} |
||||
resolvePromise() |
||||
resolvePromise = null |
||||
rejectPromise = null |
||||
}, |
||||
completionPromise |
||||
} |
||||
} |
||||
|
||||
async function main() { |
||||
const {config} = envPaths("SMVA-Indexer") |
||||
const home = homeDir() |
||||
const {sourceDir, destDir} = await loadPaths(config, home) |
||||
const queue = queueMovingTo(destDir) |
||||
|
||||
const existingSavesPromise = (async () => { |
||||
try { |
||||
const existingSaves = await findExistingSaves(sourceDir) |
||||
log(chalk.cyan(`found ${chalk.cyanBright.bold(existingSaves.length)} existing saves`)) |
||||
queue.push(...existingSaves) |
||||
} catch (ex) { |
||||
log(chalk.red(`failed searching for existing saves: ${chalk.redBright.bold(ex)}`)) |
||||
} |
||||
})() |
||||
|
||||
const newSavesPromise = (async () => { |
||||
for await (const newSave of watchNewSaves(sourceDir)) { |
||||
queue.push(newSave) |
||||
} |
||||
})() |
||||
await Promise.all([newSavesPromise, existingSavesPromise, queue.completionPromise]) |
||||
} |
||||
|
||||
main() |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,33 @@ |
||||
{ |
||||
"name": "smva-indexer", |
||||
"version": "1.0.0", |
||||
"description": "Simple SMVA save file templating system.", |
||||
"bin": "indexer.mjs", |
||||
"scripts": { |
||||
"start": "node indexer.mjs", |
||||
"build": "concurrently -n Win,Mac,Lin -c blue,white,yellow \"npm run build-win\" \"npm run build-mac\" \"npm run build-lin\"", |
||||
"build-win": "nexe -i indexer.mjs -t windows-x86-14.15.3 -o build/smva-indexer-win.exe", |
||||
"build-mac": "nexe -i indexer.mjs -t mac-x64-14.15.3 -o build/smva-indexer-macos", |
||||
"build-lin": "nexe -i indexer.mjs -t linux-x64-14.15.3 -o build/smva-indexer-linux" |
||||
}, |
||||
"keywords": [], |
||||
"author": { |
||||
"name": "Reya C.", |
||||
"email": "reya@deliciousreya.net" |
||||
}, |
||||
"license": "MIT", |
||||
"type": "module", |
||||
"dependencies": { |
||||
"chalk": "^5.0.0", |
||||
"enquirer": "^2.3.6", |
||||
"env-paths": "^3.0.0", |
||||
"glob": "^7.2.0", |
||||
"home-dir": "^1.0.0", |
||||
"ini": "^2.0.0", |
||||
"make-dir": "^3.1.0" |
||||
}, |
||||
"devDependencies": { |
||||
"concurrently": "^7.0.0", |
||||
"nexe": "^4.0.0-beta.19" |
||||
} |
||||
} |
Loading…
Reference in new issue