audiometa-python 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- audiometa/__init__.py +1240 -0
- audiometa/__main__.py +6 -0
- audiometa/_audio_file.py +602 -0
- audiometa/cli.py +347 -0
- audiometa/exceptions.py +167 -0
- audiometa/manager/_MetadataManager.py +665 -0
- audiometa/manager/__init__.py +1 -0
- audiometa/manager/_rating_supporting/_RatingSupportingMetadataManager.py +250 -0
- audiometa/manager/_rating_supporting/__init__.py +1 -0
- audiometa/manager/_rating_supporting/id3v2/_Id3v2Manager.py +945 -0
- audiometa/manager/_rating_supporting/id3v2/__init__.py +25 -0
- audiometa/manager/_rating_supporting/id3v2/_id3v2_constants.py +11 -0
- audiometa/manager/_rating_supporting/riff/_RiffManager.py +778 -0
- audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
- audiometa/manager/_rating_supporting/riff/_riff_constants.py +10 -0
- audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +525 -0
- audiometa/manager/_rating_supporting/vorbis/__init__.py +17 -0
- audiometa/manager/_rating_supporting/vorbis/_vorbis_constants.py +6 -0
- audiometa/manager/id3v1/_Id3v1Manager.py +512 -0
- audiometa/manager/id3v1/__init__.py +1 -0
- audiometa/manager/id3v1/_constants.py +8 -0
- audiometa/manager/id3v1/id3v1_raw_metadata.py +242 -0
- audiometa/manager/id3v1/id3v1_raw_metadata_key.py +13 -0
- audiometa/test/__init__.py +1 -0
- audiometa/test/assets/create_test_files.py +72 -0
- audiometa/test/helpers/__init__.py +51 -0
- audiometa/test/helpers/common/__init__.py +6 -0
- audiometa/test/helpers/common/audio_file_creator.py +68 -0
- audiometa/test/helpers/common/external_tool_runner.py +74 -0
- audiometa/test/helpers/id3v1/__init__.py +8 -0
- audiometa/test/helpers/id3v1/id3v1_header_verifier.py +18 -0
- audiometa/test/helpers/id3v1/id3v1_metadata_deleter.py +37 -0
- audiometa/test/helpers/id3v1/id3v1_metadata_getter.py +61 -0
- audiometa/test/helpers/id3v1/id3v1_metadata_setter.py +82 -0
- audiometa/test/helpers/id3v2/__init__.py +28 -0
- audiometa/test/helpers/id3v2/id3v2_frame_manual_creator.py +305 -0
- audiometa/test/helpers/id3v2/id3v2_header_verifier.py +38 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_deleter.py +56 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_getter.py +188 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +428 -0
- audiometa/test/helpers/riff/__init__.py +8 -0
- audiometa/test/helpers/riff/riff_header_verifier.py +85 -0
- audiometa/test/helpers/riff/riff_manual_metadata_creator.py +216 -0
- audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
- audiometa/test/helpers/riff/riff_metadata_getter.py +118 -0
- audiometa/test/helpers/riff/riff_metadata_setter.py +196 -0
- audiometa/test/helpers/scripts/__init__.py +0 -0
- audiometa/test/helpers/technical_info_inspector.py +115 -0
- audiometa/test/helpers/temp_file_with_metadata.py +82 -0
- audiometa/test/helpers/vorbis/__init__.py +8 -0
- audiometa/test/helpers/vorbis/vorbis_header_verifier.py +31 -0
- audiometa/test/helpers/vorbis/vorbis_metadata_deleter.py +49 -0
- audiometa/test/helpers/vorbis/vorbis_metadata_getter.py +67 -0
- audiometa/test/helpers/vorbis/vorbis_metadata_setter.py +200 -0
- audiometa/test/tests/__init__.py +0 -0
- audiometa/test/tests/conftest.py +261 -0
- audiometa/test/tests/e2e/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/error_handling/__init__.py +1 -0
- audiometa/test/tests/e2e/cli/error_handling/test_command_structure_errors.py +77 -0
- audiometa/test/tests/e2e/cli/error_handling/test_file_access_errors.py +130 -0
- audiometa/test/tests/e2e/cli/error_handling/test_format_output_errors.py +118 -0
- audiometa/test/tests/e2e/cli/error_handling/test_input_validation_errors.py +172 -0
- audiometa/test/tests/e2e/cli/error_handling/test_missing_fields_validation.py +49 -0
- audiometa/test/tests/e2e/cli/error_handling/test_multiple_files_errors.py +160 -0
- audiometa/test/tests/e2e/cli/error_handling/test_rating_validation.py +90 -0
- audiometa/test/tests/e2e/cli/error_handling/test_year_validation.py +51 -0
- audiometa/test/tests/e2e/cli/test_cli_delete.py +20 -0
- audiometa/test/tests/e2e/cli/test_cli_formatting.py +31 -0
- audiometa/test/tests/e2e/cli/test_cli_help.py +40 -0
- audiometa/test/tests/e2e/cli/test_cli_read.py +79 -0
- audiometa/test/tests/e2e/cli/test_cli_unified.py +33 -0
- audiometa/test/tests/e2e/cli/test_cli_write.py +116 -0
- audiometa/test/tests/e2e/scenarios/__init__.py +0 -0
- audiometa/test/tests/e2e/scenarios/test_user_scenarios.py +166 -0
- audiometa/test/tests/e2e/workflows/__init__.py +0 -0
- audiometa/test/tests/e2e/workflows/test_core_workflows.py +166 -0
- audiometa/test/tests/e2e/workflows/test_deletion_workflows.py +318 -0
- audiometa/test/tests/e2e/workflows/test_error_handling_workflows.py +165 -0
- audiometa/test/tests/e2e/workflows/test_format_specific_workflows.py +129 -0
- audiometa/test/tests/e2e/workflows/test_rating_workflows.py +124 -0
- audiometa/test/tests/integration/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/flac/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_delete_all.py +108 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_reading_all.py +61 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_reading_field.py +65 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_writing.py +69 -0
- audiometa/test/tests/integration/audio_format/mp3/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_delete_all.py +79 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_all.py +61 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_field.py +67 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_writing.py +60 -0
- audiometa/test/tests/integration/audio_format/wav/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_delete_all.py +87 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_reading_all.py +62 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_reading_field.py +57 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_with_id3v2_tags.py +83 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_writing.py +62 -0
- audiometa/test/tests/integration/conftest.py +29 -0
- audiometa/test/tests/integration/delete_all_metadata/__init__.py +1 -0
- audiometa/test/tests/integration/delete_all_metadata/test_audio_format_all.py +102 -0
- audiometa/test/tests/integration/delete_all_metadata/test_audio_format_header_deletion.py +77 -0
- audiometa/test/tests/integration/delete_all_metadata/test_basic_functionality.py +47 -0
- audiometa/test/tests/integration/delete_all_metadata/test_error_handling.py +24 -0
- audiometa/test/tests/integration/encoding/__init__.py +1 -0
- audiometa/test/tests/integration/encoding/test_encoding.py +88 -0
- audiometa/test/tests/integration/encoding/test_special_characters_edge_cases.py +223 -0
- audiometa/test/tests/integration/get_full_metadata/__init__.py +0 -0
- audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +250 -0
- audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +297 -0
- audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata_edge_cases.py +231 -0
- audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
- audiometa/test/tests/integration/get_full_metadata/test_parsed_fields_keys.py +85 -0
- audiometa/test/tests/integration/metadata_field/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/album/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/album/test_deleting.py +73 -0
- audiometa/test/tests/integration/metadata_field/album/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/album/test_writing.py +50 -0
- audiometa/test/tests/integration/metadata_field/album_artists/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/album_artists/test_deleting.py +83 -0
- audiometa/test/tests/integration/metadata_field/album_artists/test_reading.py +38 -0
- audiometa/test/tests/integration/metadata_field/album_artists/test_writing.py +52 -0
- audiometa/test/tests/integration/metadata_field/artists/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/artists/test_deleting.py +68 -0
- audiometa/test/tests/integration/metadata_field/artists/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/artists/test_writing.py +46 -0
- audiometa/test/tests/integration/metadata_field/bpm/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/bpm/test_deleting.py +75 -0
- audiometa/test/tests/integration/metadata_field/bpm/test_reading.py +32 -0
- audiometa/test/tests/integration/metadata_field/bpm/test_writing.py +56 -0
- audiometa/test/tests/integration/metadata_field/comment/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/comment/test_deleting.py +68 -0
- audiometa/test/tests/integration/metadata_field/comment/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/comment/test_writing.py +49 -0
- audiometa/test/tests/integration/metadata_field/composer/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/composer/test_deleting.py +75 -0
- audiometa/test/tests/integration/metadata_field/composer/test_reading.py +34 -0
- audiometa/test/tests/integration/metadata_field/composer/test_writing.py +41 -0
- audiometa/test/tests/integration/metadata_field/copyright/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/copyright/test_deleting.py +81 -0
- audiometa/test/tests/integration/metadata_field/copyright/test_reading.py +35 -0
- audiometa/test/tests/integration/metadata_field/copyright/test_writing.py +41 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/test_deleting.py +56 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/test_reading.py +54 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/test_writing.py +61 -0
- audiometa/test/tests/integration/metadata_field/genre/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v1_reading.py +65 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v2_reading.py +25 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_riff_reading.py +58 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_vorbis_reading.py +61 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/test_smart_reading.py +191 -0
- audiometa/test/tests/integration/metadata_field/genre/test_deleting.py +62 -0
- audiometa/test/tests/integration/metadata_field/genre/test_writing.py +64 -0
- audiometa/test/tests/integration/metadata_field/language/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/language/test_deleting.py +75 -0
- audiometa/test/tests/integration/metadata_field/language/test_reading.py +39 -0
- audiometa/test/tests/integration/metadata_field/language/test_writing.py +43 -0
- audiometa/test/tests/integration/metadata_field/lyrics/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/lyrics/test_deleting.py +129 -0
- audiometa/test/tests/integration/metadata_field/lyrics/test_reading.py +57 -0
- audiometa/test/tests/integration/metadata_field/lyrics/test_writing.py +59 -0
- audiometa/test/tests/integration/metadata_field/publisher/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/publisher/test_deleting.py +88 -0
- audiometa/test/tests/integration/metadata_field/publisher/test_reading.py +32 -0
- audiometa/test/tests/integration/metadata_field/publisher/test_writing.py +47 -0
- audiometa/test/tests/integration/metadata_field/rating/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/test_base_100_proportional.py +81 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_non_proportional.py +33 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_proportional.py +58 -0
- audiometa/test/tests/integration/metadata_field/rating/test_deleting.py +117 -0
- audiometa/test/tests/integration/metadata_field/rating/test_error_handling.py +137 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_id3v2.py +77 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_riff.py +55 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_vorbis.py +57 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/test_comprehensive.py +192 -0
- audiometa/test/tests/integration/metadata_field/release_date/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_deleting.py +74 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_error_handling.py +82 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_reading.py +59 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_writing.py +49 -0
- audiometa/test/tests/integration/metadata_field/test_metadata_field_validation.py +134 -0
- audiometa/test/tests/integration/metadata_field/title/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/title/test_deleting.py +73 -0
- audiometa/test/tests/integration/metadata_field/title/test_error_handling.py +47 -0
- audiometa/test/tests/integration/metadata_field/title/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/title/test_writing.py +64 -0
- audiometa/test/tests/integration/metadata_field/track_number/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/track_number/reading/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/track_number/reading/test_edge_cases.py +43 -0
- audiometa/test/tests/integration/metadata_field/track_number/reading/test_metadata_format.py +32 -0
- audiometa/test/tests/integration/metadata_field/track_number/test_deleting.py +59 -0
- audiometa/test/tests/integration/metadata_field/track_number/test_writing.py +73 -0
- audiometa/test/tests/integration/multiple_values/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/reading/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v1.py +23 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_3.py +92 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_4.py +216 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_riff.py +84 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_vorbis.py +169 -0
- audiometa/test/tests/integration/multiple_values/reading/test_performance_large_data.py +209 -0
- audiometa/test/tests/integration/multiple_values/reading/test_smart_parsing_scenarios.py +198 -0
- audiometa/test/tests/integration/multiple_values/reading/test_unicode_handling.py +24 -0
- audiometa/test/tests/integration/multiple_values/writing/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v1.py +62 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_3.py +36 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_4.py +34 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_riff.py +32 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_vorbis.py +54 -0
- audiometa/test/tests/integration/multiple_values/writing/test_error_handling.py +42 -0
- audiometa/test/tests/integration/multiple_values/writing/test_large_values.py +98 -0
- audiometa/test/tests/integration/reading/__init__.py +1 -0
- audiometa/test/tests/integration/reading/test_read_multiple_metadata.py +80 -0
- audiometa/test/tests/integration/reading/test_reading_error_handling.py +36 -0
- audiometa/test/tests/integration/real_audio_files/__init__.py +0 -0
- audiometa/test/tests/integration/real_audio_files/test_reading.py +146 -0
- audiometa/test/tests/integration/real_audio_files/test_writing.py +198 -0
- audiometa/test/tests/integration/technical_info/__init__.py +0 -0
- audiometa/test/tests/integration/technical_info/flac_md5/__init__.py +0 -0
- audiometa/test/tests/integration/technical_info/flac_md5/conftest.py +103 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/__init__.py +0 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_audio_data_corruption.py +21 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_flipped_md5.py +29 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_non_flac_error.py +13 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_partial_md5.py +29 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_random_md5.py +29 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_unset_md5.py +56 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_valid_md5.py +21 -0
- audiometa/test/tests/integration/technical_info/test_bitrate.py +38 -0
- audiometa/test/tests/integration/technical_info/test_channels.py +38 -0
- audiometa/test/tests/integration/technical_info/test_duration_in_sec.py +38 -0
- audiometa/test/tests/integration/technical_info/test_sample_rate.py +40 -0
- audiometa/test/tests/integration/test_audio_file.py +23 -0
- audiometa/test/tests/integration/test_audio_format_readable_after_update_all_metadata_formats.py +95 -0
- audiometa/test/tests/integration/writing/__init__.py +0 -0
- audiometa/test/tests/integration/writing/test_error_handling.py +44 -0
- audiometa/test/tests/integration/writing/test_forced_format.py +224 -0
- audiometa/test/tests/integration/writing/test_multiple_format_preservation.py +223 -0
- audiometa/test/tests/integration/writing/test_partial_update.py +36 -0
- audiometa/test/tests/integration/writing/writing_strategies/__init__.py +0 -0
- audiometa/test/tests/integration/writing/writing_strategies/test_cleanup_strategy.py +79 -0
- audiometa/test/tests/integration/writing/writing_strategies/test_preserve_strategy.py +76 -0
- audiometa/test/tests/integration/writing/writing_strategies/test_sync_strategy.py +215 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/__init__.py +0 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_fail_behavior.py +42 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_no_writing_on_failure.py +93 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_strategy_specific.py +99 -0
- audiometa/test/tests/unit/__init__.py +0 -0
- audiometa/test/tests/unit/audio_file/__init__.py +0 -0
- audiometa/test/tests/unit/audio_file/technical_info/__init__.py +0 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_bitrate.py +26 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_channels.py +31 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_duration_in_sec.py +38 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_error_handling.py +130 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_file_size.py +51 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_format_name.py +28 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_sample_rate.py +31 -0
- audiometa/test/tests/unit/audio_file/test_context_manager.py +30 -0
- audiometa/test/tests/unit/audio_file/test_file_validation.py +40 -0
- audiometa/test/tests/unit/audio_file/test_operations.py +20 -0
- audiometa/test/tests/unit/audio_file/test_path_handling.py +23 -0
- audiometa/test/tests/unit/cli/__init__.py +0 -0
- audiometa/test/tests/unit/cli/test_expand_file_patterns.py +234 -0
- audiometa/test/tests/unit/metadata_managers/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/conftest.py +142 -0
- audiometa/test/tests/unit/metadata_managers/header_info/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_id3v1.py +49 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_id3v2.py +66 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_riff.py +57 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_vorbis.py +53 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/test_smart_parsing.py +186 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_separator_selection.py +142 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_value_filtering.py +76 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_normalization.py +152 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_profiles_values.py +23 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/test_rating_validation.py +77 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_validation.py +151 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_writing_profiles.py +61 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_date_format_validation.py +135 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_track_number_validation.py +46 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_type_validation_exception.py +22 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_validation.py +83 -0
- audiometa/test/tests/unit/metadata_managers/test_metadata_format_managers_write_and_read.py +74 -0
- audiometa/utils/__init__.py +1 -0
- audiometa/utils/id3v1_genre_code_map.py +205 -0
- audiometa/utils/metadata_format.py +31 -0
- audiometa/utils/metadata_writing_strategy.py +16 -0
- audiometa/utils/os_dependencies_checker/__init__.py +24 -0
- audiometa/utils/os_dependencies_checker/base.py +62 -0
- audiometa/utils/os_dependencies_checker/config.py +51 -0
- audiometa/utils/os_dependencies_checker/macos.py +236 -0
- audiometa/utils/os_dependencies_checker/ubuntu.py +95 -0
- audiometa/utils/os_dependencies_checker/windows.py +227 -0
- audiometa/utils/rating_profiles.py +110 -0
- audiometa/utils/tool_path_resolver.py +135 -0
- audiometa/utils/types.py +82 -0
- audiometa/utils/unified_metadata_key.py +81 -0
- audiometa_python-0.2.2.dist-info/METADATA +1464 -0
- audiometa_python-0.2.2.dist-info/RECORD +318 -0
- audiometa_python-0.2.2.dist-info/WHEEL +5 -0
- audiometa_python-0.2.2.dist-info/entry_points.txt +2 -0
- audiometa_python-0.2.2.dist-info/licenses/LICENSE +202 -0
- audiometa_python-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Standard ID3v1 genre codes mapping.
|
|
2
|
+
|
|
3
|
+
This is the complete standard genre map used by both ID3v1 and RIFF formats. Genres 0-79 are from the original ID3v1
|
|
4
|
+
spec. Genres 80-125 were added by Winamp. Genres 126-147 were added by other players. Genres 148-191 were added in
|
|
5
|
+
Winamp 5.6 (November 2010).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
ID3V1_GENRE_CODE_MAP = {
|
|
9
|
+
0: "Blues",
|
|
10
|
+
1: "Classic Rock",
|
|
11
|
+
2: "Country",
|
|
12
|
+
3: "Dance",
|
|
13
|
+
4: "Disco",
|
|
14
|
+
5: "Funk",
|
|
15
|
+
6: "Grunge",
|
|
16
|
+
7: "Hip-Hop",
|
|
17
|
+
8: "Jazz",
|
|
18
|
+
9: "Metal",
|
|
19
|
+
10: "New Age",
|
|
20
|
+
11: "Oldies",
|
|
21
|
+
12: "Other",
|
|
22
|
+
13: "Pop",
|
|
23
|
+
14: "R&B",
|
|
24
|
+
15: "Rap",
|
|
25
|
+
16: "Reggae",
|
|
26
|
+
17: "Rock",
|
|
27
|
+
18: "Techno",
|
|
28
|
+
19: "Industrial",
|
|
29
|
+
20: "Alternative",
|
|
30
|
+
21: "Ska",
|
|
31
|
+
22: "Death Metal",
|
|
32
|
+
23: "Pranks",
|
|
33
|
+
24: "Soundtrack",
|
|
34
|
+
25: "Euro-Techno",
|
|
35
|
+
26: "Ambient",
|
|
36
|
+
27: "Trip-Hop",
|
|
37
|
+
28: "Vocal",
|
|
38
|
+
29: "Jazz+Funk",
|
|
39
|
+
30: "Fusion",
|
|
40
|
+
31: "Trance",
|
|
41
|
+
32: "Classical",
|
|
42
|
+
33: "Instrumental",
|
|
43
|
+
34: "Acid",
|
|
44
|
+
35: "House",
|
|
45
|
+
36: "Game",
|
|
46
|
+
37: "Sound Clip",
|
|
47
|
+
38: "Gospel",
|
|
48
|
+
39: "Noise",
|
|
49
|
+
40: "Alternative Rock",
|
|
50
|
+
41: "Bass",
|
|
51
|
+
42: "Soul",
|
|
52
|
+
43: "Punk",
|
|
53
|
+
44: "Space",
|
|
54
|
+
45: "Meditative",
|
|
55
|
+
46: "Instrumental Pop",
|
|
56
|
+
47: "Instrumental Rock",
|
|
57
|
+
48: "Ethnic",
|
|
58
|
+
49: "Gothic",
|
|
59
|
+
50: "Darkwave",
|
|
60
|
+
51: "Techno-Industrial",
|
|
61
|
+
52: "Electronic",
|
|
62
|
+
53: "Pop-Folk",
|
|
63
|
+
54: "Eurodance",
|
|
64
|
+
55: "Dream",
|
|
65
|
+
56: "Southern Rock",
|
|
66
|
+
57: "Comedy",
|
|
67
|
+
58: "Cult",
|
|
68
|
+
59: "Gangsta",
|
|
69
|
+
60: "Top 40",
|
|
70
|
+
61: "Christian Rap",
|
|
71
|
+
62: "Pop/Funk",
|
|
72
|
+
63: "Jungle",
|
|
73
|
+
64: "Native US",
|
|
74
|
+
65: "Cabaret",
|
|
75
|
+
66: "New Wave",
|
|
76
|
+
67: "Psychedelic",
|
|
77
|
+
68: "Rave",
|
|
78
|
+
69: "Showtunes",
|
|
79
|
+
70: "Trailer",
|
|
80
|
+
71: "Lo-Fi",
|
|
81
|
+
72: "Tribal",
|
|
82
|
+
73: "Acid Punk",
|
|
83
|
+
74: "Acid Jazz",
|
|
84
|
+
75: "Polka",
|
|
85
|
+
76: "Retro",
|
|
86
|
+
77: "Musical",
|
|
87
|
+
78: "Rock & Roll",
|
|
88
|
+
79: "Hard Rock",
|
|
89
|
+
# Winamp extensions
|
|
90
|
+
80: "Folk",
|
|
91
|
+
81: "Folk-Rock",
|
|
92
|
+
82: "National Folk",
|
|
93
|
+
83: "Swing",
|
|
94
|
+
84: "Fast Fusion",
|
|
95
|
+
85: "Bebop",
|
|
96
|
+
86: "Latin",
|
|
97
|
+
87: "Revival",
|
|
98
|
+
88: "Celtic",
|
|
99
|
+
89: "Bluegrass",
|
|
100
|
+
90: "Avantgarde",
|
|
101
|
+
91: "Gothic Rock",
|
|
102
|
+
92: "Progressive Rock",
|
|
103
|
+
93: "Psychedelic Rock",
|
|
104
|
+
94: "Symphonic Rock",
|
|
105
|
+
95: "Slow Rock",
|
|
106
|
+
96: "Big Band",
|
|
107
|
+
97: "Chorus",
|
|
108
|
+
98: "Easy Listening",
|
|
109
|
+
99: "Acoustic",
|
|
110
|
+
100: "Humour",
|
|
111
|
+
101: "Speech",
|
|
112
|
+
102: "Chanson",
|
|
113
|
+
103: "Opera",
|
|
114
|
+
104: "Chamber Music",
|
|
115
|
+
105: "Sonata",
|
|
116
|
+
106: "Symphony",
|
|
117
|
+
107: "Booty Bass",
|
|
118
|
+
108: "Primus",
|
|
119
|
+
109: "Porn Groove",
|
|
120
|
+
110: "Satire",
|
|
121
|
+
111: "Slow Jam",
|
|
122
|
+
112: "Club",
|
|
123
|
+
113: "Tango",
|
|
124
|
+
114: "Samba",
|
|
125
|
+
115: "Folklore",
|
|
126
|
+
116: "Ballad",
|
|
127
|
+
117: "Power Ballad",
|
|
128
|
+
118: "Rhythmic Soul",
|
|
129
|
+
119: "Freestyle",
|
|
130
|
+
120: "Duet",
|
|
131
|
+
121: "Punk Rock",
|
|
132
|
+
122: "Drum Solo",
|
|
133
|
+
123: "A Cappella",
|
|
134
|
+
124: "Euro-House",
|
|
135
|
+
125: "Dance Hall",
|
|
136
|
+
# Other extensions
|
|
137
|
+
126: "Goa",
|
|
138
|
+
127: "Drum & Bass",
|
|
139
|
+
128: "Club-House",
|
|
140
|
+
129: "Hardcore",
|
|
141
|
+
130: "Terror",
|
|
142
|
+
131: "Indie",
|
|
143
|
+
132: "BritPop",
|
|
144
|
+
133: "Negerpunk",
|
|
145
|
+
134: "Polsk Punk",
|
|
146
|
+
135: "Beat",
|
|
147
|
+
136: "Christian Gangsta Rap",
|
|
148
|
+
137: "Heavy Metal",
|
|
149
|
+
138: "Black Metal",
|
|
150
|
+
139: "Crossover",
|
|
151
|
+
140: "Contemporary Christian",
|
|
152
|
+
141: "Christian Rock",
|
|
153
|
+
142: "Merengue",
|
|
154
|
+
143: "Salsa",
|
|
155
|
+
144: "Thrash Metal",
|
|
156
|
+
145: "Anime",
|
|
157
|
+
146: "JPop",
|
|
158
|
+
147: "Synthpop",
|
|
159
|
+
# Winamp 5.6 extensions (November 2010)
|
|
160
|
+
148: "Christmas",
|
|
161
|
+
149: "Art Rock",
|
|
162
|
+
150: "Baroque",
|
|
163
|
+
151: "Bhangra",
|
|
164
|
+
152: "Big Beat",
|
|
165
|
+
153: "Breakbeat",
|
|
166
|
+
154: "Chillout",
|
|
167
|
+
155: "Downtempo",
|
|
168
|
+
156: "Dub",
|
|
169
|
+
157: "EBM",
|
|
170
|
+
158: "Eclectic",
|
|
171
|
+
159: "Electro",
|
|
172
|
+
160: "Electroclash",
|
|
173
|
+
161: "Emo",
|
|
174
|
+
162: "Experimental",
|
|
175
|
+
163: "Garage",
|
|
176
|
+
164: "Global",
|
|
177
|
+
165: "IDM",
|
|
178
|
+
166: "Illbient",
|
|
179
|
+
167: "Industro-Goth",
|
|
180
|
+
168: "Jam Band",
|
|
181
|
+
169: "Krautrock",
|
|
182
|
+
170: "Leftfield",
|
|
183
|
+
171: "Lounge",
|
|
184
|
+
172: "Math Rock",
|
|
185
|
+
173: "New Romantic",
|
|
186
|
+
174: "Nu-Breakz",
|
|
187
|
+
175: "Post-Punk",
|
|
188
|
+
176: "Post-Rock",
|
|
189
|
+
177: "Psytrance",
|
|
190
|
+
178: "Shoegaze",
|
|
191
|
+
179: "Space Rock",
|
|
192
|
+
180: "Trop Rock",
|
|
193
|
+
181: "World Music",
|
|
194
|
+
182: "Neoclassical",
|
|
195
|
+
183: "Audiobook",
|
|
196
|
+
184: "Audio Theatre",
|
|
197
|
+
185: "Neue Deutsche Welle",
|
|
198
|
+
186: "Podcast",
|
|
199
|
+
187: "Indie Rock",
|
|
200
|
+
188: "G-Funk",
|
|
201
|
+
189: "Dubstep",
|
|
202
|
+
190: "Garage Rock",
|
|
203
|
+
191: "Psybient",
|
|
204
|
+
255: None,
|
|
205
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tag type constants for audio metadata handling.
|
|
2
|
+
|
|
3
|
+
This module defines the supported metadata formats and their file extension priorities for reading and writing audio
|
|
4
|
+
metadata across different file types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MetadataFormat(str, Enum):
|
|
11
|
+
"""Enumeration of supported audio metadata formats."""
|
|
12
|
+
|
|
13
|
+
ID3V2 = "id3v2"
|
|
14
|
+
ID3V1 = "id3v1"
|
|
15
|
+
VORBIS = "vorbis"
|
|
16
|
+
RIFF = "riff"
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_priorities(cls) -> dict[str, list["MetadataFormat"]]:
|
|
20
|
+
"""Get tag format priorities for different file formats.
|
|
21
|
+
|
|
22
|
+
First tag format in each list has highest priority.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
dictionary mapping file extensions to ordered list of tag types
|
|
26
|
+
"""
|
|
27
|
+
return {
|
|
28
|
+
".flac": [cls.VORBIS, cls.ID3V2, cls.ID3V1],
|
|
29
|
+
".mp3": [cls.ID3V2, cls.ID3V1],
|
|
30
|
+
".wav": [cls.RIFF, cls.ID3V2, cls.ID3V1],
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Metadata writing strategy constants for audio metadata handling."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MetadataWritingStrategy(str, Enum):
|
|
7
|
+
"""Strategy for handling metadata when writing to files with existing metadata in other formats."""
|
|
8
|
+
|
|
9
|
+
SYNC = "sync"
|
|
10
|
+
"""Write to native format and synchronize other metadata formats that are already present (default)"""
|
|
11
|
+
|
|
12
|
+
PRESERVE = "preserve"
|
|
13
|
+
"""Write to native format only, preserve existing metadata in other formats."""
|
|
14
|
+
|
|
15
|
+
CLEANUP = "cleanup"
|
|
16
|
+
"""Write to native format and remove all non-native metadata formats."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""OS-specific dependency checkers for verifying system dependencies."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
from audiometa.utils.os_dependencies_checker.base import OsDependenciesChecker
|
|
6
|
+
from audiometa.utils.os_dependencies_checker.macos import MacOSDependenciesChecker
|
|
7
|
+
from audiometa.utils.os_dependencies_checker.ubuntu import UbuntuDependenciesChecker
|
|
8
|
+
from audiometa.utils.os_dependencies_checker.windows import WindowsDependenciesChecker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_dependencies_checker() -> OsDependenciesChecker | None:
|
|
12
|
+
"""Get the appropriate OS-specific dependencies checker.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
OS-specific checker instance, or None if OS not supported
|
|
16
|
+
"""
|
|
17
|
+
system = platform.system().lower()
|
|
18
|
+
if system == "darwin":
|
|
19
|
+
return MacOSDependenciesChecker()
|
|
20
|
+
if system == "linux":
|
|
21
|
+
return UbuntuDependenciesChecker()
|
|
22
|
+
if system == "windows":
|
|
23
|
+
return WindowsDependenciesChecker()
|
|
24
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Base class for OS-specific dependency checkers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OsDependenciesChecker(ABC):
|
|
7
|
+
"""Base class for OS-specific dependency checkers."""
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def get_os_type(cls) -> str:
|
|
12
|
+
"""Get OS type identifier."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def check_tool_available(self, tool_name: str) -> bool:
|
|
16
|
+
"""Check if a tool is available (in PATH or default locations).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
tool_name: Name of the tool to check
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True if tool is available, False otherwise
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def get_installed_version(self, package: str, expected_version: str | None = None) -> str | None:
|
|
27
|
+
"""Get installed version of a package.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
package: Package name
|
|
31
|
+
expected_version: Optional expected/pinned version
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Installed version string, or None if not found
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _normalize_version(version: str) -> str:
|
|
39
|
+
"""Normalize version string by removing revision suffix.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
version: Version string (e.g., "7.1_4" or "1.5.0")
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Normalized version without revision suffix
|
|
46
|
+
"""
|
|
47
|
+
return version.split("_")[0]
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _versions_match(version1: str, version2: str) -> bool:
|
|
51
|
+
"""Check if two version strings match (handles different precision).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
version1: First version string
|
|
55
|
+
version2: Second version string
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if versions match, False otherwise
|
|
59
|
+
"""
|
|
60
|
+
v1_normalized = OsDependenciesChecker._normalize_version(version1)
|
|
61
|
+
v2_normalized = OsDependenciesChecker._normalize_version(version2)
|
|
62
|
+
return v1_normalized == v2_normalized or v2_normalized.startswith(v1_normalized + ".")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Configuration loading for OS-specific dependency checkers."""
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_dependencies_pinned_versions() -> dict[str, dict[str, str]] | None:
|
|
8
|
+
"""Load pinned versions from system-dependencies.toml.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
Dictionary mapping tool names to OS-specific versions, or None if config not found
|
|
12
|
+
"""
|
|
13
|
+
# Try to find system-dependencies.toml relative to this file
|
|
14
|
+
# This file is in audiometa/utils/os_dependencies_checker/, so go up to project root
|
|
15
|
+
config_path = Path(__file__).parent.parent.parent.parent / "system-dependencies.toml"
|
|
16
|
+
|
|
17
|
+
if not config_path.exists():
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
with config_path.open("rb") as f:
|
|
22
|
+
config = tomllib.load(f)
|
|
23
|
+
|
|
24
|
+
pinned_versions: dict[str, dict[str, str]] = {}
|
|
25
|
+
|
|
26
|
+
# Extract versions for each OS
|
|
27
|
+
for os_type in ["ubuntu", "macos", "windows"]:
|
|
28
|
+
if os_type not in config:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
os_config = config[os_type]
|
|
32
|
+
for tool in ["ffmpeg", "flac", "mediainfo", "id3v2", "bwfmetaedit", "exiftool"]:
|
|
33
|
+
if tool not in os_config:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
version_value = os_config[tool]
|
|
37
|
+
# Handle both string values and dict values (for bwfmetaedit, exiftool on Windows)
|
|
38
|
+
if isinstance(version_value, str):
|
|
39
|
+
version = version_value
|
|
40
|
+
elif isinstance(version_value, dict) and "pinned_version" in version_value:
|
|
41
|
+
version = version_value["pinned_version"]
|
|
42
|
+
else:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if tool not in pinned_versions:
|
|
46
|
+
pinned_versions[tool] = {}
|
|
47
|
+
pinned_versions[tool][os_type] = version
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
else:
|
|
51
|
+
return pinned_versions
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""MacOS-specific dependency checker using Homebrew."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from audiometa.utils.os_dependencies_checker.base import OsDependenciesChecker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MacOSDependenciesChecker(OsDependenciesChecker):
|
|
11
|
+
"""MacOS-specific dependency checker using Homebrew."""
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def get_os_type(cls) -> str:
|
|
15
|
+
return "macos"
|
|
16
|
+
|
|
17
|
+
def _get_brew_prefix(self) -> str | None:
|
|
18
|
+
"""Get Homebrew prefix path."""
|
|
19
|
+
try:
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
["brew", "--prefix"],
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
check=True,
|
|
25
|
+
)
|
|
26
|
+
if result.stdout:
|
|
27
|
+
return result.stdout.strip()
|
|
28
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
29
|
+
pass
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def _extract_version_from_output(self, output: str, tool_name: str) -> str | None:
|
|
33
|
+
"""Extract version number from tool output."""
|
|
34
|
+
if tool_name in ["flac", "metaflac"]:
|
|
35
|
+
match = re.search(r"(\d+\.\d+\.\d+)", output)
|
|
36
|
+
elif tool_name == "mediainfo":
|
|
37
|
+
match = re.search(r"(\d+\.\d+(?:\.\d+)?)", output)
|
|
38
|
+
elif tool_name in ["id3v2", "bwfmetaedit"]:
|
|
39
|
+
match = re.search(r"(\d+\.\d+\.\d+)", output)
|
|
40
|
+
elif tool_name == "exiftool":
|
|
41
|
+
match = re.search(r"(\d+\.\d+(?:\.\d+)?)", output)
|
|
42
|
+
else:
|
|
43
|
+
match = re.search(r"(\d+\.\d+\.\d+)", output)
|
|
44
|
+
|
|
45
|
+
return match.group(1) if match else None
|
|
46
|
+
|
|
47
|
+
def check_tool_available(self, tool_name: str) -> bool:
|
|
48
|
+
"""Check if tool is available in PATH or Homebrew locations."""
|
|
49
|
+
brew_prefix = self._get_brew_prefix()
|
|
50
|
+
if brew_prefix:
|
|
51
|
+
tool_paths = [
|
|
52
|
+
f"{brew_prefix}/opt/{tool_name}/bin/{tool_name}",
|
|
53
|
+
f"{brew_prefix}/bin/{tool_name}",
|
|
54
|
+
]
|
|
55
|
+
# Special handling for ffmpeg/ffprobe (keg-only packages)
|
|
56
|
+
if tool_name in ["ffmpeg", "ffprobe"]:
|
|
57
|
+
for version in ["7", "6", "5"]:
|
|
58
|
+
tool_paths.insert(0, f"{brew_prefix}/opt/ffmpeg@{version}/bin/{tool_name}")
|
|
59
|
+
|
|
60
|
+
for tool_path in tool_paths:
|
|
61
|
+
if Path(tool_path).exists() and Path(tool_path).is_file():
|
|
62
|
+
try:
|
|
63
|
+
# exiftool uses -ver, not --version
|
|
64
|
+
if tool_name == "exiftool":
|
|
65
|
+
version_flag = "-ver"
|
|
66
|
+
elif tool_name == "ffprobe":
|
|
67
|
+
version_flag = "-version"
|
|
68
|
+
else:
|
|
69
|
+
version_flag = "--version"
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
[tool_path, version_flag],
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
check=False,
|
|
75
|
+
)
|
|
76
|
+
if result.stdout or result.stderr:
|
|
77
|
+
return True
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Fallback to PATH check
|
|
82
|
+
try:
|
|
83
|
+
# exiftool uses -ver, not --version
|
|
84
|
+
if tool_name == "exiftool":
|
|
85
|
+
version_flag = "-ver"
|
|
86
|
+
elif tool_name == "ffprobe":
|
|
87
|
+
version_flag = "-version"
|
|
88
|
+
else:
|
|
89
|
+
version_flag = "--version"
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
[tool_name, version_flag],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
check=False,
|
|
95
|
+
)
|
|
96
|
+
return bool(result.stdout or result.stderr)
|
|
97
|
+
except FileNotFoundError:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def _get_ffmpeg_version(self) -> str | None:
|
|
101
|
+
"""Get ffmpeg version (special handling for keg-only package)."""
|
|
102
|
+
ffprobe_paths = ["ffprobe"]
|
|
103
|
+
brew_prefix = self._get_brew_prefix()
|
|
104
|
+
if brew_prefix:
|
|
105
|
+
for version in ["7", "6", "5"]:
|
|
106
|
+
ffprobe_paths.append(f"{brew_prefix}/opt/ffmpeg@{version}/bin/ffprobe")
|
|
107
|
+
|
|
108
|
+
for ffprobe_path in ffprobe_paths:
|
|
109
|
+
try:
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
[ffprobe_path, "-version"],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
if result.stdout or result.stderr:
|
|
117
|
+
output = result.stdout + result.stderr
|
|
118
|
+
match = re.search(r"version\s+(\d+(?:\.\d+)*)", output)
|
|
119
|
+
if match:
|
|
120
|
+
return match.group(1)
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
continue
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def _get_running_version_from_executable(self, package: str, tool_name: str) -> str | None:
|
|
126
|
+
"""Get version from tool executable."""
|
|
127
|
+
tool_paths = [tool_name]
|
|
128
|
+
brew_prefix = self._get_brew_prefix()
|
|
129
|
+
if brew_prefix:
|
|
130
|
+
tool_paths.extend(
|
|
131
|
+
[
|
|
132
|
+
f"{brew_prefix}/opt/{package}/bin/{tool_name}",
|
|
133
|
+
f"{brew_prefix}/bin/{tool_name}",
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for tool_path in tool_paths:
|
|
138
|
+
try:
|
|
139
|
+
# exiftool uses -ver, not --version
|
|
140
|
+
if tool_name == "exiftool":
|
|
141
|
+
version_flag = "-ver"
|
|
142
|
+
elif tool_name == "ffprobe":
|
|
143
|
+
version_flag = "-version"
|
|
144
|
+
else:
|
|
145
|
+
version_flag = "--version"
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
[tool_path, version_flag],
|
|
148
|
+
capture_output=True,
|
|
149
|
+
text=True,
|
|
150
|
+
check=False,
|
|
151
|
+
)
|
|
152
|
+
if result.stdout or result.stderr:
|
|
153
|
+
output = result.stdout + result.stderr
|
|
154
|
+
version = self._extract_version_from_output(output, tool_name)
|
|
155
|
+
if version:
|
|
156
|
+
return version
|
|
157
|
+
except FileNotFoundError:
|
|
158
|
+
continue
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _get_installed_versions_from_brew(self, package: str) -> list[str] | None:
|
|
162
|
+
"""Get list of installed versions from Homebrew."""
|
|
163
|
+
try:
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
["brew", "list", "--versions", package],
|
|
166
|
+
capture_output=True,
|
|
167
|
+
text=True,
|
|
168
|
+
check=True,
|
|
169
|
+
)
|
|
170
|
+
if result.stdout:
|
|
171
|
+
parts = result.stdout.strip().split()
|
|
172
|
+
if len(parts) > 1:
|
|
173
|
+
return parts[1:]
|
|
174
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
175
|
+
pass
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _verify_pinned_version_installed(self, installed_versions: list[str], expected_version: str) -> bool:
|
|
179
|
+
"""Verify that pinned version is in the installed versions list."""
|
|
180
|
+
expected_normalized = self._normalize_version(expected_version)
|
|
181
|
+
for version in installed_versions:
|
|
182
|
+
version_normalized = self._normalize_version(version)
|
|
183
|
+
if self._versions_match(expected_normalized, version_normalized):
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _find_pinned_version_in_list(self, installed_versions: list[str], expected_version: str) -> str | None:
|
|
188
|
+
"""Find and return the pinned version from installed versions list."""
|
|
189
|
+
expected_normalized = self._normalize_version(expected_version)
|
|
190
|
+
for version in installed_versions:
|
|
191
|
+
version_normalized = self._normalize_version(version)
|
|
192
|
+
if self._versions_match(expected_normalized, version_normalized):
|
|
193
|
+
return version_normalized
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def get_installed_version(self, package: str, expected_version: str | None = None) -> str | None:
|
|
197
|
+
"""Get installed package version on macOS."""
|
|
198
|
+
# Special handling for ffmpeg
|
|
199
|
+
if package == "ffmpeg":
|
|
200
|
+
return self._get_ffmpeg_version()
|
|
201
|
+
|
|
202
|
+
# Map package name to tool executable name
|
|
203
|
+
tool_name = package
|
|
204
|
+
if package == "media-info":
|
|
205
|
+
tool_name = "mediainfo"
|
|
206
|
+
|
|
207
|
+
# Get running version from executable
|
|
208
|
+
running_version = self._get_running_version_from_executable(package, tool_name)
|
|
209
|
+
|
|
210
|
+
# Verify pinned version is installed
|
|
211
|
+
installed_versions = self._get_installed_versions_from_brew(package)
|
|
212
|
+
if installed_versions is None:
|
|
213
|
+
return running_version
|
|
214
|
+
|
|
215
|
+
# If expected version is provided, check if running version or Homebrew version matches
|
|
216
|
+
if expected_version:
|
|
217
|
+
# Accept running version if it matches the pinned version
|
|
218
|
+
# This handles cases where tool is installed manually or from another source
|
|
219
|
+
if running_version and self._versions_match(expected_version, running_version):
|
|
220
|
+
return running_version
|
|
221
|
+
|
|
222
|
+
# If running version doesn't match, check if Homebrew has the pinned version
|
|
223
|
+
if installed_versions:
|
|
224
|
+
pinned_version = self._find_pinned_version_in_list(installed_versions, expected_version)
|
|
225
|
+
if pinned_version:
|
|
226
|
+
# Homebrew has the pinned version, but running version doesn't match
|
|
227
|
+
# Return None to indicate mismatch (running version should match pinned version)
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# Pinned version not found in Homebrew and running version doesn't match
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
# If expected version not provided, return running version or first installed version
|
|
234
|
+
if running_version:
|
|
235
|
+
return running_version
|
|
236
|
+
return self._normalize_version(installed_versions[0])
|