audiometa-python 0.6.0__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 +1297 -0
- audiometa/__main__.py +6 -0
- audiometa/_audio_file.py +607 -0
- audiometa/cli.py +476 -0
- audiometa/exceptions.py +167 -0
- audiometa/manager/_MetadataManager.py +768 -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 +1032 -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 +1002 -0
- audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
- audiometa/manager/_rating_supporting/riff/_riff_constants.py +17 -0
- audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +542 -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 +349 -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 +189 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +506 -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 +298 -0
- audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
- audiometa/test/helpers/riff/riff_metadata_getter.py +219 -0
- audiometa/test/helpers/riff/riff_metadata_setter.py +374 -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 +221 -0
- audiometa/test/tests/__init__.py +0 -0
- audiometa/test/tests/conftest.py +276 -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/read/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/read/test_basic.py +58 -0
- audiometa/test/tests/e2e/cli/read/test_comprehensive.py +240 -0
- audiometa/test/tests/e2e/cli/read/test_formats.py +55 -0
- audiometa/test/tests/e2e/cli/read/test_metadata_content.py +164 -0
- audiometa/test/tests/e2e/cli/read/test_multiple_files.py +149 -0
- audiometa/test/tests/e2e/cli/read/test_options.py +88 -0
- audiometa/test/tests/e2e/cli/read/test_unified.py +84 -0
- audiometa/test/tests/e2e/cli/test_delete.py +20 -0
- audiometa/test/tests/e2e/cli/test_formatting.py +31 -0
- audiometa/test/tests/e2e/cli/test_help.py +41 -0
- audiometa/test/tests/e2e/cli/write/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/write/test_basic.py +51 -0
- audiometa/test/tests/e2e/cli/write/test_comprehensive.py +210 -0
- audiometa/test/tests/e2e/cli/write/test_force_format.py +336 -0
- audiometa/test/tests/e2e/cli/write/test_integer_fields.py +145 -0
- audiometa/test/tests/e2e/cli/write/test_list_fields.py +107 -0
- audiometa/test/tests/e2e/cli/write/test_rating.py +74 -0
- audiometa/test/tests/e2e/cli/write/test_string_fields.py +54 -0
- audiometa/test/tests/e2e/cli/write/test_validation.py +85 -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_audio_formats.py +122 -0
- audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +250 -0
- audiometa/test/tests/integration/get_full_metadata/test_consistency.py +67 -0
- audiometa/test/tests/integration/get_full_metadata/test_edge_cases.py +123 -0
- audiometa/test/tests/integration/get_full_metadata/test_error_handling.py +40 -0
- audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +43 -0
- audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
- audiometa/test/tests/integration/get_full_metadata/test_performance.py +95 -0
- audiometa/test/tests/integration/get_full_metadata/test_riff_bext.py +128 -0
- audiometa/test/tests/integration/get_full_metadata/test_structure.py +161 -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/disc_number/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/disc_number/test_deleting.py +97 -0
- audiometa/test/tests/integration/metadata_field/disc_number/test_reading.py +92 -0
- audiometa/test/tests/integration/metadata_field/disc_number/test_writing.py +153 -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/isrc/__init__.py +1 -0
- audiometa/test/tests/integration/metadata_field/isrc/test_deleting.py +31 -0
- audiometa/test/tests/integration/metadata_field/isrc/test_reading.py +35 -0
- audiometa/test/tests/integration/metadata_field/isrc/test_writing.py +165 -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 +135 -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 +79 -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 +35 -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 +190 -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_is_audio_file.py +49 -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 +343 -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_configuration_error.py +43 -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_disc_number_validation.py +75 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_isrc_format_validation.py +121 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_isrc_type_validation.py +30 -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/test/tests/unit/metadata_managers/test_riff_configuration_error.py +26 -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/mutagen_exception_handler.py +24 -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 +77 -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 +87 -0
- audiometa_python-0.6.0.dist-info/METADATA +1593 -0
- audiometa_python-0.6.0.dist-info/RECORD +352 -0
- audiometa_python-0.6.0.dist-info/WHEEL +5 -0
- audiometa_python-0.6.0.dist-info/entry_points.txt +2 -0
- audiometa_python-0.6.0.dist-info/licenses/LICENSE +202 -0
- audiometa_python-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -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])
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Ubuntu-specific dependency checker using dpkg."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from audiometa.utils.os_dependencies_checker.base import OsDependenciesChecker
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UbuntuDependenciesChecker(OsDependenciesChecker):
|
|
9
|
+
"""Ubuntu-specific dependency checker using dpkg."""
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def get_os_type(cls) -> str:
|
|
13
|
+
return "ubuntu"
|
|
14
|
+
|
|
15
|
+
def check_tool_available(self, tool_name: str) -> bool:
|
|
16
|
+
"""Check if tool is available in PATH."""
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
[tool_name, "--version"],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
check=False,
|
|
23
|
+
)
|
|
24
|
+
return bool(result.stdout or result.stderr)
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def get_installed_version(self, package: str, expected_version: str | None = None) -> str | None: # noqa: ARG002
|
|
29
|
+
"""Get installed package version on Ubuntu."""
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(["dpkg", "-l"], capture_output=True, text=True, check=True)
|
|
32
|
+
for line in result.stdout.split("\n"):
|
|
33
|
+
if line.startswith("ii") and package in line:
|
|
34
|
+
parts = line.split()
|
|
35
|
+
if len(parts) >= 3: # noqa: PLR2004
|
|
36
|
+
return parts[2]
|
|
37
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
38
|
+
pass
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _versions_match(version1: str, version2: str) -> bool:
|
|
43
|
+
"""Check if two Ubuntu/Debian package version strings match.
|
|
44
|
+
|
|
45
|
+
Handles Debian package version format: upstream-version-debian-revision
|
|
46
|
+
Compares the upstream version part (before the first '-') for compatibility.
|
|
47
|
+
Supports flexible prefix matching (e.g., "24.01" matches "24.01.1" or "24.01+dfsg").
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
version1: First version string (e.g., "24.01", "24.01.1-1build2", or "25.04.1")
|
|
51
|
+
version2: Second version string (e.g., "24.01+dfsg-1build2", "24.01.1-1build2", or "25.04.1-1")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if upstream versions match, False otherwise
|
|
55
|
+
"""
|
|
56
|
+
# Extract upstream version (part before first '-')
|
|
57
|
+
# Handle both formats: "24.01.1-1build2" -> "24.01.1" and "25.04.1" -> "25.04.1"
|
|
58
|
+
v1_upstream = version1.split("-")[0]
|
|
59
|
+
v2_upstream = version2.split("-")[0]
|
|
60
|
+
|
|
61
|
+
# Normalize versions: remove revision suffix (like _4) and Debian suffixes (like +dfsg)
|
|
62
|
+
# This allows "24.01" to match "24.01+dfsg" or "24.01.1"
|
|
63
|
+
v1_normalized = UbuntuDependenciesChecker._normalize_debian_version(v1_upstream)
|
|
64
|
+
v2_normalized = UbuntuDependenciesChecker._normalize_debian_version(v2_upstream)
|
|
65
|
+
|
|
66
|
+
# Check if versions match exactly
|
|
67
|
+
if v1_normalized == v2_normalized:
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
# Check if one version is a prefix of the other
|
|
71
|
+
# "24.01" should match "24.01.1" (v2 starts with v1 + ".")
|
|
72
|
+
# "24.01.1" should match "24.01" (v1 starts with v2 + ".")
|
|
73
|
+
return v2_normalized.startswith(v1_normalized + ".") or v1_normalized.startswith(v2_normalized + ".")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _normalize_debian_version(version: str) -> str:
|
|
77
|
+
"""Normalize Debian version string by removing suffixes.
|
|
78
|
+
|
|
79
|
+
Removes revision suffixes (like _4) and Debian-specific suffixes (like +dfsg, +ds).
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
version: Version string (e.g., "24.01+dfsg" or "24.01.1_4")
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Normalized version without suffixes
|
|
86
|
+
"""
|
|
87
|
+
# Remove revision suffix (like _4)
|
|
88
|
+
normalized = OsDependenciesChecker._normalize_version(version)
|
|
89
|
+
|
|
90
|
+
# Remove Debian-specific suffixes (like +dfsg, +ds)
|
|
91
|
+
# Split on '+' and take the first part
|
|
92
|
+
if "+" in normalized:
|
|
93
|
+
normalized = normalized.split("+")[0]
|
|
94
|
+
|
|
95
|
+
return normalized
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Windows-specific dependency checker."""
|
|
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 WindowsDependenciesChecker(OsDependenciesChecker):
|
|
11
|
+
"""Windows-specific dependency checker."""
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def get_os_type(cls) -> str:
|
|
15
|
+
return "windows"
|
|
16
|
+
|
|
17
|
+
def check_tool_available(self, tool_name: str) -> bool:
|
|
18
|
+
"""Check if tool is available in PATH or default Windows locations."""
|
|
19
|
+
# Check default installation paths for manually installed tools
|
|
20
|
+
if tool_name == "bwfmetaedit":
|
|
21
|
+
exe_path = r"C:\Program Files\BWFMetaEdit\bwfmetaedit.exe"
|
|
22
|
+
if Path(exe_path).exists():
|
|
23
|
+
return True
|
|
24
|
+
elif tool_name == "exiftool":
|
|
25
|
+
exe_path = r"C:\Program Files\ExifTool\exiftool.exe"
|
|
26
|
+
if Path(exe_path).exists():
|
|
27
|
+
return True
|
|
28
|
+
elif tool_name == "id3v2":
|
|
29
|
+
wrapper_path = r"C:\Program Files\id3v2-wrapper\id3v2.bat"
|
|
30
|
+
if Path(wrapper_path).exists():
|
|
31
|
+
return True
|
|
32
|
+
try:
|
|
33
|
+
subprocess.run(["wsl", "--version"], capture_output=True, check=False)
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
pass
|
|
36
|
+
else:
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
# Check PATH
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
[tool_name, "--version" if tool_name != "ffprobe" else "-version"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
check=False,
|
|
46
|
+
)
|
|
47
|
+
return bool(result.stdout or result.stderr)
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def get_installed_version(self, package: str, expected_version: str | None = None) -> str | None: # noqa: ARG002
|
|
52
|
+
"""Get installed package version on Windows."""
|
|
53
|
+
# Handle different installation methods
|
|
54
|
+
if package == "id3v2":
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
["wsl", "id3v2", "--version"],
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
check=False,
|
|
61
|
+
)
|
|
62
|
+
if result.stdout or result.stderr:
|
|
63
|
+
output = result.stdout + result.stderr
|
|
64
|
+
match = re.search(r"(\d+\.\d+\.\d+)", output)
|
|
65
|
+
if match:
|
|
66
|
+
return match.group(1)
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
pass
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
if package == "bwfmetaedit":
|
|
72
|
+
exe_path = r"C:\Program Files\BWFMetaEdit\bwfmetaedit.exe"
|
|
73
|
+
try:
|
|
74
|
+
if not Path(exe_path).exists():
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
[exe_path, "--version"],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
check=False,
|
|
82
|
+
timeout=10,
|
|
83
|
+
)
|
|
84
|
+
output = result.stdout + result.stderr
|
|
85
|
+
|
|
86
|
+
if not output.strip() or result.returncode != 0:
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
[exe_path],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
check=False,
|
|
92
|
+
timeout=10,
|
|
93
|
+
)
|
|
94
|
+
output = result.stdout + result.stderr
|
|
95
|
+
|
|
96
|
+
if output:
|
|
97
|
+
patterns = [
|
|
98
|
+
r"(\d+\.\d+\.\d+)",
|
|
99
|
+
r"(\d+\.\d+)",
|
|
100
|
+
]
|
|
101
|
+
for pattern in patterns:
|
|
102
|
+
matches = re.findall(pattern, output)
|
|
103
|
+
for match in matches:
|
|
104
|
+
version = str(match)
|
|
105
|
+
if re.match(r"^\d+\.\d+", version):
|
|
106
|
+
return version
|
|
107
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
108
|
+
pass
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
if package == "exiftool":
|
|
112
|
+
exe_path = r"C:\Program Files\ExifTool\exiftool.exe"
|
|
113
|
+
try:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
[exe_path, "-ver"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
check=False,
|
|
119
|
+
)
|
|
120
|
+
if result.stdout:
|
|
121
|
+
version = result.stdout.strip()
|
|
122
|
+
if re.match(r"^\d+\.\d+", version):
|
|
123
|
+
return version
|
|
124
|
+
except FileNotFoundError:
|
|
125
|
+
pass
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# For Chocolatey packages
|
|
129
|
+
choco_version = None
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
["choco", "list", "--local-only", package, "--exact"],
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
check=False,
|
|
136
|
+
)
|
|
137
|
+
output = result.stdout + result.stderr
|
|
138
|
+
|
|
139
|
+
if output.strip():
|
|
140
|
+
pattern = rf"^{re.escape(package)}\s+(\S+)"
|
|
141
|
+
for raw_line in output.split("\n"):
|
|
142
|
+
line = raw_line.strip()
|
|
143
|
+
if not line or line.startswith("Chocolatey"):
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
match = re.match(pattern, line, re.IGNORECASE)
|
|
147
|
+
if match:
|
|
148
|
+
version = match.group(1)
|
|
149
|
+
if version.startswith(("v", "V")):
|
|
150
|
+
version = version[1:]
|
|
151
|
+
if re.match(r"^\d+\.\d+", version):
|
|
152
|
+
choco_version = version
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if "|" in line:
|
|
156
|
+
parts = line.split("|")
|
|
157
|
+
if len(parts) >= 2 and parts[0].strip().lower() == package.lower(): # noqa: PLR2004
|
|
158
|
+
version = parts[1].strip()
|
|
159
|
+
if version.startswith(("v", "V")):
|
|
160
|
+
version = version[1:]
|
|
161
|
+
if re.match(r"^\d+\.\d+", version):
|
|
162
|
+
choco_version = version
|
|
163
|
+
break
|
|
164
|
+
except FileNotFoundError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
if choco_version:
|
|
168
|
+
return choco_version
|
|
169
|
+
|
|
170
|
+
# Fallback: Get version from executable directly (for Chocolatey-installed tools)
|
|
171
|
+
# This handles cases where Chocolatey version detection fails but tool is installed
|
|
172
|
+
tool_name = "ffprobe" if package == "ffmpeg" else package
|
|
173
|
+
try:
|
|
174
|
+
version_flag = "-version" if tool_name == "ffprobe" else "--version"
|
|
175
|
+
result = subprocess.run(
|
|
176
|
+
[tool_name, version_flag],
|
|
177
|
+
capture_output=True,
|
|
178
|
+
text=True,
|
|
179
|
+
check=False,
|
|
180
|
+
)
|
|
181
|
+
output = result.stdout + result.stderr
|
|
182
|
+
if output:
|
|
183
|
+
# Extract version from output (look for patterns like "version 7.1.0" or "7.1.0")
|
|
184
|
+
patterns = [
|
|
185
|
+
r"version\s+(\d+\.\d+\.\d+)",
|
|
186
|
+
r"version\s+(\d+\.\d+)",
|
|
187
|
+
r"(\d+\.\d+\.\d+)",
|
|
188
|
+
r"(\d+\.\d+)",
|
|
189
|
+
]
|
|
190
|
+
for pattern in patterns:
|
|
191
|
+
matches = re.findall(pattern, output, re.IGNORECASE)
|
|
192
|
+
for match in matches:
|
|
193
|
+
version = str(match)
|
|
194
|
+
if re.match(r"^\d+\.\d+", version):
|
|
195
|
+
return version
|
|
196
|
+
except FileNotFoundError:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _versions_match(version1: str, version2: str) -> bool:
|
|
203
|
+
"""Check if two Windows version strings match (handles different precision).
|
|
204
|
+
|
|
205
|
+
Handles cases where versions have different precision levels:
|
|
206
|
+
- "7.1.0" matches "7.1" (v2 is prefix of v1)
|
|
207
|
+
- "7.1" matches "7.1.0" (v1 is prefix of v2)
|
|
208
|
+
- "7.1.0" matches "7.1.0" (exact match)
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
version1: First version string (e.g., "7.1.0")
|
|
212
|
+
version2: Second version string (e.g., "7.1")
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if versions match, False otherwise
|
|
216
|
+
"""
|
|
217
|
+
v1_normalized = OsDependenciesChecker._normalize_version(version1)
|
|
218
|
+
v2_normalized = OsDependenciesChecker._normalize_version(version2)
|
|
219
|
+
|
|
220
|
+
# Check exact match
|
|
221
|
+
if v1_normalized == v2_normalized:
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
# Check if one version is a prefix of the other
|
|
225
|
+
# "7.1" should match "7.1.0" (v2 starts with v1 + ".")
|
|
226
|
+
# "7.1.0" should match "7.1" (v1 starts with v2 + ".")
|
|
227
|
+
return v2_normalized.startswith(v1_normalized + ".") or v1_normalized.startswith(v2_normalized + ".")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Rating Compatibility Table Across Different Audio Players.
|
|
2
|
+
|
|
3
|
+
⚠️ AUTHORITATIVE SOURCE: This is the single source of truth for rating compatibility.
|
|
4
|
+
The README references this table for the complete details.
|
|
5
|
+
|
|
6
|
+
The following table shows how different audio players handle ratings across various audio formats.
|
|
7
|
+
Values represent the actual numbers written to files for each star rating (0-5 stars).
|
|
8
|
+
|
|
9
|
+
+----+----------------+------------+------------+------------+------------+---------+
|
|
10
|
+
| ⭐ | kid3 | Windows | MusicBee | Winamp | Traktor | iTunes |
|
|
11
|
+
| | /Lollypop |Media Player| | | | |
|
|
12
|
+
+----+---+-------+----+------------+------------+------------+------------+---------+
|
|
13
|
+
|ext.|mp3| wav |flac|mp3 wav flac|mp3 wav flac|mp3 wav flac|mp3 wav flac|W ops not|
|
|
14
|
+
+----+-----------+----+------------+------------+------------+------------+ +
|
|
15
|
+
|tags|id3|rif id3|vorb|id3 ✗ vorb|id3 id3 vorb|id3 ✗ vorb|id3 ✗ vorb|supported|
|
|
16
|
+
+----+---+-------+----+------------+------------+------------+------------+---------+
|
|
17
|
+
|None| ✗ ✗ ✗ ✗ | ✗ ✗ | ✗ ✗ ✗ | ✗ ✗ | 0 0 | |
|
|
18
|
+
| 0 | | | 0 0 0 | | | |
|
|
19
|
+
|0.5 | | |13 10 10 | | | |
|
|
20
|
+
| 1 | 1 20 1 20 | 1 20 | 1 20 20 | 1 20 | 51 51 | |
|
|
21
|
+
|1.5 | | |54 30 30 | | | |
|
|
22
|
+
| 2 |64 40 64 40 | 64 40 |64 40 40 | 64 40 |102 102 | |
|
|
23
|
+
|2.5 | | |118 50 50 | | | |
|
|
24
|
+
| 3 |128 60 128 60 | 128 60 |128 60 60 | 128 60 |153 153 | |
|
|
25
|
+
|3.5 | | |186 70 70 | | | |
|
|
26
|
+
| 4 |196 80 196 80 |196 80 80 | 196 80 | 196 80 |204 204 | |
|
|
27
|
+
|4.5 | | |242 90 90 | | | |
|
|
28
|
+
| 5 |255 100 255 100| 255 100 |255 100 100 | 255 100 |255 255 | |
|
|
29
|
+
+----+----------------+------------+------------+------------+------------+---------+
|
|
30
|
+
|Prof| A B A B | A ✗ B | A B B | A ✗ B | C ✗ C | ✗ |
|
|
31
|
+
+----+----------------+------------+------------+------------+------------+---------+
|
|
32
|
+
|
|
33
|
+
Legend:
|
|
34
|
+
id3 = id3v2
|
|
35
|
+
rif = RIFF
|
|
36
|
+
vorb = Vorbis
|
|
37
|
+
✗ = No tag written
|
|
38
|
+
empty = Rating value not supported
|
|
39
|
+
✓ = Can write ratings
|
|
40
|
+
|
|
41
|
+
- Rating Profiles:
|
|
42
|
+
A. 255 non-proportional: .mp3 id3v2 not Traktor, RIFF
|
|
43
|
+
B. 100 proportional: Vorbis not Traktor, .wav id3v2
|
|
44
|
+
C. 255 proportional: Traktor id3v2/Vorbis
|
|
45
|
+
|
|
46
|
+
- Key Point
|
|
47
|
+
Despite having different profiles, each rating value uniquely maps to one star value, enabling reliable star rating
|
|
48
|
+
interpretation regardless of the source profile.
|
|
49
|
+
|
|
50
|
+
- Example: All these values map to 3 stars
|
|
51
|
+
assert rating_to_stars(128) == 3.0 # Profile A
|
|
52
|
+
assert rating_to_stars(60) == 3.0 # Profile B
|
|
53
|
+
assert rating_to_stars(153) == 3.0 # Profile C
|
|
54
|
+
|
|
55
|
+
- Exception:
|
|
56
|
+
0 can either mean no rating (Traktor) or 0 stars (MusicBee).
|
|
57
|
+
Luckily, Traktor ratings are written with special tags making them easy to distinguish.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from collections.abc import Iterator
|
|
61
|
+
from enum import Enum
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RatingReadProfile(Enum):
|
|
65
|
+
"""Enumeration of rating read profiles for different audio formats."""
|
|
66
|
+
|
|
67
|
+
BASE_255_NON_PROPORTIONAL = (0, 13, 1, 54, 64, 118, 128, 186, 196, 242, 255)
|
|
68
|
+
BASE_255_PROPORTIONAL_TRAKTOR = (None, None, 51, None, 102, None, 153, None, 204, None, 255)
|
|
69
|
+
BASE_100_PROPORTIONAL = (0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
|
|
70
|
+
|
|
71
|
+
def __getitem__(self, index: int) -> int | None:
|
|
72
|
+
result = self.value[index]
|
|
73
|
+
return result if isinstance(result, int | type(None)) else int(result)
|
|
74
|
+
|
|
75
|
+
def __len__(self) -> int:
|
|
76
|
+
return len(self.value)
|
|
77
|
+
|
|
78
|
+
def __iter__(self) -> Iterator[int | None]:
|
|
79
|
+
return iter(self.value)
|
|
80
|
+
|
|
81
|
+
def __contains__(self, item: object) -> bool:
|
|
82
|
+
return item in self.value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
Regarding the ratings that the app will write in the audio files, the app currently uses the 2 most widely supported
|
|
87
|
+
profiles:
|
|
88
|
+
- 255 non proportional (ID3v2, RIFF)
|
|
89
|
+
- 100 proportional (Vorbis)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RatingWriteProfile(Enum):
|
|
94
|
+
"""Enumeration of rating write profiles for different audio formats."""
|
|
95
|
+
|
|
96
|
+
BASE_255_NON_PROPORTIONAL = RatingReadProfile.BASE_255_NON_PROPORTIONAL.value
|
|
97
|
+
BASE_100_PROPORTIONAL = RatingReadProfile.BASE_100_PROPORTIONAL.value
|
|
98
|
+
|
|
99
|
+
def __getitem__(self, index: int) -> int | None:
|
|
100
|
+
result = self.value[index]
|
|
101
|
+
return result if isinstance(result, int | type(None)) else int(result)
|
|
102
|
+
|
|
103
|
+
def __len__(self) -> int:
|
|
104
|
+
return len(self.value)
|
|
105
|
+
|
|
106
|
+
def __iter__(self) -> Iterator[int | None]:
|
|
107
|
+
return iter(self.value)
|
|
108
|
+
|
|
109
|
+
def __contains__(self, item: object) -> bool:
|
|
110
|
+
return item in self.value
|