Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
7b3c90a
introduced a separation of jpgs by the month the picture has been tak…
tfrdidi Sep 17, 2015
38ee707
added a mechanism to distinguis pictures from different event. Thereb…
tfrdidi Sep 18, 2015
80aa1ed
added a readme
tfrdidi Sep 18, 2015
daf30bb
minor formattings of readme
tfrdidi Sep 18, 2015
0c5430b
camel case routine names
tfrdidi Sep 20, 2015
2b454cb
jpgSorter.py
tfrdidi Nov 29, 2015
e7c91f8
Propmt when input parameter missing, progress output
tfrdidi Nov 29, 2015
4dc8054
Updated to new version
tfrdidi Nov 29, 2015
7179123
used file counter for file names, one method to be called after image…
tfrdidi Dec 6, 2015
330ceee
process images only afterwards, log method, limitation of files per d…
tfrdidi Dec 6, 2015
525537c
limit number of files/directory with subdirectories
tfrdidi Dec 6, 2015
a13cc55
described new functions like file/directory limit
tfrdidi Dec 6, 2015
a6f6930
only update status ever 1%, copy metadata
tfrdidi Dec 6, 2015
39d78c8
check for file existance before moving, deleting if already existing …
tfrdidi Dec 6, 2015
1f21b8d
Fixed errors due to integer division.
dgnthr Jan 28, 2016
38b70c8
Merge pull request #1 from dgnthr/master
tfrdidi Feb 14, 2016
8e4c2a7
put commands for usage under the usage headline
tfrdidi Mar 12, 2016
5d8b7f3
Update readme.md
hashimaziz1 Sep 18, 2017
4cbc8dc
Update readme.md
hashimaziz1 Sep 18, 2017
3b54393
Update readme.md
hashimaziz1 Sep 18, 2017
bf9aa63
Update readme.md
hashimaziz1 Sep 18, 2017
55284d3
Update readme.md
hashimaziz1 Sep 18, 2017
73aa74c
Extend jpgSorter.py to allow sorting into years *and* months.
woutgg Nov 25, 2017
db3970d
Add .gitignore to ignore *.pyc.
woutgg Nov 25, 2017
a1266c6
Cleanup whitespace.
woutgg Nov 25, 2017
dc9b462
Make indentation style consistent (all spaces now).
woutgg Nov 25, 2017
a69f558
Use argparse to process commandline arguments and add argument to spl…
woutgg Nov 26, 2017
3e636e3
Fix hashbang.
woutgg Nov 26, 2017
f09b88f
Update readme.md.
woutgg Nov 26, 2017
42e00be
Merge pull request #2 from woutgg/master
tfrdidi Nov 30, 2017
5f56682
Added info to install exifread
tfrdidi Nov 30, 2017
284d329
added parameter "keep filename"
tfrdidi Nov 30, 2017
0bfbd40
Merge branch 'master' of https://github.com/tfrdidi/sort-PhotorecReco…
tfrdidi Nov 30, 2017
6ee334b
ordered imports
tfrdidi Nov 30, 2017
26c334f
ordered imports #2
tfrdidi Nov 30, 2017
caf9b84
min event delta is configurable via parameter now =)
tfrdidi Nov 30, 2017
4f214f8
Update readme.md
tfrdidi Nov 30, 2017
1a98397
Update readme.md
hashimaziz1 Mar 30, 2019
033e5ac
Fix SyntaxWarning in recovery.py
the-emerald Jun 26, 2021
f59c7bf
Merge pull request #3 from the-emerald/fix-is
tfrdidi Jul 9, 2021
ee18f84
fix division by zero if less than 100 files are copied
the1311 Nov 16, 2021
23bc59f
add additional token for DateTime to jpgSorter.py
the1311 Nov 16, 2021
747a7dd
add renaming jpg-files with <Date>_<Time> as option
the1311 Nov 16, 2021
6c22af1
changed messages in recovery.py and update readme.md
the1311 Nov 16, 2021
f616cc3
Update readme.md
the1311 Nov 16, 2021
f7e4543
Update readme.md
the1311 Nov 16, 2021
20b3655
Update readme.md
the1311 Nov 16, 2021
aa7cf6b
Update readme.md
the1311 Nov 16, 2021
442640b
Merge branch 'master' of https://github.com/tfrdidi/sort-PhotorecReco…
hashimaziz1 Apr 4, 2022
4020afb
Update readme.md
hashimaziz1 Apr 4, 2022
4bf4057
file sorting: Accomodate files without a suffix
gadse Apr 12, 2022
951dd1a
Merge pull request #8 from gadse/master
tfrdidi Aug 20, 2022
a7549b3
Merge pull request #7 from Kaos-Industries/master
tfrdidi Aug 20, 2022
a223b9f
Merge branch 'master' into add-renaming-jpgs
tfrdidi Aug 20, 2022
fb32347
Merge pull request #6 from the1311/add-renaming-jpgs
tfrdidi Aug 20, 2022
cfd0496
Merge pull request #4 from the1311/fix-div-by-zero-in-recovery.py
tfrdidi Aug 20, 2022
bddb6d2
Merge pull request #5 from the1311/add-token-to-jpgSorter.py
tfrdidi Aug 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
.vscode
128 changes: 128 additions & 0 deletions jpgSorter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os.path
import ntpath
from time import localtime, strftime, strptime, mktime
import shutil
import exifread

unknownDateFolderName = "date-unknown"

def getMinimumCreationTime(exif_data):
creationTime = None
dateTime = exif_data.get('DateTime')
if (dateTime is None):
dateTime = exif_data.get('Image DateTime')
dateTimeOriginal = exif_data.get('EXIF DateTimeOriginal')
dateTimeDigitized = exif_data.get('EXIF DateTimeDigitized')

# 3 differnt time fields that can be set independently result in 9 if-cases
if (dateTime is None):
if (dateTimeOriginal is None):
# case 1/9: dateTime, dateTimeOriginal, and dateTimeDigitized = None
# case 2/9: dateTime and dateTimeOriginal = None, then use dateTimeDigitized
creationTime = dateTimeDigitized
else:
# case 3/9: dateTime and dateTimeDigitized = None, then use dateTimeOriginal
# case 4/9: dateTime = None, prefere dateTimeOriginal over dateTimeDigitized
creationTime = dateTimeOriginal
else:
# case 5-9: when creationTime is set, prefere it over the others
creationTime = dateTime

return creationTime

def postprocessImage(images, imageDirectory, fileName):
imagePath = os.path.join(imageDirectory, fileName)
image = open(imagePath, 'rb')
creationTime = None
try:
exifTags = exifread.process_file(image, details=False)
creationTime = getMinimumCreationTime(exifTags)
except:
print("invalid exif tags for " + fileName)

# distinct different time types
if creationTime is None:
creationTime = localtime(os.path.getctime(imagePath))
else:
try:
creationTime = strptime(str(creationTime), "%Y:%m:%d %H:%M:%S")
except:
creationTime = localtime(os.path.getctime(imagePath))

images.append((mktime(creationTime), imagePath))
image.close()

# Creates the requested path recursively.
def createPath(newPath):
if not os.path.exists(newPath):
os.makedirs(newPath)

# Pass None for month to create 'year/eventNumber' directories instead of 'year/month/eventNumber'.
def createNewFolder(destinationRoot, year, month, eventNumber):
if month is not None:
newPath = os.path.join(destinationRoot, year, month, str(eventNumber))
else:
newPath = os.path.join(destinationRoot, year, str(eventNumber))

createPath(newPath)

def createUnknownDateFolder(destinationRoot):
path = os.path.join(destinationRoot, unknownDateFolderName)
createPath(path)

def writeImages(images, destinationRoot, minEventDeltaDays, splitByMonth=False):
minEventDelta = minEventDeltaDays * 60 * 60 * 24 # convert in seconds
sortedImages = sorted(images)
previousTime = None
eventNumber = 0
previousDestination = None
today = strftime("%d/%m/%Y")

for imageTuple in sortedImages:
destination = ""
destinationFilePath = ""
t = localtime(imageTuple[0])
year = strftime("%Y", t)
month = splitByMonth and strftime("%m", t) or None
creationDate = strftime("%d/%m/%Y", t)
fileName = ntpath.basename(imageTuple[1])

if(creationDate == today):
createUnknownDateFolder(destinationRoot)
destination = os.path.join(destinationRoot, unknownDateFolderName)
destinationFilePath = os.path.join(destination, fileName)

else:
if (previousTime == None) or ((previousTime + minEventDelta) < imageTuple[0]):
eventNumber = eventNumber + 1
createNewFolder(destinationRoot, year, month, eventNumber)

previousTime = imageTuple[0]

destComponents = [destinationRoot, year, month, str(eventNumber)]
destComponents = [v for v in destComponents if v is not None]
destination = os.path.join(*destComponents)

# it may be possible that an event covers 2 years.
# in such a case put all the images to the event in the old year
if not (os.path.exists(destination)):
destination = previousDestination
# destination = os.path.join(destinationRoot, str(int(year) - 1), str(eventNumber))

previousDestination = destination
destinationFilePath = os.path.join(destination, fileName)

if not (os.path.exists(destinationFilePath)):
shutil.move(imageTuple[1], destination)
else:
if (os.path.exists(imageTuple[1])):
os.remove(imageTuple[1])


def postprocessImages(imageDirectory, minEventDeltaDays, splitByMonth):
images = []
for root, dirs, files in os.walk(imageDirectory):
for file in files:
postprocessImage(images, imageDirectory, file)

writeImages(images, imageDirectory, minEventDeltaDays, splitByMonth)
27 changes: 27 additions & 0 deletions numberOfFilesPerFolderLimiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import sys
import math
import os
import shutil


def limitFilesPerFolder(folder, maxNumberOfFilesPerFolder):
for root, dirs, files in os.walk(folder, topdown=False):
for dir in dirs:
dirPath = os.path.join(root, dir)
filesInFolder = len(os.listdir(dirPath))
if(filesInFolder > maxNumberOfFilesPerFolder):
numberOfSubfolders = ((filesInFolder - 1) // maxNumberOfFilesPerFolder) + 1
for subFolderNumber in range(1, numberOfSubfolders+1):
subFolderPath = os.path.join(dirPath, str(subFolderNumber))
if not os.path.exists(subFolderPath):
os.mkdir(subFolderPath)
fileCounter = 1
for file in os.listdir(dirPath):
source = os.path.join(dirPath, file)
if os.path.isfile(source):
destDir = str(((fileCounter - 1) // maxNumberOfFilesPerFolder) + 1)
destination = os.path.join(dirPath, destDir, file)
shutil.move(source, destination)
fileCounter += 1


97 changes: 97 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Sort files recovered by Photorec

Photorec does a great job when recovering deleted files. But the result is a huge, unsorted, unnamed amount of files. Especially for external hard drives serving as backup of all the personal data, sorting them is an endless job.

This program sPRF helps you sorting your files. First of all, the **files are copied to own folders for each file type**. Second, **jpgs are distinguished by the year, and optionally by month as well** when they have been taken **and by the event**. We thereby define an event as a time span during them photos are taken. It has a delta of 4 days without a photo to another event. If no date from the past can be detected, these jpgs are put into one folder to be sorted manually.

## Installation

First install the package [exifread](https://pypi.python.org/pypi/ExifRead):

`pip install exifread`

## Run the sorter

Then run the sorter:

`python recovery.py <path to files recovered by Photorec> <destination>`

This copies the recovered files to their file type folder in the destination directory. The recovered files are not modified. If a file already exists in the destination directory, it is skipped. This means that the program can be interrupted with Ctrl+C and then continued at a later point by running it again.

The first output of the programm is the number of files to copy. To count them might take some minutes depending on the amount of recovered files. Afterwareds you get some feedback on the processed files.

### Parameters

For an overview of all arguments, run with the `-h` option: `python recovery.py -h`.

#### Max numbers of files per folder

All directories contain a maximum of 500 files by default. If there are more for a file type, numbered subdirectories are created. If you want another file-limit, e.g. 1000, pass that number as the third parameter when running the program:

`python recovery.py <path to files recovered by Photorec> <destination> -n1000`

#### Folder for each month

sPRF usually sorts your photos by year:

```
destination
|- 2015
|- 1.jpg
|- 2.jpg
|- ...
|- 2016
|- ...
```

Sometimes you might want to sort each year by month:

`python recovery.py <path to files recovered by Photorec> <destination> -m`

Now you get:

```
destination
|- 2015
|- 1
|- 1.jpg
|- 2.jpg
|- 2
|- 3.jpg
|- 4.jpg
|- ...
|- 2016
|- ...
```

#### Keep original filenames

Use the -k parameter to keep the original filenames:

`python recovery.py <path to files recovered by Photorec> <destination> -k`

#### Adjust event distance

For the case you want to reduce or increase the timespan between events, simply use the parameter -d. The default is 4:

```python recovery.py <path to files recovered by Photorec> <destination> -d10```

#### Rename jpg-files with ```<Date>_<Time>``` from EXIF data if possible

If the original jpg image files were named by ```<Date>_<Time>``` it might be useful to rename the recovered files in the same way. This can be done by adding the parameter -j.

```python recovery.py <path to files recovered by Photorec> <destination> -j```

If no EXIF data can be retrieved the original filename is kept.

In case there are two or more files with the same EXIF data the filename is extended by an index to avoid overwritng files.

The result will look like:
```
20210121_134407.jpg
20210122_145205.jpg
20210122_145205(1).jpg
20210122_145205(2).jpg
20210122_145813.jpg
20210122_153155.jpg
```
Loading