Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c283e0221a | |||
| c051e120fe | |||
| cc3f0ea89b | |||
| 161525fbb5 | |||
| c21167f941 | |||
| 892df0144e | |||
| aaf7060252 | |||
| 0660628647 | |||
| e6c59507c4 | |||
| f8383567fe | |||
| a869b60933 | |||
| 3c8974ee74 | |||
| cdf8948193 | |||
| f2fd803b57 | |||
| 033eb269f0 | |||
| 480b97059d | |||
| e5ed2e7319 | |||
| 0f22b70e06 | |||
| ffdf3f3779 | |||
| 4d8c0c1dda | |||
| 6bea0ee524 | |||
| 5d7ca9172b | |||
| 0f6601ecd9 | |||
| f32822c861 | |||
| 52b776d495 | |||
| 941aa141af | |||
| 3e0a40eee9 | |||
| c774b8428c | |||
| d440f3ff1d | |||
| 6b4e8df4a5 | |||
| a5160aa0c9 | |||
| 250f7b6554 | |||
| d67b6f5669 | |||
| 968c549bbe | |||
| 0cf66c5781 | |||
| fc9ce09e0f | |||
| c6bda88974 | |||
| 4544681f45 | |||
| 27c5cd5076 | |||
| 0012c52adf | |||
| 9ae71af1aa | |||
| 3ebe5bba7f | |||
| 529776bac6 | |||
| d9ac0072ac | |||
| 2da422abc5 | |||
| cb9e02f4a2 |
3
.coveragerc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[run]
|
||||||
|
source = src
|
||||||
|
omit = tests/*, __init__.py, updater.py, main.py
|
||||||
5
.gitignore
vendored
@@ -1,4 +1,9 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.idea/
|
.idea/
|
||||||
app/
|
app/
|
||||||
|
.test_folder/
|
||||||
|
*/coverage/
|
||||||
*.log
|
*.log
|
||||||
|
*.xml
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
|||||||
13
.gitlab-ci.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
coverage: '/Code coverage: \d+(?:\.\d+)?/'
|
||||||
|
artifacts:
|
||||||
|
name: "$CI_JOB_NAME"
|
||||||
|
reports:
|
||||||
|
junit: report.xml
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
@@ -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]
|
## v0.1.0 [2023-11-30]
|
||||||
- Added updater
|
- Added updater
|
||||||
- Added Changelog file
|
- Added Changelog file
|
||||||
|
|||||||
71
README.md
@@ -1,6 +1,75 @@
|
|||||||
# AutoPicture V3
|
# AutoPicture V3
|
||||||
|
|
||||||
Dies ist die dritte Version von AutoPicture.
|
Bildersortierprogramm geschrieben in Python3.12.
|
||||||
|
|
||||||
|
Example structure:
|
||||||
|
```bash
|
||||||
|
app/Bilder/
|
||||||
|
└── SONY
|
||||||
|
└── 2023
|
||||||
|
├── 10
|
||||||
|
│ ├── 21
|
||||||
|
│ │ ├── DSC02975.JPG
|
||||||
|
│ │ └── DSC02976.JPG
|
||||||
|
│ ├── 25
|
||||||
|
│ │ ├── DSC03030.JPG
|
||||||
|
│ │ ├── DSC03031.JPG
|
||||||
|
│ │ └── DSC03096.JPG
|
||||||
|
│ ├── 28
|
||||||
|
│ │ ├── DSC03097.JPG
|
||||||
|
│ │ ├── DSC03098.JPG
|
||||||
|
│ │ └── DSC03116.JPG
|
||||||
|
│ ├── 29
|
||||||
|
│ │ ├── DSC03117.JPG
|
||||||
|
│ │ ├── DSC03118.JPG
|
||||||
|
│ │ └── DSC03135.JPG
|
||||||
|
│ └── 30
|
||||||
|
│ ├── DSC03136.JPG
|
||||||
|
│ └── DSC03137.JPG
|
||||||
|
├── 11
|
||||||
|
│ ├── 16
|
||||||
|
│ │ └── DSC03144.JPG
|
||||||
|
│ ├── 17
|
||||||
|
│ │ ├── DSC03145.JPG
|
||||||
|
│ │ └── DSC03146.JPG
|
||||||
|
│ └── 28
|
||||||
|
│ ├── DSC03153.JPG
|
||||||
|
│ ├── DSC03154.JPG
|
||||||
|
│ └── DSC03155.JPG
|
||||||
|
├── ...
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
# Erste Schritte
|
# Erste Schritte
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sh ./tests/start_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
4
develop_requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-factoryboy
|
||||||
|
gitlabci-local
|
||||||
1
install.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sudo apt-get install -y python3 python3-pip
|
||||||
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
pythonpath = tests
|
||||||
|
addopts = --ignore-glob=**/tests/**
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
pillow
|
pillow
|
||||||
|
pyyaml
|
||||||
python-magic
|
python-magic
|
||||||
progressbar
|
progressbar
|
||||||
virtualenv
|
virtualenv
|
||||||
requests
|
requests
|
||||||
|
filetype
|
||||||
|
python-i18n[YAML]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"date": "2023-11-30"
|
"date": "2024-05-25"
|
||||||
}
|
}
|
||||||
0
src/__init__.py
Normal file
17
src/config.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
|
||||||
|
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||||
|
config_file = f"{workdir}{os.sep}config.yml"
|
||||||
|
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
with open(file=config_file, mode='r') as file:
|
||||||
|
return Config(yaml.safe_load(file))
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.src = data['src']
|
||||||
|
self.dst = data['dst']
|
||||||
|
self.language = data['language']
|
||||||
7
src/config.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# src (source) is the path, were the images are, that should be sorted
|
||||||
|
src: "../app/Temp"
|
||||||
|
|
||||||
|
# dst (destination) ist the path, were the images are going to be sorted
|
||||||
|
dst: "../app/Bilder"
|
||||||
|
|
||||||
|
language: "en"
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
class ExifData:
|
class ExifData:
|
||||||
"""This is for an object that stores the data of a picture"""
|
"""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:
|
def __init__(self, data:dict) -> None:
|
||||||
self.path = image_path
|
self.path:str = str(data['image_path'])
|
||||||
self.name = image_name
|
self.name:str = str(data['image_name'])
|
||||||
self.day = int(day)
|
self.day:int = int(data['day'])
|
||||||
self.month = int(month)
|
self.month:int = int(data['month'])
|
||||||
self.year = int(year)
|
self.year:int = int(data['year'])
|
||||||
self.time = str(time)
|
self.time = data['time']
|
||||||
self.make = str(make)
|
self.make:str = str(data['make'])
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
from progressbar.progressbar import ProgressBar
|
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):
|
def sort_pictures(images:list, dst:str, logger:logging.Logger):
|
||||||
image_total = len(images)
|
image_total = len(images)
|
||||||
image_counter = 0
|
image_counter = 0
|
||||||
logging_infos = []
|
|
||||||
progress_bar = ProgressBar(
|
progress_bar = ProgressBar(
|
||||||
maxval=image_total,
|
maxval=image_total,
|
||||||
term_width=70
|
term_width=70
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Start sorting {image_total} images\n")
|
|
||||||
progress_bar.start()
|
progress_bar.start()
|
||||||
start_timer = time.time()
|
|
||||||
for image in images:
|
for image in images:
|
||||||
image:ExifData
|
image:ExifData
|
||||||
|
if not image: continue
|
||||||
path = os.path.join(dst, str(image.make), str(image.year), str(image.month), str(image.day))
|
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)
|
if not os.path.exists(path): os.makedirs(path)
|
||||||
|
|
||||||
stat_info = os.stat(image.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)
|
# 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)
|
progress_bar.update(image_counter)
|
||||||
image_counter += 1
|
image_counter += 1
|
||||||
end_timer = time.time()
|
|
||||||
progress_bar.finish()
|
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
@@ -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
@@ -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'
|
||||||
26
src/main.py
@@ -1,14 +1,9 @@
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
from process import start_process
|
||||||
from meta_data_handler import MetadataHandler
|
|
||||||
from file_handler import sort_pictures
|
|
||||||
from folder_handler import *
|
|
||||||
|
|
||||||
sys.path.append("../")
|
sys.path.append("../")
|
||||||
log_folder = "."
|
log_folder = "."
|
||||||
src = "../app/TempPic"
|
|
||||||
dst = "../app/Bilder"
|
|
||||||
|
|
||||||
logger = logging.getLogger('AutoPicture')
|
logger = logging.getLogger('AutoPicture')
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
@@ -16,21 +11,4 @@ handler = logging.FileHandler(filename=f'{log_folder}/AutoPicture.log', encoding
|
|||||||
handler.setFormatter(logging.Formatter('%(asctime)s|:%(message)s'))
|
handler.setFormatter(logging.Formatter('%(asctime)s|:%(message)s'))
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
metadata_handler = MetadataHandler()
|
start_process(logger=logger)
|
||||||
|
|
||||||
|
|
||||||
def start_process():
|
|
||||||
try:
|
|
||||||
exif_data = metadata_handler.get_meta_data(images=files)
|
|
||||||
sort_pictures(images=exif_data, dst=dst, logger=logger)
|
|
||||||
except Exception as err:
|
|
||||||
print(err)
|
|
||||||
logger.error(err)
|
|
||||||
raise err
|
|
||||||
|
|
||||||
|
|
||||||
files = recursive_scan_dir(src)
|
|
||||||
if len(files) > 0:
|
|
||||||
start_process()
|
|
||||||
else:
|
|
||||||
print("No images found")
|
|
||||||
|
|||||||
@@ -1,63 +1,97 @@
|
|||||||
import magic
|
import magic
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import filetype
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from PIL import ExifTags
|
from PIL import ExifTags
|
||||||
|
|
||||||
from exif_data import ExifData
|
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"]
|
||||||
|
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"]
|
||||||
|
|
||||||
|
|
||||||
class MetadataHandler:
|
def is_file_video(path:str):
|
||||||
"""This class is for getting the meta data from a image or a video"""
|
|
||||||
|
|
||||||
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)
|
mime = magic.Magic(mime=True)
|
||||||
file = mime.from_file(path)
|
file = mime.from_file(path)
|
||||||
if file.find('video') != -1: return True
|
if file.find('video') != -1: return True
|
||||||
else: return False
|
else: return False
|
||||||
|
|
||||||
def __get_image_meta_data(self, image_path):
|
|
||||||
image_extension = str(image_path).split("/")[-1].split(".")
|
def is_file_picture(path:str):
|
||||||
# TODO: Sort out videos
|
mime = magic.Magic(mime=True)
|
||||||
img = Image.open(f"{image_path}")
|
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 = []
|
values = []
|
||||||
for tag, text in img.getexif().items():
|
for tag, text in img.getexif().items():
|
||||||
if tag in ExifTags.TAGS:
|
if tag in ExifTags.TAGS: values.append([ExifTags.TAGS[tag], str(text)])
|
||||||
if image_extension[1].upper() in self.__pictureFormats:
|
return filter_date_and_make(meta_tags=filter_data(values=values), image_path=image)
|
||||||
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]
|
|
||||||
|
|
||||||
_date = str(metaTags[0]).split("|")
|
def handle_video(video:str):
|
||||||
time = _date[1].split(" ")[1]
|
pass
|
||||||
_date = _date[1].split(" ")[0].split(":")
|
|
||||||
day = _date[2]
|
|
||||||
month = _date[1]
|
|
||||||
year = _date[0]
|
|
||||||
|
|
||||||
return ExifData(image_path=imagePath, image_name=image_name, day=day, month=month, year=year, time=time, make=make)
|
|
||||||
|
|
||||||
def __filter_data(self, value):
|
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):
|
||||||
|
make = meta_tags[0][1]
|
||||||
|
image_name = str(image_path).split("/")[-1]
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
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(values):
|
||||||
|
"""Filters the data according to the meta tags from the keyword list"""
|
||||||
value_return = []
|
value_return = []
|
||||||
for k in self.__keyWords:
|
for value in values:
|
||||||
for v in value:
|
value_return.append(value) if value[0] in key_words else {}
|
||||||
temp = v.split("|")
|
|
||||||
if temp[0] == k:
|
|
||||||
value_return.append(v)
|
|
||||||
return value_return
|
return value_return
|
||||||
|
|
||||||
def get_meta_data(self, images: list):
|
|
||||||
|
def get_meta_data(images: list):
|
||||||
exif_data_list = []
|
exif_data_list = []
|
||||||
for image in images:
|
for image in images:
|
||||||
exif_data_list.append(self.__get_image_meta_data(image_path=image))
|
exif_data_list.append(get_image_meta_data(image_path=image))
|
||||||
return exif_data_list
|
return exif_data_list
|
||||||
|
|||||||
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
|
||||||
43
src/process.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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):
|
||||||
|
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(i18n.t('no_images_found'))
|
||||||
|
return False
|
||||||
|
except Exception as err:
|
||||||
|
print(err)
|
||||||
|
logger.error(err)
|
||||||
|
raise err
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def scan_dir(path:str):
|
def scan_folder(path:str):
|
||||||
return next(os.walk(path))[2]
|
return next(os.walk(path))[2]
|
||||||
|
|
||||||
|
|
||||||
def recursive_scan_dir(path:str):
|
def recursive_scan_folder(path:str):
|
||||||
results = []
|
results = []
|
||||||
for root, folders, files in os.walk(path):
|
for root, folders, files in os.walk(path):
|
||||||
list_files = os.listdir(root)
|
list_files = os.listdir(root)
|
||||||
@@ -1,18 +1,69 @@
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# Proof of concept
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
def read_version():
|
||||||
with open(file=".version.json") as file:
|
with open(file=".version.json") as file:
|
||||||
version = json.load(file)
|
version = Version(json.load(file))
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def install():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def check_for_update():
|
def check_for_update():
|
||||||
request = "https://gitlab.com/DasMoorhuhn/autopicture-v3/-/raw/main/src/.version.json"
|
version_current = read_version()
|
||||||
response = requests.get(request)
|
request = "https://gitlab.com/api/v4/projects/52637155/releases"
|
||||||
|
response = requests.get(url=request, timeout=1)
|
||||||
if not response.ok: return
|
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()
|
check_for_update()
|
||||||
|
|||||||
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
17
tests/edit_source.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
with open(file="coverage.xml") as file:
|
||||||
|
file_content = file.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
for line in file_content:
|
||||||
|
try:
|
||||||
|
orig_line = file_content.index(line)
|
||||||
|
line = line.strip().split("<")[1].split(">")
|
||||||
|
if line[0] == "source":
|
||||||
|
print("FOUND")
|
||||||
|
file_content[orig_line] = "<source>src</source>\n"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
with open(file="coverage.xml", mode="w") as file:
|
||||||
|
file.writelines(file_content)
|
||||||
14
tests/get_coverage_percent.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
with open(file="coverage.xml") as file:
|
||||||
|
file_content = file.readlines()
|
||||||
|
|
||||||
|
line_rate = None
|
||||||
|
|
||||||
|
for line in file_content:
|
||||||
|
try:
|
||||||
|
line = line.strip().split("<")[1].split(">")[0].split(" ")
|
||||||
|
if line[0] == "package":
|
||||||
|
line_rate = "Code coverage: " + str(float(line[2].split("=\"")[1].split("\"")[0])*100) + "%"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(line_rate)
|
||||||
0
tests/helpers/__init__.py
Normal file
44
tests/helpers/folder_helper.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.scan_folder import recursive_scan_folder
|
||||||
|
|
||||||
|
TEST_FOLDER = ".test_folder"
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_images(brand:str, model:str):
|
||||||
|
delete_folder()
|
||||||
|
create_folders()
|
||||||
|
files = recursive_scan_folder(path=TEST_IMAGES)
|
||||||
|
for file in files:
|
||||||
|
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)
|
||||||
|
|
||||||
10
tests/start_tests.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
python3.12 -m pytest \
|
||||||
|
--no-header \
|
||||||
|
-rfp \
|
||||||
|
--cov \
|
||||||
|
--cov-report html:tests/coverage \
|
||||||
|
--cov-report xml:tests/coverage/coverage.xml \
|
||||||
|
--junitxml=tests/coverage/report.xml \
|
||||||
|
tests/
|
||||||
|
|
||||||
|
exit $?
|
||||||
13
tests/start_tests_gitlab.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
apk add --update libmagic
|
||||||
|
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 tests/get_coverage_percent.py
|
||||||
|
exit $exit_code
|
||||||
24
tests/test_config.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from src import config as config_module
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig(unittest.TestCase):
|
||||||
|
def test_config_class(self):
|
||||||
|
test_config = {
|
||||||
|
"src": "/src/src",
|
||||||
|
"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"
|
||||||
21
tests/test_exif_data.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from src.exif_data import ExifData
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TestExifData(unittest.TestCase):
|
||||||
|
def test_exif_data(self):
|
||||||
|
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"
|
||||||
|
|
||||||
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/iphone_x_001.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/test_files/iphone_x_002.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/test_files/iphone_x_003.jpeg
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
tests/test_files/iphone_x_004.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/test_files/iphone_x_005.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/test_files/iphone_x_006.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/test_files/samsung_a54_001.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
tests/test_files/samsung_a54_002.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_003.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/test_files/samsung_a54_004.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_005.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_006.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_007.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/test_files/test_image_001.JPG
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/test_files/test_image_002.JPG
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/test_files/test_video.mp4
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)
|
||||||
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
|
||||||
|
|
||||||
14
tests/test_process.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from src import config
|
||||||
|
from src.process import start_process
|
||||||
|
|
||||||
|
config.config_file = "src/config.yml"
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcess(unittest.TestCase):
|
||||||
|
def test_process(self):
|
||||||
|
start_process(logger=Mock())
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
52
tests/test_with_real_data.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from helpers.folder_helper import TEST_FOLDER
|
||||||
|
from helpers.folder_helper import copy_images
|
||||||
|
|
||||||
|
from src.meta_data_handler import get_meta_data
|
||||||
|
from src.scan_folder import recursive_scan_folder
|
||||||
|
|
||||||
|
|
||||||
|
class TestSamsung(unittest.TestCase):
|
||||||
|
def test_a54(self):
|
||||||
|
copy_images(brand="samsung", model="a54")
|
||||||
|
files = recursive_scan_folder(path=TEST_FOLDER)
|
||||||
|
meta_data = get_meta_data(images=files)
|
||||||
|
for image in meta_data:
|
||||||
|
assert image.make == "samsung"
|
||||||
|
|
||||||
|
image = next((image for image in meta_data if image.name == "samsung_a54_001.jpg"), None)
|
||||||
|
assert image.day == 2
|
||||||
|
assert image.month == 12
|
||||||
|
assert image.year == 2023
|
||||||
|
|
||||||
|
image = next((image for image in meta_data if image.name == "samsung_a54_003.jpg"), None)
|
||||||
|
assert image.day == 8
|
||||||
|
assert image.month == 12
|
||||||
|
assert image.year == 2023
|
||||||
|
|
||||||
|
@unittest.skip("")
|
||||||
|
def test_a52s(self):
|
||||||
|
copy_images(brand="samsung", model="a52s")
|
||||||
|
files = recursive_scan_folder(path=TEST_FOLDER)
|
||||||
|
meta_data = get_meta_data(images=files)
|
||||||
|
for image in meta_data:
|
||||||
|
assert image.make == "samsung"
|
||||||
|
|
||||||
|
@unittest.skip("")
|
||||||
|
def test_a14(self):
|
||||||
|
copy_images(brand="samsung", model="a14")
|
||||||
|
files = recursive_scan_folder(path=TEST_FOLDER)
|
||||||
|
meta_data = get_meta_data(images=files)
|
||||||
|
for image in meta_data:
|
||||||
|
assert image.make == "samsung"
|
||||||
|
|
||||||
|
|
||||||
|
class TestApple(unittest.TestCase):
|
||||||
|
def test_iphone_x(self):
|
||||||
|
copy_images(brand="iphone", model="x")
|
||||||
|
files = recursive_scan_folder(path=TEST_FOLDER)
|
||||||
|
meta_data = get_meta_data(images=files)
|
||||||
|
for image in meta_data:
|
||||||
|
assert image.make == "Apple"
|
||||||
|
|
||||||
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>
|
||||||