diff --git a/README.md b/README.md index 74231e6..e780d0a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# rangement +# Devine le rangement -scripts nodejs pour ranger ses fichiers selon la méthodo PIM de Karl Voit, avec quelques utilitaires personnels \ No newline at end of file +script qui devine comment renommer des fichiers selon un pattern de date +trouve des infos exif et prend la plus ancienne pour renseigner le nom de fichier. +Inspiré des travaux de Karl Voit et de ses libs python GuessFileName, append2name, move2archive. + +# lancement de renommage +`devine mon_fichier.jpg mon_autre_fichier.pdf` + +✅ +➡️ +🎉 + +## options +* -n , dry-run, ne pas renommer +* --photos-folder, spécifie un dossier pour les photos \ No newline at end of file diff --git a/configs.mjs b/configs.mjs new file mode 100644 index 0000000..0ec87b4 --- /dev/null +++ b/configs.mjs @@ -0,0 +1,6 @@ + +export const tagSeparator = ' ' +export const tagSectionSeparator = '--' +export const enableTestsLocally = false +export const reportStatistics = false +export const version = '1.0.0' \ No newline at end of file diff --git a/finder.mjs b/finder.mjs new file mode 100644 index 0000000..790b239 --- /dev/null +++ b/finder.mjs @@ -0,0 +1,244 @@ +/** + * la classe qui repère des patterns + */ +import { tagSectionSeparator, tagSeparator } from './configs.mjs' +import exifr from 'exifr' +import moment from 'moment' +import path from 'path' + +/** + * finds patterns for file name + */ +export default class finder { + + static statistics = { + filesModified: 0, + } + static patternsFiles = { + 'downloaded_pic': /^\-\w{15}\.jpg/, // FyB8cZnWIAc21rw.jpg + 'telegram_pic': /^\-\d{19}_\d{4}/, // -4900281569878475578_1109.jpg + 'open_camera': /^IMG_OC_\d{8}/i, // IMG_OC_20230617_092120_3.jpg + 'screenshot': /^Screenshot/i, // Screenshot 2023-06-15 at 15-26-04 Instance Panoramax OSM-FR.png + } + + static reportStatistics () { + console.log('statistics', + this.statistics) + } + + static findScreenshot (inputString) { + return inputString.match(/screenshot/i) || inputString.match(/capture d'écran/i) + } + + static findFormattedDate (filepath) { + let match = filepath.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ig) + // console.log('match findFormattedDate', match) + let result = '' + if (match && match[0]) { + result = match[0] + } + return result + } + + static findFileExtension (inputString) { + let result = inputString.match(/\.\w{3,4}$/i) + return result + } + + /** + * find the section of file name which contains the free text to describe the picture + * @param fileName + * @returns {*|string} + */ + static findFileNameFreeTextPart (fileName) { + fileName = fileName.replace(this.findFileExtension(fileName), '') + let boom = fileName.split(tagSectionSeparator) + if (boom.length) { + let freeTextPart = boom[0].trim() + console.log('freeTextPart', freeTextPart) + return freeTextPart + } + return fileName.trim() + } + + /** + * find an array of tags + * @param inputString + * @returns {[]} + */ + static findTagSectionInString (inputString) { + let listOfTags = [] + // remove extension + let extensionFile = finder.findFileExtension(inputString) + + if (extensionFile) { + extensionFile = extensionFile[0] + } else { + console.log('no extensionFile', extensionFile, inputString) + extensionFile = '' + } + inputString = inputString.replace(extensionFile, '') + + // console.log('extensionFile', extensionFile) + if (inputString.includes(tagSectionSeparator)) { + // console.log('inputString', inputString) + if (inputString.length) { + + let boom = inputString.split(tagSectionSeparator) + // console.log('boom', boom) + if (boom.length) { + let fileSectionsName = boom.splice(tagSeparator) + listOfTags = [...fileSectionsName[1].trim().split(tagSeparator)] + // console.log('listOfTags', listOfTags) + } else { + console.log('no boom', boom) + } + } + } + return listOfTags + } + + static cleanSpaces (inputString) { + return inputString.trim().replace(/ *g/, ' ') + } + + static searchAndReplaceInFileName (searchString, replaceString, fileName) { + return this.cleanSpaces(fileName.replace(searchString, replaceString)) + } + + /** + * search screenshot clues and rename + */ + static searchAndRenameScreenshots (fileName) { + if (finder.findScreenshot(fileName)) { + let tags = this.findTagSectionInString(fileName) + console.log('tags', tags) + if (!tags.includes('screenshot')) { + + fileName = this.addTagInFileName('screenshot', fileName) + fileName = this.searchAndReplaceInFileName('Screenshot', '', fileName) + console.log('screenShotMockFileName:', fileName) + return this.cleanSpaces(fileName) + } + console.log('is a screenshot, remove screenshot in name, and add tag screenshot') + } else { + return null + } + } + + static addTagInFileName (tagName, fileName) { + + let tags = this.findTagSectionInString(fileName) + let firstPart = this.findFileNameFreeTextPart(fileName) + + tags.push(tagName) + let uniqueArray = [...new Set(tags)] + + let newFileName = firstPart + ' ' + tagSectionSeparator + ' ' + tags.join(tagSeparator) + newFileName = newFileName.replace(/ {*}/, '') + this.findFileExtension(fileName) + return this.cleanSpaces(newFileName) + } + + /** + * convertit un nom de fichier en une structure décrivant plusieurs parties correspondant au pattern d'archivage + * @param fullPath + * @returns {{extension: *, dateStamp: string, freeText: (*|string), tags: *[]}} + */ + static destructurateFileName (fullPath) { + let [folderPath, fileNameOriginal] = this.findFolderPath(fullPath) + let dateStampInFileNameOriginal = this.findFormattedDate(fileNameOriginal) + return { + fullPath, + folderPath, + fileNameOriginal, + dateStampInFileNameOriginal, + dateStampExif: '', + freeText: this.findFileNameFreeTextPart(fileNameOriginal), + tags: this.findTagSectionInString(fileNameOriginal), + extension: this.findFileExtension(fileNameOriginal), + } + + } + + /** + * finds the earliest part in several exif date info + * @param exifData + * @returns {string} + */ + static findEarliestDateInExifData (exifData) { + if (exifData) { + + let moments = [] + + // console.log('exif data : ', exifData) // Do something with your data! + if (exifData.DateTimeOriginal) { + // console.log('image créée le : DateTimeOriginal : ', exifData.DateTimeOriginal) // Do something with your data! + moments.push(exifData.DateTimeOriginal) + } + if (exifData.ModificationDateTime) { + // console.log('image créée le : ModificationDateTime : ', exifData.ModificationDateTime) // Do something with your data! + moments.push(exifData.ModificationDateTime) + } + if (exifData.ModifyDate) { + // console.log('image créée le : ModifyDate : ', exifData.ModifyDate) // Do something with your data! + moments.push(exifData.ModifyDate) + } + if (exifData.FileAccessDateTime) { + moments.push(exifData.FileAccessDateTime) + } + if (exifData.FileInodeChangeDateTime) { + moments.push(exifData.FileInodeChangeDateTime) + } + if (exifData.FileModificationDateTime) { + // console.log('image créée le : FileModificationDateTime : ', exifData.FileModificationDateTime) // Do something with your data! + moments.push(exifData.FileModificationDateTime) + } + if (exifData.CreateDate) { + // console.log('image créée le : CreateDate : ', exifData.CreateDate) // Do something with your data! + moments.push(exifData.CreateDate) + } + + moments = moments.map(d => { + let newdate = moment(d) + return newdate + }) + let minDate = moment.min(moments) + + // console.log('minDate :::::::::', minDate) + console.log('minDate :::::::::', minDate.format('yyyy-MM-DDTHH:mm:ss')) + + return minDate.format('yyyy-MM-DDTHH:mm:ss') + } else { + console.log('pas de exif data') + return '' + } + } + + /** + * examine plusieurs propriétés exif de date et retourne la plus ancienne + * @param filepath + */ + static async findExifCreationDate (filepath) { + + console.log('filepath', filepath) + let dateAlreadyInFileName = finder.findFormattedDate(filepath) + if (dateAlreadyInFileName) { + + console.log('------ dateAlreadyInFileName', dateAlreadyInFileName) + } + + return await exifr.parse(filepath) + + } + + static findFolderPath (filePath) { + let folders = filePath.split('/') + let fileName = folders.pop() + folders = filePath.replace(fileName, '') + + console.log('\n - folders', folders) + console.log(' - fileName', fileName, '\n') + return [folders, fileName] + + } +} \ No newline at end of file diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..75964fb --- /dev/null +++ b/index.mjs @@ -0,0 +1,134 @@ +/**--------------------- + * @name tykayn Rangement + * @description Rangement sorts and rename files depending on their exif data + * @contact contact@cipherbliss.com + --------------------- */ +/** --------------------- + libs + --------------------- */ +import fs from 'node-fs' +import minimist from 'minimist' +/** --------------------- + custom utilities and configuration + --------------------- */ +import { enableTestsLocally, reportStatistics,tagSectionSeparator, tagSeparator } from './configs.mjs' +import { + TestFindFormattedDate, + TestScreenShotIsFoundAndRenamed, + TestTagsAreDetectedInFileName +} from './testFunctions.mjs' +import finder from './finder.mjs' + +let mini_arguments +console.log(' ') + +function parseArguments () { + mini_arguments = minimist(process.argv.slice(2)) + console.log('arguments', mini_arguments) +} + +parseArguments() + +function renameFile (originalFileName, fileMixedNewName) { + fs.rename(originalFileName, fileMixedNewName, function (err) { + if (err) console.log('rename ERROR: ' + err) + }) +} + +function appendFileName (fileProperties, newText) { + fileProperties.freeText = finder.cleanSpaces(fileProperties.freeText + ' ' + newText) + return fileProperties +} + +function prependFileName (fileProperties, newText) { + fileProperties.freeText = finder.cleanSpaces(newText + ' ' + fileProperties.freeText) + return fileProperties +} + +function makeFileNameFromProperties(fileProperties) { + + let tagPlace = '' + if (fileProperties.tags.length) { + tagPlace = ' ' + tagSectionSeparator + ' ' + } + // return finder.cleanSpaces(fileProperties.dateStamp + ' ' + fileProperties.freeText + tagPlace + fileProperties.tags.join(tagSeparator) + fileProperties.extension).replace(+' ' + tagSectionSeparator + ' ' + '.', '.') + return ''+fileProperties.dateStampExif + ' ' + fileProperties.freeText + tagPlace + fileProperties.tags.join(tagSeparator) + fileProperties.extension +} + +function shouldWeChangeName (structureForFile) { + console.log(' ______ allez hop fini la recherche on fait un nouveau nom') + console.log('structureForFile', structureForFile) + let newName = makeFileNameFromProperties(structureForFile) + if (structureForFile.fileNameOriginal !== newName) { + + console.log('\n ancien nom :', structureForFile.fileNameOriginal) + // console.log(' nouveau nom:', foundDate +structureForFile.freeText + structureForFile.tags.join(tagSeparator) + structureForFile.extension ) + console.log(' nouveau nom:', newName) + } else { + console.log(' rien à changer') + } + +} + +async function guessFileNameOnAllFilesFromArguments () { + + // parcourir les dossiers + // parcourir les fichiers + + console.log('liste des fichiers', mini_arguments._) + let fileList = mini_arguments._ + + fileList.forEach(fullPath => { + + let structureForFile = finder.destructurateFileName(fullPath) + + // examiner les infos exif de chaque fichier pour proposer un nouveau nom + if (!structureForFile.dateStampInFileNameOriginal) { + console.log(' le nom de fichier ne contient pas de date formatée au début') + + finder.findExifCreationDate(structureForFile.fullPath) + .then(data => { + console.log(' ... chercher la date de création') + let foundDate = finder.findEarliestDateInExifData(data) + + console.log(' =>>>>>>> foundDate : ', foundDate) + if (foundDate) { + + + // finder.findEarliestDateInExifData(fullPath).then(response => { + // console.log(' ... trouvée') + // if (response) { + structureForFile.dateStampExif = foundDate + + shouldWeChangeName(structureForFile) + // } + // }) + + } else { + console.log('pas de date trouvée dans le nom') + } + + } + , + (error) => { + console.log('/////////// Error in reading exif of file: ' + error.message) + return '' + }) + } + } + ) + +} + +guessFileNameOnAllFilesFromArguments() + +// run tests +if (enableTestsLocally) { + + TestTagsAreDetectedInFileName() + TestFindFormattedDate() + TestScreenShotIsFoundAndRenamed() +} +if (reportStatistics || mini_arguments.stats) { + finder.reportStatistics() +} diff --git a/main.test.js b/main.test.js new file mode 100644 index 0000000..be3d3c6 --- /dev/null +++ b/main.test.js @@ -0,0 +1,22 @@ +import finder from "./finders.mjs"; +// const finders = require('./finders.mjs') + +describe('rangement file name', () => { + + test('detects date in file name', () => { + expect(finder.findFormattedDate('2023-06-23T18.36.47 -- machin bidule.jpg')).toBe('2023-06-23T18.36.47'); + }); + test('detects file extension in file name', () => { + expect(finder.findFileExtension()('2023-06-23T18.36.47 -- machin bidule.jpg')).toBe('jpg'); + }); +}) +console.log('finders', finder) + +test('adding positive numbers is not zero', () => { + for (let a = 1; a < 10; a++) { + for (let b = 1; b < 10; b++) { + expect(a + b).not.toBe(0); + } + } +}); + diff --git a/testFiles/2sqdf45s5g456ghdf.jpg b/testFiles/2sqdf45s5g456ghdf.jpg new file mode 100644 index 0000000..7e387cf Binary files /dev/null and b/testFiles/2sqdf45s5g456ghdf.jpg differ diff --git a/testFiles/un_document.pdf b/testFiles/un_document.pdf new file mode 100644 index 0000000..e69de29 diff --git a/testFiles/une_photo.jpg b/testFiles/une_photo.jpg new file mode 100644 index 0000000..e69de29 diff --git a/testFiles/une_photo_pas_pareil.jpg b/testFiles/une_photo_pas_pareil.jpg new file mode 100644 index 0000000..e69de29 diff --git a/testFiles/une_photo_pas_pareil.png b/testFiles/une_photo_pas_pareil.png new file mode 100644 index 0000000..e69de29 diff --git a/testFunctions.mjs b/testFunctions.mjs new file mode 100644 index 0000000..548b4dd --- /dev/null +++ b/testFunctions.mjs @@ -0,0 +1,48 @@ +import finder from './finder.mjs' + + +const pathFolder = '/home/poule/encrypted/stockage-syncable/photos/a_dispatcher/tout' +const sortingFolder = '/home/poule/encrypted/stockage-syncable/photos/a_dispatcher' + +const fileDefinition = { + dateStamp: '', + freeText: '', + tags: [], + extension: '', +} + +export function TestScreenShotIsFoundAndRenamed() { + + let screenShotMockFileName = 'Screenshot 2023-06-15 at 15-28-21 Instance Panoramax OSM-FR.png' + let screenShotMockFileNameExpected = '2023-06-15 at 15-28-21 Instance Panoramax OSM-FR -- screenshot.png' + let found = finder.searchAndRenameScreenshots(screenShotMockFileName) + console.log('found', found) + if (found === screenShotMockFileNameExpected) { + console.log('TestScreenShotIsFoundAndRenamed : test succès') + } else { + console.log('TestScreenShotIsFoundAndRenamed : FAIL:') + console.log(found) + console.log(screenShotMockFileNameExpected) + } +} + +export function TestTagsAreDetectedInFileName() { + let mockFileName = '2023-06-15T10:11:12 -- screeenshot festival.png' + let expectedResult = ['screeenshot', 'festival'] + let found = finder.findTagSectionInString(mockFileName) + if (found === expectedResult) { + console.info('Succès') + } +} + + +export function TestFindFormattedDate() { + let mockFileName = 'Capture d\'écran 2023-06-15T10:11:12.png' + let expectedResult = '2023-06-15T10:11:12' + let found = finder.findFormattedDate(mockFileName) + console.log('foundDate', found, expectedResult) + console.log('foundDate', found) + if (found === expectedResult) { + console.info('Succès') + } +} diff --git a/workInProgress/not_ready_functions.mjs b/workInProgress/not_ready_functions.mjs new file mode 100644 index 0000000..fd67409 --- /dev/null +++ b/workInProgress/not_ready_functions.mjs @@ -0,0 +1,103 @@ +import finder from './finders' +import fs from 'node-fs' + +/** + ----------------------- parties non réalisées ----------------------- + * work in progress +// TODO + --------------------------------------------------------------------- + **/ + +function TestDownloadedTelegramPictureRename (fileName) { + let fileProperties = destructurateFileName(fileName) +} + +function hasDifferentDateInNameThanExif (fileName) { + let foundDate = finder.findFormattedDate(fileName) + + if (foundDate && foundDate != getExifCreationDate(fileName)) { + return true + } + return false +} + +function moveToArchive (targetDirectory, fileFullPath) { + // find current directory, + // rename file to move it +} + +function getStatisticsOnArchiveFolder (fileFullPath) { + + return { + foldersCount: 'TODO', + filesWithoutSemanticName: 'TODO' + } +} + +function getControlledVocabularyFromFiles (fileFullPath) { + + let controlledVocabulary = ['TODO'] + // find all tags + return controlledVocabulary +} + +function moveToSortingFolder (fileFullPath) { + return 'TODO' +} + +/** + * écrit un nouveau nom de fichier formatté + * @param convertedToName + * @param originalFileName + * @returns {*} + */ +function mixDateNameWithFileName (convertedToName, originalFileName) { + // enlever l'ancien timestamp si il existe + // ajouter en début de nom le nouveau timestamp avec un espace et conserver le reste du nom + return originalFileName +} + +function TestMixingName () { + + let fileMixedNewName = mixDateNameWithFileName(convertedToName, originalFileName) + console.log('new name', fileMixedNewName) + + if (fileMixedNewName !== originalFileName) { + console.log('renommage =>', fileMixedNewName) + // renameFile(originalFileName, fileMixedNewName) + } +} + + + +/** + * obtenir une liste des dossiers uniquement dans le dossier courant + * @param path + * @returns {*} + */ +function getDirectories (path) { + return fs.readdirSync(path).filter(function (file) { + return fs.statSync(path + '/' + file).isDirectory() + }) +} + + +function convertDateToTimeInFileName (inputDate) { + return inputDate.replace(' ', 'T') +} + +function testthings(){ + // let list = getDirectories(pathFolder) +// console.log('list', list) + + let originalFileName = '2015-04-30T09.09.02 -- scan papier.jpg' + let formattedDatePIMBefore = finder.findFormattedDate(originalFileName) + console.log('formattedDatePIMBefore', formattedDatePIMBefore) + + let creationDateFound = finder.getExifCreationDate(pathFolder + '/' + originalFileName) + let convertedToName = '' + if (creationDateFound) { + convertedToName = convertDateToTimeInFileName(creationDateFound) + } + console.log('convertedToName', convertedToName) +} \ No newline at end of file