Merge branch 'develop' into 'main'
develop See merge request DasMoorhuhn/autopicture-v3!7
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
[run]
|
||||
source = src
|
||||
omit = tests/*, __init__.py, updater.py
|
||||
omit = tests/*, __init__.py, updater.py, main.py
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,5 +4,6 @@ app/
|
||||
.test_folder/
|
||||
*/coverage/
|
||||
*.log
|
||||
*.xml
|
||||
.coverage
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ pytest:
|
||||
image: python:3.12-alpine
|
||||
script:
|
||||
- sh tests/start_tests_gitlab.sh
|
||||
- sed -i "s#<source>/builds/DasMoorhuhn/autopicture-v3/src</source>#<source>${CI_PROJECT_DIR}</source>#g" coverage.xml
|
||||
# - sed -i "s#<source>/builds/DasMoorhuhn/autopicture-v3/src</source>#<source>${CI_PROJECT_DIR}</source>#g" coverage.xml
|
||||
coverage: '/Code coverage: \d+(?:\.\d+)?/'
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME"
|
||||
|
||||
@@ -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
|
||||
|
||||
26
README.md
26
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
|
||||
|
||||
4
develop_requirements.txt
Normal file
4
develop_requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-factoryboy
|
||||
gitlabci-local
|
||||
1
install.sh
Normal file
1
install.sh
Normal file
@@ -0,0 +1 @@
|
||||
sudo apt-get install -y python3 python3-pip
|
||||
@@ -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.*
|
||||
pillow
|
||||
pyyaml
|
||||
python-magic
|
||||
progressbar
|
||||
virtualenv
|
||||
requests
|
||||
filetype
|
||||
python-i18n[YAML]
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"date": "2023-11-30"
|
||||
"version": "0.2.0",
|
||||
"date": "2024-05-25"
|
||||
}
|
||||
@@ -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']
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
# src (source) is the path, were the images are, that should be sorted
|
||||
src: "../app/Temp"
|
||||
dst: "../app/Bilder"
|
||||
|
||||
# dst (destination) ist the path, were the images are going to be sorted
|
||||
dst: "../app/Bilder"
|
||||
|
||||
language: "en"
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
5
src/i18n_translations/de.yml
Normal file
5
src/i18n_translations/de.yml
Normal file
@@ -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'
|
||||
5
src/i18n_translations/en.yml
Normal file
5
src/i18n_translations/en.yml
Normal file
@@ -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'
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
18
src/mime_types.py
Normal file
18
src/mime_types.py
Normal file
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,3 +6,5 @@ python3.12 -m pytest \
|
||||
--cov-report xml:tests/coverage/coverage.xml \
|
||||
--junitxml=tests/coverage/report.xml \
|
||||
tests/
|
||||
|
||||
exit $?
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
BIN
tests/test_files/test_video.mp4
Normal file
BIN
tests/test_files/test_video.mp4
Normal file
Binary file not shown.
24
tests/test_mime_types.py
Normal file
24
tests/test_mime_types.py
Normal file
@@ -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
|
||||
|
||||
1
tests/tests/coverage/report.xml
Normal file
1
tests/tests/coverage/report.xml
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="0" time="0.024" timestamp="2024-03-17T01:13:57.781687" hostname="framework-13" /></testsuites>
|
||||
Reference in New Issue
Block a user