diff --git a/.coveragerc b/.coveragerc
index 264914b..10aad34 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,3 +1,3 @@
[run]
source = src
-omit = tests/*, __init__.py, updater.py
+omit = tests/*, __init__.py, updater.py, main.py
diff --git a/.gitignore b/.gitignore
index 3f46fba..754218c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,6 @@ app/
.test_folder/
*/coverage/
*.log
+*.xml
.coverage
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 73f6fb2..f2d4434 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,7 @@ pytest:
image: python:3.12-alpine
script:
- sh tests/start_tests_gitlab.sh
- - sed -i "s#/builds/DasMoorhuhn/autopicture-v3/src#${CI_PROJECT_DIR}#g" coverage.xml
+ # - sed -i "s#/builds/DasMoorhuhn/autopicture-v3/src#${CI_PROJECT_DIR}#g" coverage.xml
coverage: '/Code coverage: \d+(?:\.\d+)?/'
artifacts:
name: "$CI_JOB_NAME"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2194843..f38b603 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## v0.2.0 []
+- Added i18n for multi language support
+- Fixed tests
+- Fixed gitlab CI file
+- Fixed bug on string splitting at filtering date/time
+- Only pictures will be sorted
+- WIP: RAW sorting
## v0.1.0 [2023-11-30]
- Added updater
- Added Changelog file
diff --git a/README.md b/README.md
index 3f5a77f..474461c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Bildersortierprogramm geschrieben in Python3.12.
-Beispiel Struktur der Sortierung:
+Example structure:
```bash
app/Bilder/
└── SONY
@@ -10,7 +10,7 @@ app/Bilder/
├── 10
│ ├── 21
│ │ ├── DSC02975.JPG
- │ │ ├── DSC02976.JPG
+ │ │ └── DSC02976.JPG
│ ├── 25
│ │ ├── DSC03030.JPG
│ │ ├── DSC03031.JPG
@@ -25,17 +25,17 @@ app/Bilder/
│ │ └── DSC03135.JPG
│ └── 30
│ ├── DSC03136.JPG
- │ ├── DSC03137.JPG
+ │ └── DSC03137.JPG
├── 11
│ ├── 16
│ │ └── DSC03144.JPG
│ ├── 17
│ │ ├── DSC03145.JPG
- │ │ ├── DSC03146.JPG
+ │ │ └── DSC03146.JPG
│ └── 28
│ ├── DSC03153.JPG
│ ├── DSC03154.JPG
- │ ├── DSC03155.JPG
+ │ └── DSC03155.JPG
├── ...
@@ -45,8 +45,24 @@ app/Bilder/
## Python
+### Linux
+
+Debian:
+```bash
+sudo apt-get install -y python3 python3-pip
+```
+
+### Windows
+
+TODO:
+Download the latest version of Python and install it
+
## Pip
+```bash
+pip3 install -r requirements.txt
+```
+
## Config
## Starten
diff --git a/develop_requirements.txt b/develop_requirements.txt
new file mode 100644
index 0000000..159bb67
--- /dev/null
+++ b/develop_requirements.txt
@@ -0,0 +1,4 @@
+pytest
+pytest-cov
+pytest-factoryboy
+gitlabci-local
\ No newline at end of file
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..0b5bb03
--- /dev/null
+++ b/install.sh
@@ -0,0 +1 @@
+sudo apt-get install -y python3 python3-pip
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 52779a4..bf2a17f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,8 @@
-pillow==10.1.*
-pyyaml==6.0.*
-python-magic==0.4.*
-progressbar==2.5
-virtualenv==20.25.*
-requests==2.31.*
-pytest==7.4.*
-pytest-cov==4.1.*
-pytest-factoryboy==2.5.*
\ No newline at end of file
+pillow
+pyyaml
+python-magic
+progressbar
+virtualenv
+requests
+filetype
+python-i18n[YAML]
\ No newline at end of file
diff --git a/src/.version.json b/src/.version.json
index 3e0073b..cc28943 100644
--- a/src/.version.json
+++ b/src/.version.json
@@ -1,4 +1,4 @@
{
- "version": "0.1.0",
- "date": "2023-11-30"
+ "version": "0.2.0",
+ "date": "2024-05-25"
}
\ No newline at end of file
diff --git a/src/config.py b/src/config.py
index bdb3c8d..c387c9b 100644
--- a/src/config.py
+++ b/src/config.py
@@ -1,7 +1,8 @@
import yaml
import os
-config_file = "config.yml"
+workdir, filename = os.path.split(os.path.abspath(__file__))
+config_file = f"{workdir}{os.sep}config.yml"
def get_config():
@@ -13,3 +14,4 @@ class Config:
def __init__(self, data):
self.src = data['src']
self.dst = data['dst']
+ self.language = data['language']
diff --git a/src/config.yml b/src/config.yml
index a0bfb57..6cfe118 100644
--- a/src/config.yml
+++ b/src/config.yml
@@ -1,2 +1,7 @@
+# src (source) is the path, were the images are, that should be sorted
src: "../app/Temp"
-dst: "../app/Bilder"
\ No newline at end of file
+
+# dst (destination) ist the path, were the images are going to be sorted
+dst: "../app/Bilder"
+
+language: "en"
\ No newline at end of file
diff --git a/src/exif_data.py b/src/exif_data.py
index be26a64..6136ed0 100644
--- a/src/exif_data.py
+++ b/src/exif_data.py
@@ -1,10 +1,10 @@
class ExifData:
"""This is for an object that stores the data of a picture"""
- def __init__(self, image_path:str, image_name:str, day:int, month:int, year:int, time:str, make:str) -> None:
- self.path:str = image_path
- self.name:str = image_name
- self.day:int = int(day)
- self.month:int = int(month)
- self.year = int(year)
- self.time = str(time)
- self.make = str(make)
+ def __init__(self, data:dict) -> None:
+ self.path:str = str(data['image_path'])
+ self.name:str = str(data['image_name'])
+ self.day:int = int(data['day'])
+ self.month:int = int(data['month'])
+ self.year:int = int(data['year'])
+ self.time = data['time']
+ self.make:str = str(data['make'])
diff --git a/src/file_handler.py b/src/file_handler.py
index beba1a4..1be3dc0 100644
--- a/src/file_handler.py
+++ b/src/file_handler.py
@@ -1,7 +1,6 @@
import os
import sys
sys.path.append("../")
-import time
import shutil
import logging
from progressbar.progressbar import ProgressBar
@@ -12,28 +11,30 @@ from src.exif_data import ExifData
def sort_pictures(images:list, dst:str, logger:logging.Logger):
image_total = len(images)
image_counter = 0
- logging_infos = []
progress_bar = ProgressBar(
maxval=image_total,
term_width=70
)
- print(f"Start sorting {image_total} images\n")
progress_bar.start()
- start_timer = time.time()
+
for image in images:
image:ExifData
+ if not image: continue
path = os.path.join(dst, str(image.make), str(image.year), str(image.month), str(image.day))
if not os.path.exists(path): os.makedirs(path)
stat_info = os.stat(image.path)
- shutil.move(src=image.path, dst=f"{path}/{image.name}")
+ image_dst = f"{path}{os.sep}{image.name}"
+ shutil.move(src=image.path, dst=image_dst)
# os.chmod(path=f"{path}/{image.name}", mode=stat_info.st_mode)
- logger.info(f"Moved {image.path} -> {path}/{image.name}")
+ logger.info(f"Moved {image.path} -> {image_dst}")
progress_bar.update(image_counter)
image_counter += 1
- end_timer = time.time()
progress_bar.finish()
- print(f"\nDone\nSorted {image_total} images in {round(end_timer - start_timer, 2)} seconds")
+
+
+def sort_raws(raws:list, dst:str, logger:logging):
+ pass
diff --git a/src/i18n_translations/de.yml b/src/i18n_translations/de.yml
new file mode 100644
index 0000000..43c95dc
--- /dev/null
+++ b/src/i18n_translations/de.yml
@@ -0,0 +1,5 @@
+de:
+ start_sorting_images: 'Sortierung von %{image_count} Bild(ern) wird gestartet'
+ done_sorting_images: '%{image_count} Bild(er) in %{time} Sekunden sortiert'
+ done: 'Fertig'
+ no_images_found: 'Es wurden keine Bilder gefunden'
\ No newline at end of file
diff --git a/src/i18n_translations/en.yml b/src/i18n_translations/en.yml
new file mode 100644
index 0000000..27e34ca
--- /dev/null
+++ b/src/i18n_translations/en.yml
@@ -0,0 +1,5 @@
+en:
+ start_sorting_images: 'Start sorting %{image_count} image(s)\n'
+ done_sorting_images: 'Sorted %{image_count} image(s) in %{time} seconds'
+ done: 'Done'
+ no_images_found: 'No images found'
\ No newline at end of file
diff --git a/src/meta_data_handler.py b/src/meta_data_handler.py
index 72fb1ee..70a285a 100644
--- a/src/meta_data_handler.py
+++ b/src/meta_data_handler.py
@@ -1,14 +1,22 @@
import magic
import sys
-
+import os
+import filetype
from PIL import Image
from PIL import ExifTags
sys.path.append("../")
from src.exif_data import ExifData
+from src.mime_types import MimeTypes
video_formats = ["MP4", "MOV", "M4V", "MKV", "AVI", "WMV", "AVCHD", "WEBM", "MPEG"]
-picture_formats = ["JPG", "JPEG", "PNG", "TIFF"]
+picture_formats = ["JPG", "JPEG", "PNG"]
+raw_formats = ["CR2", "RAF", "RW2", "ERF", "NRW", "NEF", "ARW", "RWZ", "EIP",
+ "DNG", "BAY", "DCR", "GPR", "RAW", "CRW", "3FR", "SR2", "K25",
+ "KC2", "MEF", "DNG", "CS1", "ORF", "MOS", "KDC", "CR3", "ARI",
+ "SRF", "SRW", "J6I", "FFF", "MRW", "MFW", "RWL", "X3F", "PEF",
+ "IIQ", "CXI", "NKSC", "MDC"]
+
key_words = ["DateTime", "Make"]
@@ -19,43 +27,66 @@ def is_file_video(path:str):
else: return False
-def get_image_meta_data(image_path):
- image_extension = str(image_path).split("/")[-1].split(".")
- # TODO: Sort out videos
- img = Image.open(f"{image_path}")
+def is_file_picture(path:str):
+ mime = magic.Magic(mime=True)
+ file = mime.from_file(path)
+ if file.find('picture') != -1: return True
+ else: return False
+
+
+def handle_raw(image:str):
+ image_creation_time = os.path.getctime(filename=image)
+ print(image_creation_time)
+
+
+def handle_image(image:str):
+ img = Image.open(image)
values = []
for tag, text in img.getexif().items():
- if tag in ExifTags.TAGS:
- if image_extension[1].upper() in picture_formats:
- values.append(ExifTags.TAGS[tag] + "|" + str(text))
- return filter_date_and_make(meta_tags=filter_data(value=values), image_path=image_path)
+ if tag in ExifTags.TAGS: values.append([ExifTags.TAGS[tag], str(text)])
+ return filter_date_and_make(meta_tags=filter_data(values=values), image_path=image)
+
+
+def handle_video(video:str):
+ pass
+
+
+def get_image_meta_data(image_path):
+ image_extension = str(image_path).split("/")[-1].split(".")[1].upper()
+ # TODO: Sort out videos using mime type of file
+ # mime = MimeTypes(file_path=image_path)
+
+ if image_extension in picture_formats: return handle_image(image=image_path)
+ elif image_extension in video_formats: return handle_video(video=image_path)
+ elif image_extension in raw_formats: return handle_raw(image=image_path)
def filter_date_and_make(meta_tags:list, image_path):
- day = None
- month = None
- year = None
- time = None
- make = str(meta_tags[1]).split("|")[1]
+ make = meta_tags[0][1]
image_name = str(image_path).split("/")[-1]
- _date = str(meta_tags[0]).split("|")
- time = _date[1].split(" ")[1]
- _date = _date[1].split(" ")[0].split(":")
- day = int(_date[2])
- month = int(_date[1])
- year = int(_date[0])
+ date_time = meta_tags[1][1].split(" ")
+ date, time = date_time[0].split(":"), date_time[1]
+ year, month, day = date[0], date[1], date[2]
- return ExifData(image_path=image_path, image_name=image_name, day=day, month=month, year=year, time=time, make=make)
+ exif_data_dict = {
+ "day": day,
+ "month": month,
+ "year": year,
+ "make": make,
+ "time": time,
+ "image_path": image_path,
+ "image_name": image_name
+ }
+
+ return ExifData(exif_data_dict)
-def filter_data(value):
+def filter_data(values):
+ """Filters the data according to the meta tags from the keyword list"""
value_return = []
- for k in key_words:
- for v in value:
- temp = v.split("|")
- if temp[0] == k:
- value_return.append(v)
+ for value in values:
+ value_return.append(value) if value[0] in key_words else {}
return value_return
diff --git a/src/mime_types.py b/src/mime_types.py
new file mode 100644
index 0000000..c278da5
--- /dev/null
+++ b/src/mime_types.py
@@ -0,0 +1,18 @@
+import filetype
+
+
+class MimeTypes:
+ def __init__(self, file_path):
+ self.is_image = False
+ self.is_video = False
+ self.is_raw = False
+ self.is_unsupported_file_type = False
+ self.__proceed(file_path)
+
+ def __proceed(self, file_path):
+ if filetype.is_image(file_path):
+ self.is_image = True
+ elif filetype.is_video(file_path):
+ self.is_video = True
+ else:
+ self.is_unsupported_file_type = True
diff --git a/src/process.py b/src/process.py
index 5165a82..faa2418 100644
--- a/src/process.py
+++ b/src/process.py
@@ -1,21 +1,42 @@
+import os
import sys
+import i18n
sys.path.append("../")
+from time import time
from src.meta_data_handler import get_meta_data
from src.file_handler import sort_pictures
from src.scan_folder import recursive_scan_folder
from src.config import get_config
+workdir, filename = os.path.split(os.path.abspath(__file__))
+config = get_config()
+
+i18n.set('locale', config.language)
+i18n.set('fallback', 'en')
+i18n.set('filename_format', '{locale}.{format}')
+i18n.load_path.append(f'{workdir}{os.sep}i18n_translations{os.sep}')
+
def start_process(logger):
- config = get_config()
try:
files = recursive_scan_folder(config.src)
if len(files) > 0:
+ start_timer = time()
exif_data = get_meta_data(images=files)
+ image_total = len(exif_data)
+ print(i18n.t('start_sorting_images', image_count=image_total))
+
sort_pictures(images=exif_data, dst=config.dst, logger=logger)
+ end_timer = time()
+ duration = round(end_timer - start_timer, 2)
+
+ print(i18n.t('done'))
+ print(i18n.t('done_sorting_images', time=duration, image_count=image_total))
+ return True
else:
- print("No images found")
+ print(i18n.t('no_images_found'))
+ return False
except Exception as err:
print(err)
logger.error(err)
diff --git a/tests/helpers/folder_helper.py b/tests/helpers/folder_helper.py
index 609b1a7..536497f 100644
--- a/tests/helpers/folder_helper.py
+++ b/tests/helpers/folder_helper.py
@@ -5,7 +5,7 @@ from pathlib import Path
from src.scan_folder import recursive_scan_folder
TEST_FOLDER = ".test_folder"
-TEST_IMAGES = "tests/test_files"
+TEST_IMAGES = f"tests{os.sep}test_files"
TEST_TEMP_FOLDER = os.path.join(TEST_FOLDER, 'Temp')
TEST_IMAGE_FOLDER = os.path.join(TEST_FOLDER, 'Images')
@@ -37,7 +37,7 @@ def copy_images(brand:str, model:str):
create_folders()
files = recursive_scan_folder(path=TEST_IMAGES)
for file in files:
- file_name = file.split("/")[2:][0]
+ file_name = file.split(os.sep)[2:][0]
file_name = file_name.split("_")
if file_name[0] == brand and file_name[1] == model:
shutil.copy(src=file, dst=TEST_FOLDER)
diff --git a/tests/start_tests.sh b/tests/start_tests.sh
index 459ec60..c25bf8c 100755
--- a/tests/start_tests.sh
+++ b/tests/start_tests.sh
@@ -6,3 +6,5 @@ python3.12 -m pytest \
--cov-report xml:tests/coverage/coverage.xml \
--junitxml=tests/coverage/report.xml \
tests/
+
+exit $?
diff --git a/tests/start_tests_gitlab.sh b/tests/start_tests_gitlab.sh
index 7c6d6cf..1225c9e 100755
--- a/tests/start_tests_gitlab.sh
+++ b/tests/start_tests_gitlab.sh
@@ -1,9 +1,13 @@
apk add --update libmagic
-pip3.12 install -r requirements.txt
+pip install --upgrade pip
+python3 -m pip install -r requirements.txt
+python3 -m pip install -r develop_requirements.txt
pwd
sh tests/start_tests.sh
+exit_code=$?
cp tests/coverage/coverage.xml ./coverage.xml
cp tests/coverage/report.xml ./report.xml
-python3.12 tests/get_coverage_percent.py
+python3 tests/get_coverage_percent.py
+exit $exit_code
\ No newline at end of file
diff --git a/tests/test_config.py b/tests/test_config.py
index c17e3de..76a2ca7 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -7,15 +7,18 @@ class TestConfig(unittest.TestCase):
def test_config_class(self):
test_config = {
"src": "/src/src",
- "dst": "/src/dst"
+ "dst": "/src/dst",
+ "language": "en"
}
config = config_module.Config(test_config)
assert config.src == "/src/src"
assert config.dst == "/src/dst"
+ assert config.language == "en"
def test_get_config(self):
config_module.config_file = "src/config.yml"
config = config_module.get_config()
assert config.src == "../app/Temp"
assert config.dst == "../app/Bilder"
+ assert config.language == "en"
diff --git a/tests/test_exif_data.py b/tests/test_exif_data.py
index 52593b1..4810321 100644
--- a/tests/test_exif_data.py
+++ b/tests/test_exif_data.py
@@ -4,12 +4,18 @@ import unittest
class TestExifData(unittest.TestCase):
def test_exif_data(self):
- exif_data = ExifData(image_path="/path/to/image",
- image_name="Image.jpeg",
- make="CAMERA",
- month=12,
- day=12,
- year=2023,
- time="10:10:10")
+ exif_data_dict = {
+ "day": 2,
+ "month": 2,
+ "year": 2222,
+ "make": "CAMERA",
+ "time": "10:10:10",
+ "image_path": "/path/to/image",
+ "image_name": "Image.jpeg"
+ }
+ exif_data = ExifData(exif_data_dict)
assert exif_data.make == "CAMERA"
+ assert exif_data.year == 2222
+ assert exif_data.time == "10:10:10"
+
diff --git a/tests/test_files/test_video.mp4 b/tests/test_files/test_video.mp4
new file mode 100644
index 0000000..f2c1c8f
Binary files /dev/null and b/tests/test_files/test_video.mp4 differ
diff --git a/tests/test_mime_types.py b/tests/test_mime_types.py
new file mode 100644
index 0000000..4dc3992
--- /dev/null
+++ b/tests/test_mime_types.py
@@ -0,0 +1,24 @@
+import unittest
+
+from src.mime_types import MimeTypes
+
+
+class TestMimeTypes(unittest.TestCase):
+ def test_mime_type_image(self):
+ mime_type = MimeTypes(file_path="tests/test_files/test_image_001.JPG")
+ assert mime_type.is_image
+ assert not mime_type.is_video
+ assert not mime_type.is_unsupported_file_type
+
+ def test_mime_type_video(self):
+ mime_type = MimeTypes(file_path="tests/test_files/test_video.mp4")
+ assert not mime_type.is_image
+ assert mime_type.is_video
+ assert not mime_type.is_unsupported_file_type
+
+ def test_mime_type_unsupported_file_type(self):
+ mime_type = MimeTypes(file_path="tests/test_mime_types.py")
+ assert not mime_type.is_image
+ assert not mime_type.is_video
+ assert mime_type.is_unsupported_file_type
+
diff --git a/tests/tests/coverage/report.xml b/tests/tests/coverage/report.xml
new file mode 100644
index 0000000..4ef3cb0
--- /dev/null
+++ b/tests/tests/coverage/report.xml
@@ -0,0 +1 @@
+
\ No newline at end of file