Merge branch 'develop' into 'main'
Develop See merge request DasMoorhuhn/autopicture-v3!2
This commit is contained in:
3
.coveragerc
Normal file
3
.coveragerc
Normal file
@@ -0,0 +1,3 @@
|
||||
[run]
|
||||
source = src
|
||||
omit = tests/*, __init__.py
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
__pycache__/
|
||||
.idea/
|
||||
app/
|
||||
*.log
|
||||
*/coverage/
|
||||
*.log
|
||||
.coverage
|
||||
|
||||
@@ -4,3 +4,9 @@ Dies ist die dritte Version von AutoPicture.
|
||||
|
||||
# Erste Schritte
|
||||
|
||||
# Tests
|
||||
|
||||
```bash
|
||||
sh ./tests/start_tests.sh
|
||||
```
|
||||
|
||||
|
||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
pythonpath = tests
|
||||
addopts = --ignore-glob=**/tests/**
|
||||
@@ -2,4 +2,7 @@ pillow
|
||||
python-magic
|
||||
progressbar
|
||||
virtualenv
|
||||
requests
|
||||
requests
|
||||
pytest==7.4.*
|
||||
pytest-cov==4.1.*
|
||||
pytest-factoryboy==2.5.*
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
@@ -1,10 +1,10 @@
|
||||
class ExifData:
|
||||
"""This is for an object that stores the data of a picture"""
|
||||
def __init__(self, image_path, image_name, day, month, year, time, make) -> None:
|
||||
self.path = image_path
|
||||
self.name = image_name
|
||||
self.day = int(day)
|
||||
self.month = int(month)
|
||||
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)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import os
|
||||
import sys
|
||||
sys.path.append("../")
|
||||
import time
|
||||
import shutil
|
||||
import logging
|
||||
from progressbar.progressbar import ProgressBar
|
||||
|
||||
from exif_data import ExifData
|
||||
from src.exif_data import ExifData
|
||||
|
||||
|
||||
def sort_pictures(images:list, dst:str, logger:logging.Logger):
|
||||
|
||||
10
src/main.py
10
src/main.py
@@ -1,9 +1,9 @@
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from meta_data_handler import MetadataHandler
|
||||
from meta_data_handler import get_meta_data
|
||||
from file_handler import sort_pictures
|
||||
from folder_handler import *
|
||||
from scan_folder import *
|
||||
|
||||
sys.path.append("../")
|
||||
log_folder = "."
|
||||
@@ -16,12 +16,10 @@ handler = logging.FileHandler(filename=f'{log_folder}/AutoPicture.log', encoding
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s|:%(message)s'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
metadata_handler = MetadataHandler()
|
||||
|
||||
|
||||
def start_process():
|
||||
try:
|
||||
exif_data = metadata_handler.get_meta_data(images=files)
|
||||
exif_data = get_meta_data(images=files)
|
||||
sort_pictures(images=exif_data, dst=dst, logger=logger)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
@@ -29,7 +27,7 @@ def start_process():
|
||||
raise err
|
||||
|
||||
|
||||
files = recursive_scan_dir(src)
|
||||
files = recursive_scan_folder(src)
|
||||
if len(files) > 0:
|
||||
start_process()
|
||||
else:
|
||||
|
||||
@@ -1,63 +1,66 @@
|
||||
import magic
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ExifTags
|
||||
|
||||
from exif_data import ExifData
|
||||
sys.path.append("../")
|
||||
from src.exif_data import ExifData
|
||||
|
||||
video_formats = ["MP4", "MOV", "M4V", "MKV", "AVI", "WMV", "AVCHD", "WEBM", "MPEG"]
|
||||
picture_formats = ["JPG", "JPEG", "PNG", "TIFF"]
|
||||
key_words = ["DateTime", "Make"]
|
||||
|
||||
|
||||
class MetadataHandler:
|
||||
"""This class is for getting the meta data from a image or a video"""
|
||||
def is_file_video(path:str):
|
||||
mime = magic.Magic(mime=True)
|
||||
file = mime.from_file(path)
|
||||
if file.find('video') != -1: return True
|
||||
else: return False
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__videoFormats = ["MP4", "MOV", "M4V", "MKV", "AVI", "WMV", "AVCHD", "WEBM", "MPEG"]
|
||||
self.__pictureFormats = ["JPG", "JPEG", "PNG", "TIFF"]
|
||||
self.__keyWords = ["DateTime", "Make"]
|
||||
|
||||
def __is_file_video(self, path:str):
|
||||
mime = magic.Magic(mime=True)
|
||||
file = mime.from_file(path)
|
||||
if file.find('video') != -1: return True
|
||||
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}")
|
||||
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)
|
||||
|
||||
def __get_image_meta_data(self, image_path):
|
||||
image_extension = str(image_path).split("/")[-1].split(".")
|
||||
# TODO: Sort out videos
|
||||
img = Image.open(f"{image_path}")
|
||||
values = []
|
||||
for tag, text in img.getexif().items():
|
||||
if tag in ExifTags.TAGS:
|
||||
if image_extension[1].upper() in self.__pictureFormats:
|
||||
values.append(ExifTags.TAGS[tag] + "|" + str(text))
|
||||
return self.__filter_date_and_make(metaTags=self.__filter_data(value=values), imagePath=image_path)
|
||||
|
||||
def __filter_date_and_make(self, metaTags: list, imagePath):
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
time = None
|
||||
make = str(metaTags[1]).split("|")[1]
|
||||
image_name = str(imagePath).split("/")[-1]
|
||||
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]
|
||||
image_name = str(image_path).split("/")[-1]
|
||||
|
||||
_date = str(metaTags[0]).split("|")
|
||||
time = _date[1].split(" ")[1]
|
||||
_date = _date[1].split(" ")[0].split(":")
|
||||
day = _date[2]
|
||||
month = _date[1]
|
||||
year = _date[0]
|
||||
_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])
|
||||
|
||||
return ExifData(image_path=imagePath, image_name=image_name, day=day, month=month, year=year, time=time, make=make)
|
||||
return ExifData(image_path=image_path, image_name=image_name, day=day, month=month, year=year, time=time, make=make)
|
||||
|
||||
def __filter_data(self, value):
|
||||
value_return = []
|
||||
for k in self.__keyWords:
|
||||
for v in value:
|
||||
temp = v.split("|")
|
||||
if temp[0] == k:
|
||||
value_return.append(v)
|
||||
return value_return
|
||||
|
||||
def get_meta_data(self, images: list):
|
||||
exif_data_list = []
|
||||
for image in images:
|
||||
exif_data_list.append(self.__get_image_meta_data(image_path=image))
|
||||
return exif_data_list
|
||||
def filter_data(value):
|
||||
value_return = []
|
||||
for k in key_words:
|
||||
for v in value:
|
||||
temp = v.split("|")
|
||||
if temp[0] == k:
|
||||
value_return.append(v)
|
||||
return value_return
|
||||
|
||||
|
||||
def get_meta_data(images: list):
|
||||
exif_data_list = []
|
||||
for image in images:
|
||||
exif_data_list.append(get_image_meta_data(image_path=image))
|
||||
return exif_data_list
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
|
||||
|
||||
def scan_dir(path:str):
|
||||
def scan_folder(path:str):
|
||||
return next(os.walk(path))[2]
|
||||
|
||||
|
||||
def recursive_scan_dir(path:str):
|
||||
def recursive_scan_folder(path:str):
|
||||
results = []
|
||||
for root, folders, files in os.walk(path):
|
||||
list_files = os.listdir(root)
|
||||
@@ -2,17 +2,66 @@ import requests
|
||||
import json
|
||||
|
||||
|
||||
class Version:
|
||||
def __init__(self, data:dict):
|
||||
self.version:str = data['version']
|
||||
self.version_int:int = int(self.version.replace(".", ""))
|
||||
self.date:str = data['date']
|
||||
|
||||
|
||||
class Release:
|
||||
def __init__(self, data:dict):
|
||||
self.name = data['name']
|
||||
self.tag_name = data['tag_name']
|
||||
self.description = data['description']
|
||||
self.created_at = data['created_at']
|
||||
self.released_at = data['released_at']
|
||||
self.upcoming_release = data['upcoming_release']
|
||||
self.version_int = int(self.tag_name.replace(".", ""))
|
||||
self.zip_file_url = data['assets']['sources']
|
||||
self.__proceed()
|
||||
|
||||
def __proceed(self):
|
||||
for assest in self.zip_file_url:
|
||||
if assest['format'] == 'zip':
|
||||
self.zip_file_url = assest['url']
|
||||
break
|
||||
|
||||
|
||||
def read_version():
|
||||
with open(file=".version.json") as file:
|
||||
version = json.load(file)
|
||||
version = Version(json.load(file))
|
||||
return version
|
||||
|
||||
|
||||
def install():
|
||||
pass
|
||||
|
||||
|
||||
def check_for_update():
|
||||
request = "https://gitlab.com/DasMoorhuhn/autopicture-v3/-/raw/main/src/.version.json"
|
||||
response = requests.get(request)
|
||||
version_current = read_version()
|
||||
request = "https://gitlab.com/api/v4/projects/52637155/releases"
|
||||
response = requests.get(url=request, timeout=1)
|
||||
if not response.ok: return
|
||||
print(response.text)
|
||||
|
||||
# Get the latest release
|
||||
releases_json = json.loads(response.text)
|
||||
# index version
|
||||
latest_release = [0, 0]
|
||||
for release_json in releases_json:
|
||||
release = Release(release_json)
|
||||
if release.version_int > version_current.version_int:
|
||||
latest_release[0] = releases_json.index(release_json)
|
||||
latest_release[1] = release.version_int
|
||||
|
||||
if latest_release == [0, 0]: return
|
||||
release = Release(releases_json[latest_release[0]])
|
||||
print(f"v{version_current.version} -> v{release.tag_name}")
|
||||
|
||||
if release.version_int > version_current.version_int:
|
||||
# Update
|
||||
print("Update")
|
||||
print(release.zip_file_url)
|
||||
|
||||
|
||||
check_for_update()
|
||||
|
||||
30
test.gitlab-ci.yml
Normal file
30
test.gitlab-ci.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
prepare_job:
|
||||
stage: prepare # This stage must run before the release stage
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: never # Do not run this job when a tag is created manually
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
|
||||
script:
|
||||
- echo "EXTRA_DESCRIPTION=some message" >> variables.env # Generate the EXTRA_DESCRIPTION and TAG environment variables
|
||||
- echo "TAG=v$(cat VERSION)" >> variables.env # and append to the variables.env file
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: variables.env # Use artifacts:reports:dotenv to expose the variables to other jobs
|
||||
|
||||
release_job:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
needs:
|
||||
- job: prepare_job
|
||||
artifacts: true
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: never # Do not run this job when a tag is created manually
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
|
||||
script:
|
||||
- echo "running release_job for $TAG"
|
||||
release:
|
||||
name: 'Release $TAG'
|
||||
description: 'Created using the release-cli $EXTRA_DESCRIPTION' # $EXTRA_DESCRIPTION and the $TAG
|
||||
tag_name: '$TAG' # variables must be defined elsewhere
|
||||
ref: '$CI_COMMIT_SHA' # in the pipeline. For example, in the
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/helpers/__init__.py
Normal file
0
tests/helpers/__init__.py
Normal file
30
tests/helpers/folder_helper.py
Normal file
30
tests/helpers/folder_helper.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
TEST_FOLDER = ".test_folder"
|
||||
TEST_IMAGES = "tests/test_files"
|
||||
TEST_TEMP_FOLDER = os.path.join(TEST_FOLDER, 'Temp')
|
||||
TEST_IMAGE_FOLDER = os.path.join(TEST_FOLDER, 'Images')
|
||||
|
||||
|
||||
def create_file(file):
|
||||
Path(os.path.join(TEST_FOLDER, file)).touch()
|
||||
|
||||
|
||||
def delete_folder():
|
||||
shutil.rmtree(TEST_FOLDER, ignore_errors=True)
|
||||
|
||||
|
||||
def create_folders():
|
||||
delete_folder()
|
||||
os.makedirs(os.path.join(TEST_FOLDER, '001', '001'))
|
||||
os.makedirs(os.path.join(TEST_FOLDER, '002', '001'))
|
||||
|
||||
|
||||
def copy_test_images():
|
||||
delete_folder()
|
||||
os.makedirs(TEST_TEMP_FOLDER)
|
||||
os.makedirs(TEST_IMAGE_FOLDER)
|
||||
shutil.copy(src=os.path.join(TEST_IMAGES, 'test_image_001.JPG'), dst=TEST_TEMP_FOLDER)
|
||||
shutil.copy(src=os.path.join(TEST_IMAGES, 'test_image_002.JPG'), dst=TEST_TEMP_FOLDER)
|
||||
1
tests/start_tests.sh
Executable file
1
tests/start_tests.sh
Executable file
@@ -0,0 +1 @@
|
||||
python3.10 -m pytest --no-header -rfp --cov --cov-report html:tests/coverage --cov-report xml:tests/coverage/coverage.xml tests/
|
||||
15
tests/test_exif_data.py
Normal file
15
tests/test_exif_data.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from src.exif_data import ExifData
|
||||
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")
|
||||
|
||||
assert exif_data.make == "CAMERA"
|
||||
25
tests/test_file_handle.py
Normal file
25
tests/test_file_handle.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from helpers.folder_helper import TEST_FOLDER
|
||||
from helpers.folder_helper import TEST_IMAGE_FOLDER
|
||||
from helpers.folder_helper import delete_folder
|
||||
from helpers.folder_helper import copy_test_images
|
||||
|
||||
from src.file_handler import sort_pictures
|
||||
from src.scan_folder import recursive_scan_folder
|
||||
from src.meta_data_handler import get_meta_data
|
||||
|
||||
|
||||
class TestFileHandler(unittest.TestCase):
|
||||
def test_file_handler(self):
|
||||
copy_test_images()
|
||||
files = recursive_scan_folder(TEST_FOLDER)
|
||||
exif_data = get_meta_data(files)
|
||||
sort_pictures(images=exif_data, logger=Mock(), dst=TEST_IMAGE_FOLDER)
|
||||
sorted_pictures = recursive_scan_folder(TEST_FOLDER)
|
||||
delete_folder()
|
||||
|
||||
assert len(sorted_pictures) == 2
|
||||
assert "".join(picture for picture in sorted_pictures if '2023/10/25/test_image_002.JPG' in picture) != ""
|
||||
assert "".join(picture for picture in sorted_pictures if '2023/10/28/test_image_001.JPG' in picture) != ""
|
||||
BIN
tests/test_files/test_image_001.JPG
Normal file
BIN
tests/test_files/test_image_001.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/test_files/test_image_002.JPG
Normal file
BIN
tests/test_files/test_image_002.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
23
tests/test_meta_data_handler.py
Normal file
23
tests/test_meta_data_handler.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from src.meta_data_handler import get_meta_data
|
||||
from src.meta_data_handler import get_image_meta_data
|
||||
from src.meta_data_handler import filter_data
|
||||
from src.meta_data_handler import filter_date_and_make
|
||||
from src.meta_data_handler import is_file_video
|
||||
from src.scan_folder import recursive_scan_folder
|
||||
|
||||
TEST_IMAGES = "tests/test_files"
|
||||
|
||||
|
||||
class TestMetadataHandler(unittest.TestCase):
|
||||
def test_get_image_meta_data(self):
|
||||
image_exif_data = get_image_meta_data(image_path=f"{TEST_IMAGES}/test_image_001.JPG")
|
||||
assert image_exif_data.name == "test_image_001.JPG"
|
||||
|
||||
def test_get_meta_data(self):
|
||||
images = recursive_scan_folder(TEST_IMAGES)
|
||||
print(images)
|
||||
exif_data = get_meta_data(images)
|
||||
assert len(exif_data) == len(images)
|
||||
46
tests/test_scan_dir.py
Normal file
46
tests/test_scan_dir.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import unittest
|
||||
|
||||
from helpers.folder_helper import delete_folder
|
||||
from helpers.folder_helper import create_folders
|
||||
from helpers.folder_helper import create_file
|
||||
from helpers.folder_helper import TEST_FOLDER
|
||||
|
||||
from src.scan_folder import scan_folder
|
||||
from src.scan_folder import recursive_scan_folder
|
||||
|
||||
|
||||
class TestScanFolder(unittest.TestCase):
|
||||
def test_scan_folder(self):
|
||||
create_folders()
|
||||
create_file(file='img_01.jpeg')
|
||||
files = scan_folder(TEST_FOLDER)
|
||||
delete_folder()
|
||||
|
||||
assert len(files) == 1
|
||||
|
||||
def test_scan_empty_folder(self):
|
||||
create_folders()
|
||||
files = scan_folder(TEST_FOLDER)
|
||||
delete_folder()
|
||||
|
||||
assert len(files) == 0
|
||||
|
||||
def test_scan_recursive_folder(self):
|
||||
create_folders()
|
||||
create_file(file='img_01.jpeg')
|
||||
create_file(file='img_02.jpeg')
|
||||
create_file(file='001/img_03.jpeg')
|
||||
create_file(file='001/001/img_04.jpeg')
|
||||
create_file(file='002/001/img_05.jpeg')
|
||||
|
||||
files = recursive_scan_folder(TEST_FOLDER)
|
||||
delete_folder()
|
||||
|
||||
assert len(files) == 5
|
||||
|
||||
def test_scan_recursive_empty_folder(self):
|
||||
create_folders()
|
||||
files = recursive_scan_folder(TEST_FOLDER)
|
||||
delete_folder()
|
||||
|
||||
assert len(files) == 0
|
||||
Reference in New Issue
Block a user