From 81ec585651931a08444165c7a87aec3d72b543ea Mon Sep 17 00:00:00 2001 From: Andrey Nikiforov Date: Sat, 6 Jul 2024 09:46:56 -0700 Subject: [PATCH 1/2] add support for os locales --- .github/workflows/quality-checks.yml | 6 + CHANGELOG.md | 2 + src/icloudpd/base.py | 13 ++ tests/test_folder_structure.py | 193 +++++++++++++++++---------- 4 files changed, 146 insertions(+), 68 deletions(-) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index ac77adc93..4112b02d5 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -77,12 +77,18 @@ jobs: matrix: python-version: [3.8, 3.9, '3.10', 3.11, 3.12] steps: + - name: Install Locales for Tests + run: | + apt-get update && apt-get -y install locales locales-all + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Install Test dependencies run: | pip3 install --disable-pip-version-check -e .[test] diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c76b90d..b1305b9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- feature: support for using locale from OS with `--use-os-locale` flag [#897](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/897) + ## 1.21.0 (2024-07-05) - feature: add webui for entering password with `--password-provider webui` parameter [#805](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/805) diff --git a/src/icloudpd/base.py b/src/icloudpd/base.py index 600858626..e79108e9d 100644 --- a/src/icloudpd/base.py +++ b/src/icloudpd/base.py @@ -521,6 +521,12 @@ def file_match_policy_generator( show_default=True, callback=mfa_provider_generator, ) +@click.option( + "--use-os-locale", + help="Use locale of the host OS to format dates", + is_flag=True, + default=False, +) # a hacky way to get proper version because automatic detection does not # work for some reason @click.version_option(version="1.21.0") @@ -568,6 +574,7 @@ def main( ], file_match_policy: FileMatchPolicy, mfa_provider: MFAProvider, + use_os_locale: bool, ) -> NoReturn: """Download all iCloud photos to a local directory""" @@ -637,6 +644,12 @@ def main( update_password_status_in_webui(status_exchange), ) + # set locale + if use_os_locale: + from locale import LC_ALL, setlocale + + setlocale(LC_ALL, "") + # start web server if mfa_provider == MFAProvider.WEBUI: server_thread = Thread(target=serve_app, daemon=True, args=[logger, status_exchange]) diff --git a/tests/test_folder_structure.py b/tests/test_folder_structure.py index 04de3a1f9..10f499ee6 100644 --- a/tests/test_folder_structure.py +++ b/tests/test_folder_structure.py @@ -1,7 +1,7 @@ import glob import inspect import os -from typing import List +from typing import List, Tuple from unittest import TestCase import pytest @@ -9,7 +9,12 @@ from icloudpd.base import main from vcr import VCR -from tests.helpers import path_from_project_root, print_result_exception, recreate_path +from tests.helpers import ( + path_from_project_root, + print_result_exception, + recreate_path, + run_icloudpd_test, +) vcr = VCR(decode_compressed_response=True) @@ -24,7 +29,108 @@ def inject_fixtures(self, caplog: pytest.LogCaptureFixture) -> None: # This is basically a copy of the listing_recent_photos test # def test_default_folder_structure(self) -> None: - ### Tests if the default directory structure is constructed correctly ### + """Tests if the default directory structure is constructed correctly""" + base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) + + files_to_create: List[Tuple[str, str, int]] = [] + files_to_download: List[Tuple[str, str]] = [] + + # Note - This test uses the same cassette as test_download_photos.py + data_dir, result = run_icloudpd_test( + self.assertEqual, + self.vcr_path, + base_dir, + "listing_photos.yml", + files_to_create, + files_to_download, + [ + "--username", + "jdoe@gmail.com", + "--password", + "password1", + "--recent", + "5", + "--only-print-filenames", + "--no-progress-bar", + "--threads-num", + "1", + ], + ) + + assert result.exit_code == 0 + + filenames = result.output.splitlines() + + self.assertEqual(len(filenames), 8) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.JPG")), filenames[0] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.MOV")), filenames[1] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.JPG")), filenames[2] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.MOV")), filenames[3] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.JPG")), filenames[4] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.MOV")), filenames[5] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7405.MOV")), filenames[6] + ) + self.assertEqual( + os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7404.MOV")), filenames[7] + ) + + def test_folder_structure_none(self) -> None: + base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) + + files_to_create: List[Tuple[str, str, int]] = [] + files_to_download: List[Tuple[str, str]] = [] + + # Note - This test uses the same cassette as test_download_photos.py + data_dir, result = run_icloudpd_test( + self.assertEqual, + self.vcr_path, + base_dir, + "listing_photos.yml", + files_to_create, + files_to_download, + [ + "--username", + "jdoe@gmail.com", + "--password", + "password1", + "--recent", + "5", + "--only-print-filenames", + "--folder-structure=none", + "--no-progress-bar", + "--threads-num", + "1", + ], + ) + + assert result.exit_code == 0 + + filenames = result.output.splitlines() + + self.assertEqual(len(filenames), 8) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7409.JPG")), filenames[0]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7409.MOV")), filenames[1]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7408.JPG")), filenames[2]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7408.MOV")), filenames[3]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7407.JPG")), filenames[4]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7407.MOV")), filenames[5]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7405.MOV")), filenames[6]) + self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7404.MOV")), filenames[7]) + + def test_folder_structure_de(self) -> None: base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) cookie_dir = os.path.join(base_dir, "cookie") data_dir = os.path.join(base_dir, "data") @@ -37,7 +143,12 @@ def test_default_folder_structure(self) -> None: # Note - This test uses the same cassette as test_download_photos.py with vcr.use_cassette(os.path.join(self.vcr_path, "listing_photos.yml")): # Pass fixed client ID via environment variable - runner = CliRunner(env={"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"}) + runner = CliRunner( + env={ + "CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321", + "LANG": "de_DE.UTF-8", + } + ) result = runner.invoke( main, [ @@ -48,7 +159,9 @@ def test_default_folder_structure(self) -> None: "--recent", "5", "--only-print-filenames", + "--folder-structure={:%Y/%B}", "--no-progress-bar", + "--use-os-locale", "-d", data_dir, "--cookie-directory", @@ -60,28 +173,28 @@ def test_default_folder_structure(self) -> None: self.assertEqual(len(filenames), 8) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.JPG")), filenames[0] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7409.JPG")), filenames[0] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.MOV")), filenames[1] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7409.MOV")), filenames[1] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.JPG")), filenames[2] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7408.JPG")), filenames[2] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.MOV")), filenames[3] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7408.MOV")), filenames[3] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.JPG")), filenames[4] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7407.JPG")), filenames[4] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.MOV")), filenames[5] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7407.MOV")), filenames[5] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7405.MOV")), filenames[6] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7405.MOV")), filenames[6] ) self.assertEqual( - os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7404.MOV")), filenames[7] + os.path.join(data_dir, os.path.normpath("2018/Juli/IMG_7404.MOV")), filenames[7] ) assert result.exit_code == 0 @@ -94,59 +207,3 @@ def test_default_folder_structure(self) -> None: assert os.path.exists( os.path.join(data_dir, os.path.normpath(file_name)) ), f"File {file_name} expected, but does not exist" - - def test_folder_structure_none(self) -> None: - base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) - cookie_dir = os.path.join(base_dir, "cookie") - data_dir = os.path.join(base_dir, "data") - - for dir in [base_dir, cookie_dir, data_dir]: - recreate_path(dir) - - files_to_download: List[str] = [] - - # Note - This test uses the same cassette as test_download_photos.py - with vcr.use_cassette(os.path.join(self.vcr_path, "listing_photos.yml")): - # Pass fixed client ID via environment variable - runner = CliRunner(env={"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"}) - result = runner.invoke( - main, - [ - "--username", - "jdoe@gmail.com", - "--password", - "password1", - "--recent", - "5", - "--only-print-filenames", - "--folder-structure=none", - "--no-progress-bar", - "-d", - data_dir, - "--cookie-directory", - cookie_dir, - ], - ) - print_result_exception(result) - filenames = result.output.splitlines() - - self.assertEqual(len(filenames), 8) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7409.JPG")), filenames[0]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7409.MOV")), filenames[1]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7408.JPG")), filenames[2]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7408.MOV")), filenames[3]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7407.JPG")), filenames[4]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7407.MOV")), filenames[5]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7405.MOV")), filenames[6]) - self.assertEqual(os.path.join(data_dir, os.path.normpath("IMG_7404.MOV")), filenames[7]) - - assert result.exit_code == 0 - - files_in_result = glob.glob(os.path.join(data_dir, "**/*.*"), recursive=True) - - assert sum(1 for _ in files_in_result) == len(files_to_download) - - for file_name in files_to_download: - assert os.path.exists( - os.path.join(data_dir, os.path.normpath(file_name)) - ), f"File {file_name} expected, but does not exist" From 5c8f22478871ad436df1459f81ccf9dc4c6e5b43 Mon Sep 17 00:00:00 2001 From: Andrey Nikiforov Date: Sat, 6 Jul 2024 09:55:11 -0700 Subject: [PATCH 2/2] use sudo --- .github/workflows/quality-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 4112b02d5..d2672fc86 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -79,7 +79,7 @@ jobs: steps: - name: Install Locales for Tests run: | - apt-get update && apt-get -y install locales locales-all + sudo apt-get update && sudo apt-get -y install locales locales-all - uses: actions/checkout@v4