Repository: mandarons/icloud-drive-docker (python)

Issue:
[FEATURE] photos extension filter
I've tried the file extention filter on the photos part, but it doesn't seem to work.  Could it be extended to that also as my wife takes a billion screenshot of "stuff" and I don't want to waste storage on these?  Basically I want to filter out the png files.

Thanks so much for this project, it is awesome.

Cheers.

Pull request description:
Feature: photo extension filters in config
Fixes #90

Patch:
diff --git a/README.md b/README.md
index 346e9fd6..514680e1 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 [![GitHub Sponsors][github-sponsors-badge]][github-sponsors]
 <a href="https://www.buymeacoffee.com/mandarons" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 30px !important;width: 150px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
 
-:love_you_gesture: ***Please star this repository if you end up using this project. If it has improved your life in any way, consider donating for my effort using 'Buy Me a Coffee' button above or sponsoring this project using GitHub Sponsors. It will help me continue supporting this product.*** :pray:
+:love*you_gesture: \*\*\_Please star this repository if you end up using this project. If it has improved your life in any way, consider donating for my effort using 'Buy Me a Coffee' button above or sponsoring this project using GitHub Sponsors. It will help me continue supporting this product.*\*\* :pray:
 
 iCloud-docker (previously known as iCloud-drive-docker) is a simple iCloud client in Docker environment. It uses [iCloudPy](https://github.com/mandarons/icloudpy) python library to interact with iCloud server.
 
diff --git a/src/config_parser.py b/src/config_parser.py
index 220b9597..cb420be8 100644
--- a/src/config_parser.py
+++ b/src/config_parser.py
@@ -261,7 +261,7 @@ def get_photos_remove_obsolete(config):
 
 def get_photos_filters(config):
     """Return photos filters from config."""
-    photos_filters = {"albums": None, "file_sizes": ["original"]}
+    photos_filters = {"albums": None, "file_sizes": ["original"], "extensions": None}
     valid_file_sizes = ["original", "medium", "thumb"]
     config_path = ["photos", "filters"]
     if not traverse_config_path(config=config, config_path=config_path):
@@ -282,6 +282,7 @@ def get_photos_filters(config):
             photos_filters["albums"] = get_config_value(
                 config=config, config_path=config_path
             )
+
         config_path[2] = "file_sizes"
         if not traverse_config_path(config=config, config_path=config_path):
             LOGGER.warning(
@@ -299,6 +300,21 @@ def get_photos_filters(config):
                     if len(file_sizes) == 0:
                         file_sizes = ["original"]
             photos_filters["file_sizes"] = file_sizes
+
+        config_path[2] = "extensions"
+        if (
+            not traverse_config_path(config=config, config_path=config_path)
+            or not get_config_value(config=config, config_path=config_path)
+            or len(get_config_value(config=config, config_path=config_path)) == 0
+        ):
+            LOGGER.warning(
+                f"{config_path_to_string(config_path=config_path)} not found. Downloading all extensions ..."
+            )
+        else:
+            photos_filters["extensions"] = get_config_value(
+                config=config, config_path=config_path
+            )
+
     return photos_filters
 
 
diff --git a/src/sync_photos.py b/src/sync_photos.py
index f5d915af..104bd552 100644
--- a/src/sync_photos.py
+++ b/src/sync_photos.py
@@ -10,6 +10,16 @@ from icloudpy import exceptions
 from src import LOGGER, config_parser
 
 
+def photo_wanted(photo, extensions):
+    """Check if photo is wanted based on extension."""
+    if not extensions or len(extensions) == 0:
+        return True
+    for extension in extensions:
+        if photo.filename.lower().endswith(str(extension).lower()):
+            return True
+    return False
+
+
 def generate_file_name(photo, file_size, destination_path):
     """Generate full path to file."""
     filename = photo.filename
@@ -77,14 +87,18 @@ def process_photo(photo, file_size, destination_path):
     return True
 
 
-def sync_album(album, destination_path, file_sizes):
+def sync_album(album, destination_path, file_sizes, extensions=None):
     """Sync given album."""
     if not (album and destination_path and file_sizes):
         return None
     os.makedirs(destination_path, exist_ok=True)
     for photo in album:
-        for file_size in file_sizes:
-            process_photo(photo, file_size, destination_path)
+        if photo_wanted(photo, extensions):
+            for file_size in file_sizes:
+                process_photo(photo, file_size, destination_path)
+        else:
+            LOGGER.debug(f"Skipping the unwanted photo {photo.filename}.")
+    return True
 
 
 def sync_photos(config, photos):
@@ -97,12 +111,14 @@ def sync_photos(config, photos):
                 album=photos.albums[album],
                 destination_path=os.path.join(destination_path, album),
                 file_sizes=filters["file_sizes"],
+                extensions=filters["extensions"],
             )
     else:
         sync_album(
             album=photos.all,
             destination_path=os.path.join(destination_path, "all"),
             file_sizes=filters["file_sizes"],
+            extensions=filters["extensions"],
         )
 
 


Test patch:
diff --git a/.github/workflows/ci-main-test-coverage-deploy.yml b/.github/workflows/ci-main-test-coverage-deploy.yml
index cc29f3ca..074f1ff5 100644
--- a/.github/workflows/ci-main-test-coverage-deploy.yml
+++ b/.github/workflows/ci-main-test-coverage-deploy.yml
@@ -115,8 +115,8 @@ jobs:
         uses: actions/download-artifact@v3
         with:
           name: test-output.zip
-#       - name: Setup tmate session
-#         uses: mxschmitt/action-tmate@v3
+      #       - name: Setup tmate session
+      #         uses: mxschmitt/action-tmate@v3
       - name: Checkout gh-pages
         uses: actions/checkout@v3
         if: always()
diff --git a/tests/test_sync_photos.py b/tests/test_sync_photos.py
index 49bfd489..af4bd186 100644
--- a/tests/test_sync_photos.py
+++ b/tests/test_sync_photos.py
@@ -398,3 +398,63 @@ class TestSyncPhotos(unittest.TestCase):
                 destination_path=self.destination_path,
             )
         )
+
+    @patch(target="keyring.get_password", return_value=data.VALID_PASSWORD)
+    @patch(
+        target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER
+    )
+    @patch("icloudpy.ICloudPyService")
+    @patch("src.read_config")
+    def test_photo_wanted_extensions_JPG(
+        self, mock_read_config, mock_service, mock_get_username, mock_get_password
+    ):
+        """Test for JPG extension filter."""
+        mock_service = self.service
+        config = self.config.copy()
+        config["photos"]["destination"] = self.destination_path
+        config["photos"]["filters"]["extensions"] = ["JpG"]
+        mock_read_config.return_value = config
+        # Sync original photos
+        self.assertIsNone(
+            sync_photos.sync_photos(config=config, photos=mock_service.photos)
+        )
+        album_0_path = os.path.join(
+            self.destination_path, config["photos"]["filters"]["albums"][0]
+        )
+        album_1_path = os.path.join(
+            self.destination_path, config["photos"]["filters"]["albums"][1]
+        )
+        self.assertTrue(os.path.isdir(album_0_path))
+        self.assertTrue(os.path.isdir(album_1_path))
+        self.assertTrue(len(os.listdir(album_0_path)) > 0)
+        self.assertTrue(len(os.listdir(album_1_path)) > 0)
+
+    @patch(target="keyring.get_password", return_value=data.VALID_PASSWORD)
+    @patch(
+        target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER
+    )
+    @patch("icloudpy.ICloudPyService")
+    @patch("src.read_config")
+    def test_photo_wanted_extensions_PNG(
+        self, mock_read_config, mock_service, mock_get_username, mock_get_password
+    ):
+        """Test for PNG extension filter."""
+        mock_service = self.service
+        config = self.config.copy()
+        config["photos"]["destination"] = self.destination_path
+        config["photos"]["filters"]["extensions"] = ["PnG"]
+        mock_read_config.return_value = config
+        # Sync original photos
+        self.assertIsNone(
+            sync_photos.sync_photos(config=config, photos=mock_service.photos)
+        )
+        album_0_path = os.path.join(
+            self.destination_path, config["photos"]["filters"]["albums"][0]
+        )
+        album_1_path = os.path.join(
+            self.destination_path, config["photos"]["filters"]["albums"][1]
+        )
+        self.assertTrue(os.path.isdir(album_0_path))
+        self.assertTrue(os.path.isdir(album_1_path))
+        self.assertTrue(len(os.listdir(album_0_path)) == 0)
+        self.assertTrue(len(os.listdir(album_1_path)) == 0)