import '@johnlindquist/kit';
import fs from 'fs';
const greekToGreeklishMap = [
{ find: '́', replace: '' },
{ find: 'ΓΧ', replace: 'GX' },
{ find: 'γχ', replace: 'gx' },
{ find: 'ΤΘ', replace: 'T8' },
{ find: 'τθ', replace: 't8' },
{ find: '(θη|Θη)', replace: '8h' },
{ find: 'ΘΗ', replace: '8H' },
{ find: 'αυ', replace: 'au' },
{ find: 'Αυ', replace: 'Au' },
{ find: 'ΑΥ', replace: 'AY' },
{ find: 'ευ', replace: 'eu' },
{ find: 'εύ', replace: 'eu' },
{ find: 'εϋ', replace: 'ey' },
{ find: 'εΰ', replace: 'ey' },
{ find: 'Ευ', replace: 'Eu' },
{ find: 'Εύ', replace: 'Eu' },
{ find: 'Εϋ', replace: 'Ey' },
{ find: 'Εΰ', replace: 'Ey' },
{ find: 'ΕΥ', replace: 'EY' },
{ find: 'ου', replace: 'ou' },
{ find: 'ού', replace: 'ou' },
{ find: 'οϋ', replace: 'oy' },
{ find: 'οΰ', replace: 'oy' },
{ find: 'Ου', replace: 'Ou' },
{ find: 'Ού', replace: 'Ou' },
{ find: 'Οϋ', replace: 'Oy' },
{ find: 'Οΰ', replace: 'Oy' },
{ find: 'ΟΥ', replace: 'OY' },
{ find: 'Α', replace: 'A' },
{ find: 'α', replace: 'a' },
{ find: 'ά', replace: 'a' },
{ find: 'Ά', replace: 'A' },
{ find: 'Β', replace: 'B' },
{ find: 'β', replace: 'b' },
{ find: 'Γ', replace: 'G' },
{ find: 'γ', replace: 'g' },
{ find: 'Δ', replace: 'D' },
{ find: 'δ', replace: 'd' },
{ find: 'Ε', replace: 'E' },
{ find: 'ε', replace: 'e' },
{ find: 'έ', replace: 'e' },
{ find: 'Έ', replace: 'E' },
{ find: 'Ζ', replace: 'Z' },
{ find: 'ζ', replace: 'z' },
{ find: 'Η', replace: 'H' },
{ find: 'η', replace: 'h' },
{ find: 'ή', replace: 'h' },
{ find: 'Ή', replace: 'H' },
{ find: 'Θ', replace: 'TH' },
{ find: 'θ', replace: 'th' },
{ find: 'Ι', replace: 'I' },
{ find: 'Ϊ', replace: 'I' },
{ find: 'ι', replace: 'i' },
{ find: 'ί', replace: 'i' },
{ find: 'ΐ', replace: 'i' },
{ find: 'ϊ', replace: 'i' },
{ find: 'Ί', replace: 'I' },
{ find: 'Κ', replace: 'K' },
{ find: 'κ', replace: 'k' },
{ find: 'Λ', replace: 'L' },
{ find: 'λ', replace: 'l' },
{ find: 'Μ', replace: 'M' },
{ find: 'μ', replace: 'm' },
{ find: 'Ν', replace: 'N' },
{ find: 'ν', replace: 'n' },
{ find: 'Ξ', replace: 'KS' },
{ find: 'ξ', replace: 'ks' },
{ find: 'Ο', replace: 'O' },
{ find: 'ο', replace: 'o' },
{ find: 'Ό', replace: 'O' },
{ find: 'ό', replace: 'o' },
{ find: 'Π', replace: 'P' },
{ find: 'π', replace: 'p' },
{ find: 'Ρ', replace: 'R' },
{ find: 'ρ', replace: 'r' },
{ find: 'Σ', replace: 'S' },
{ find: 'σ', replace: 's' },
{ find: 'Τ', replace: 'T' },
{ find: 'τ', replace: 't' },
{ find: 'Υ', replace: 'Y' },
{ find: 'Ύ', replace: 'Y' },
{ find: 'Ϋ', replace: 'Y' },
{ find: 'ΰ', replace: 'y' },
{ find: 'ύ', replace: 'y' },
{ find: 'ϋ', replace: 'y' },
{ find: 'υ', replace: 'y' },
{ find: 'Φ', replace: 'F' },
{ find: 'φ', replace: 'f' },
{ find: 'Χ', replace: 'X' },
{ find: 'χ', replace: 'x' },
{ find: 'Ψ', replace: 'Ps' },
{ find: 'ψ', replace: 'ps' },
{ find: 'Ω', replace: 'w' },
{ find: 'ω', replace: 'w' },
{ find: 'Ώ', replace: 'w' },
{ find: 'ώ', replace: 'w' },
{ find: 'ς', replace: 's' },
{ find: ';', replace: '?' },
];
const extensions = {
'1.ada': ['code'],
'2.ada': ['code'],
'3dm': ['image'],
'3ds': ['image'],
'3g2': ['video'],
'3gp': ['video'],
'7z': ['archive'],
a: ['archive'],
aac: ['audio'],
aaf: ['video'],
ada: ['code'],
adb: ['code'],
ads: ['code'],
ai: ['image'],
aiff: ['audio'],
ape: ['audio'],
apk: ['archive'],
ar: ['archive'],
asf: ['video'],
asm: ['code'],
au: ['audio'],
avchd: ['video'],
avi: ['video'],
azw: ['book'],
azw1: ['book'],
azw3: ['book'],
azw4: ['book'],
azw6: ['book'],
bas: ['code'],
bash: ['code', 'exec'],
bat: ['code', 'exec'],
bin: ['exec'],
bmp: ['image'],
bz2: ['archive'],
c: ['code'],
'c++': ['code'],
cab: ['archive'],
cbl: ['code'],
cbr: ['book'],
cbz: ['book'],
cc: ['code'],
class: ['code'],
clj: ['code'],
cob: ['code'],
command: ['exec'],
cpio: ['archive'],
cpp: ['code'],
crx: ['exec'],
cs: ['code'],
csh: ['code', 'exec'],
css: ['web'],
csv: ['sheet'],
cxx: ['code'],
d: ['code'],
dds: ['image'],
deb: ['archive'],
diff: ['code'],
dmg: ['archive'],
doc: ['text'],
docx: ['text'],
drc: ['video'],
dwg: ['image'],
dxf: ['image'],
e: ['code'],
ebook: ['text'],
egg: ['archive'],
el: ['code'],
eot: ['font'],
eps: ['image'],
epub: ['book'],
exe: ['exec'],
f: ['code'],
f77: ['code'],
f90: ['code'],
fish: ['code', 'exec'],
flac: ['audio'],
flv: ['video'],
for: ['code'],
fth: ['code'],
ftn: ['code'],
gif: ['image'],
go: ['code'],
gpx: ['image'],
groovy: ['code'],
gsm: ['audio'],
gz: ['archive'],
h: ['code'],
hh: ['code'],
hpp: ['code'],
hs: ['code'],
htm: ['code', 'web'],
html: ['code', 'web'],
hxx: ['code'],
ics: ['sheet'],
iso: ['archive'],
it: ['audio'],
jar: ['archive'],
java: ['code'],
jpeg: ['image'],
jpg: ['image'],
js: ['code', 'web'],
jsp: ['code'],
jsx: ['code', 'web'],
kml: ['image'],
kmz: ['image'],
ksh: ['code', 'exec'],
kt: ['code'],
less: ['web'],
lha: ['archive'],
lhs: ['code'],
lisp: ['code'],
log: ['text'],
lua: ['code'],
m: ['code'],
m2v: ['video'],
m3u: ['audio'],
m4: ['code'],
m4a: ['audio'],
m4p: ['video'],
m4v: ['video'],
mar: ['archive'],
max: ['image'],
md: ['text'],
mid: ['audio'],
mkv: ['video'],
mng: ['video'],
mobi: ['book'],
mod: ['audio'],
mov: ['video'],
mp2: ['video'],
mp3: ['audio'],
mp4: ['video'],
mpa: ['audio'],
mpe: ['video'],
mpeg: ['video'],
mpg: ['video'],
mpv: ['video'],
msg: ['text'],
msi: ['exec'],
mxf: ['video'],
nim: ['code'],
nsv: ['video'],
odp: ['slide'],
ods: ['sheet'],
odt: ['text'],
ogg: ['video'],
ogm: ['video'],
ogv: ['video'],
org: ['text'],
otf: ['font'],
pages: ['text'],
pak: ['archive'],
patch: ['code'],
pdf: ['text'],
pea: ['archive'],
php: ['code', 'web'],
pl: ['code'],
pls: ['audio'],
png: ['image'],
po: ['code'],
pp: ['code'],
ppt: ['slide'],
ps: ['image'],
psd: ['image'],
py: ['code'],
qt: ['video'],
r: ['code'],
ra: ['audio'],
rar: ['archive'],
rb: ['code'],
rm: ['video'],
rmvb: ['video'],
roq: ['video'],
rpm: ['archive'],
rs: ['code'],
rst: ['text'],
rtf: ['text'],
s: ['code'],
s3m: ['audio'],
s7z: ['archive'],
scala: ['code'],
scss: ['web'],
sh: ['code', 'exec'],
shar: ['archive'],
sid: ['audio'],
srt: ['video'],
svg: ['image'],
svi: ['video'],
swg: ['code'],
swift: ['code'],
tar: ['archive'],
tbz2: ['archive'],
tex: ['text'],
tga: ['image'],
tgz: ['archive'],
thm: ['image'],
tif: ['image'],
tiff: ['image'],
tlz: ['archive'],
ttf: ['font'],
txt: ['text'],
v: ['code'],
vb: ['code'],
vcf: ['sheet'],
vcxproj: ['code'],
vob: ['video'],
war: ['archive'],
wasm: ['web'],
wav: ['audio'],
webm: ['video'],
webp: ['image'],
whl: ['archive'],
wma: ['audio'],
wmv: ['video'],
woff: ['font'],
woff2: ['font'],
wpd: ['text'],
wps: ['text'],
xcf: ['image'],
xcodeproj: ['code'],
xls: ['sheet'],
xlsx: ['sheet'],
xm: ['audio'],
xml: ['code'],
xpi: ['archive'],
xz: ['archive'],
yuv: ['image', 'video'],
zip: ['archive'],
zipx: ['archive'],
zsh: ['code', 'exec'],
};
const CHECKS = {
YES: 'yes',
NO: 'no',
};
const allExtensionsValues = Object.values(extensions).flatMap((type) => type);
const allExtensionEntries = Object.entries(extensions);
let limitErrorState = {
hasLimitError: false,
lastFilesCount: 0,
};
let fileNameConfigList = [];
let shouldContinueTranslation = CHECKS.YES;
let shouldContinueNextFolder = CHECKS.YES;
const { translationPath, fileType } = await initTranslationApp();
const fileTranslationLimit = await env('OVERRIDE_FILE_TRANSLATION_LIMIT');
const shouldLogVerboseLogs = await env('TRANSLATION_VERBOSE_LOGS');
const shouldReadFilesRecursively = await env('READ_FILES_RECURSIVELY_FOR_CHILDREN_DIRS');
const fileTranslationLimitNumber = +fileTranslationLimit || 2500;
const fileNames = await getFilesNames(
translationPath,
fileType,
fileTranslationLimitNumber,
shouldReadFilesRecursively === 'true'
);
const width = 700;
const height = 600;
const initState = {
errorMessage: null,
successMessage: null,
loading: false,
hasFiles: !!fileNames.length,
limitError: limitErrorState.hasLimitError,
};
const widgetConfig = {
alwaysOnTop: true,
title: 'Music files translation widget',
width,
height,
center: true,
state: initState,
backgroundColor: '#0284C7',
};
const translationWidget = await widget(
`
<div v-if="!limitError" class="flex flex-col">
<div v-if="!hasFiles">
<h1>No files for translation found in the selected directory. 🙁</h1>
</div>
<div v-if="hasFiles" class="p-2">
<h3 class="font-bold text-sky-900 text-center">Base folder: ${translationPath}</h3>
<div v-if="successMessage">
<span class="text-emerald-500 font-bold text-lg">{{successMessage}}</span>
<button id="open-translated-dir" class="button bg-amber-100 text-amber-700 p-2 m-4 rounded">Open directory</button>
</div>
<div v-else>
<h1 class="text-sky-900 text-center">⚠️ ${fileNames.length} files will be renamed and translated to greeklish!</h1>
</div>
<div v-if="loading" class="loader">
Loading ...
</div>
<div v-if="errorMessage" class="text-rose-600 font-bold text-lg">
{{errorMessage}}
</div>
</div>
<hr/>
<div v-if="!successMessage && hasFiles" class="overflow-scroll max-h-80">
<h3 class="text-sky-900">Files preview list</h3>
<ul role="list">
${fileNames
.map(
(f) => `<li class="list-none drop-shadow-md mb-5 border-b-2 border-neutral-900 border-solid">
<div class="text-amber-100 italic">${f.oldName}</div>
<strong class="text-amber-800">⚠️ will be renamed to ⚠️</strong>
<div class="text-lime-200">${f.newName}</div>
</li>`
)
.join(' ')}
</ul>
</div>
<div class="overflow-scroll max-h-96" v-else>
<ul v-if="hasFiles">
${fileNames
.map(
(f) => `<li class="list-none drop-shadow-md mb-5 border-b-2 border-neutral-900 border-solid">
<strong>⭐️ New file name ⭐️ </strong>
<div class="text-orange-200">${f.newName}</div><hr/>
</li>`
)
.join(' ')}
</ul>
</div>
<div v-if="!successMessage && !loading && hasFiles">
<button id="rename-songs" class="button bg-amber-100 text-amber-700 p-2 m-4 rounded">Translate files</button>
</div>
</div>
<div v-else class="p-2">
<h1 class="text-rose-500">MAX LIMIT ERROR: Too many files parsed (${limitErrorState.lastFilesCount})!</h1>
<p class="text-orange-200">Current limit set to ${fileTranslationLimitNumber}</p>
</div>
`,
widgetConfig
);
translationWidget.onClick(async (data) => {
if (data.targetId === 'open-translated-dir') await $`open ${translationPath}`;
if (data.targetId === 'rename-songs' && !limitErrorState.hasLimitError) {
const hasFiles = !!fileNames.length;
try {
translationWidget.setState({ ...initState, loading: true, hasFiles });
logVerboseInfo(shouldLogVerboseLogs);
renameMp3Files(fileNames, async () => {
const successMessage = `${fileNames.length} files have been renamed successfully!`;
translationWidget.setState({
...initState,
hasFiles,
successMessage,
loading: false,
});
console.log(successMessage);
});
} catch (error) {
const errorMessage = `An error has occurred during translation! Error: ${error}`;
translationWidget.setState({
...initState,
errorMessage,
hasFiles,
loading: false,
});
console.error(errorMessage);
}
}
});
async function initTranslationApp() {
const ROOT_TRANSLATION_PATH = await env('ROOT_TRANSLATION_PATH');
const uniqueFileTypes = new Set(allExtensionsValues);
const fileType = await arg('Select a file type to translate.', Array.from(uniqueFileTypes));
const translationPath = await path({
startPath: ROOT_TRANSLATION_PATH,
hint: 'Please add the specific folder path of files you want to translate.',
onlyDirs: true,
});
return { translationPath, fileType };
}
async function getFilesNames(dir, fileType, fileTranslationLimit, shouldReadFilesRecursively = true) {
if (fileNameConfigList.length >= fileTranslationLimit) {
limitErrorState = {
...limitErrorState,
hasLimitError: true,
lastFilesCount: fileNameConfigList.length,
};
return [];
}
if (shouldContinueNextFolder === CHECKS.NO) {
return fileNameConfigList;
}
const files = await readdir(dir);
for (const fileName of files) {
if (shouldContinueTranslation === CHECKS.NO) return [];
const nextDir = getNextDir(dir, fileName);
const isDirectory = await isDir(nextDir);
if (isDirectory && shouldReadFilesRecursively) {
fileNameConfigList = await getFilesNames(nextDir, fileType, fileTranslationLimit);
} else {
const isOsxFile = fileName.startsWith('._');
const shouldAddFileToList = isValidFileExtension(fileName, fileType) && !isOsxFile;
if (shouldAddFileToList) {
const translatedMp3Name = translateToGreeklish(fileName);
fileNameConfigList.push({
oldName: fileName,
newName: translatedMp3Name,
oldSrc: path.join(dir, fileName),
newSrc: path.join(dir, translatedMp3Name),
});
}
}
}
return fileNameConfigList;
}
function getExtensionTypes(fileType) {
const isValidExtensionType = allExtensionsValues.some((type) => type === fileType);
if (isValidExtensionType)
return allExtensionEntries
.map(([ext, types]) => {
if (types.includes(fileType)) return ext;
return null;
})
.filter((item) => !!item);
}
function replaceText(text, replacementMap, isExactMatch = false, ignoreCharacters = '', regExOptions = 'g') {
let regexString, regex;
if (typeof text === 'string' && text.length > 0) {
replacementMap.forEach(function (replacementItem) {
if (isExactMatch) {
regexString = replacementItem.find;
} else {
regexString = '[' + replacementItem.find + ']';
}
if (ignoreCharacters !== '') {
regexString = '(?![' + ignoreCharacters + '])' + regexString;
}
regex = new RegExp(regexString, regExOptions);
text = text.replace(regex, replacementItem.replace);
});
}
return text;
}
function getNextDir(dirName, fileName) {
return path.join(dirName, fileName);
}
function translateToGreeklish(mp3Text) {
return replaceText(mp3Text, greekToGreeklishMap, true);
}
function isValidFileExtension(fileName, fileType = 'audio') {
const splittedAudioFile = fileName.split('.');
if (!splittedAudioFile.length) return false;
const fileExtension = splittedAudioFile[splittedAudioFile.length - 1];
return getExtensionTypes(fileType).includes(fileExtension);
}
function renameMp3Files(filesConfigList, onRenamingCompletion) {
if (filesConfigList.length)
filesConfigList.forEach((f, i) => {
fs.rename(f.oldSrc, f.newSrc, (err) => {
if (err) console.error(err);
if (i === filesConfigList.length - 1) onRenamingCompletion();
});
});
}
function logVerboseInfo(shouldLogVerboseLogs) {
if (shouldLogVerboseLogs === 'yes') {
console.log(`RENAMING ${fileNames.length} FILES......`);
fileNames.forEach((config) => {
console.log(`${config.oldName} =====> ${config.newName}`);
});
}
}