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
audiometa/cli.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AudioMeta CLI - Command-line interface for audio metadata operations."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from audiometa import (
|
|
11
|
+
UnifiedMetadataKey,
|
|
12
|
+
delete_all_metadata,
|
|
13
|
+
get_full_metadata,
|
|
14
|
+
get_unified_metadata,
|
|
15
|
+
update_metadata,
|
|
16
|
+
validate_metadata_for_update,
|
|
17
|
+
)
|
|
18
|
+
from audiometa.exceptions import (
|
|
19
|
+
FileTypeNotSupportedError,
|
|
20
|
+
InvalidRatingValueError,
|
|
21
|
+
MetadataFormatNotSupportedByAudioFormatError,
|
|
22
|
+
)
|
|
23
|
+
from audiometa.utils.metadata_format import MetadataFormat
|
|
24
|
+
from audiometa.utils.types import UnifiedMetadata
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_output(data: Any, output_format: str) -> str:
|
|
28
|
+
"""Format output data according to specified format."""
|
|
29
|
+
if output_format == "json":
|
|
30
|
+
return json.dumps(data, indent=2)
|
|
31
|
+
if output_format == "yaml":
|
|
32
|
+
try:
|
|
33
|
+
import yaml # type: ignore[import-untyped]
|
|
34
|
+
|
|
35
|
+
result = yaml.dump(data, default_flow_style=False)
|
|
36
|
+
return str(result) if result is not None else ""
|
|
37
|
+
except ImportError:
|
|
38
|
+
sys.stderr.write("Warning: PyYAML not installed, falling back to JSON\n")
|
|
39
|
+
return json.dumps(data, indent=2)
|
|
40
|
+
elif output_format == "table":
|
|
41
|
+
return format_as_table(data)
|
|
42
|
+
else:
|
|
43
|
+
return str(data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _handle_file_operation_error(exception: Exception, file_path: Path | str, continue_on_error: bool) -> None:
|
|
47
|
+
"""Handle exceptions from file operations and write appropriate error messages to stderr.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
exception: The exception that was caught
|
|
51
|
+
file_path: The path to the file being operated on
|
|
52
|
+
continue_on_error: Whether to continue on errors or exit
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(exception, FileNotFoundError):
|
|
55
|
+
error_msg = f"Error: File not found: {file_path}\n"
|
|
56
|
+
elif isinstance(exception, FileTypeNotSupportedError):
|
|
57
|
+
error_msg = f"Error: File type not supported: {file_path}\n"
|
|
58
|
+
elif isinstance(exception, PermissionError | OSError):
|
|
59
|
+
error_msg = f"Error: {exception!s}\n"
|
|
60
|
+
else:
|
|
61
|
+
error_msg = f"Error: {exception!s}\n"
|
|
62
|
+
|
|
63
|
+
sys.stderr.write(error_msg)
|
|
64
|
+
|
|
65
|
+
if not continue_on_error:
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_as_table(data: dict[str, Any]) -> str:
|
|
70
|
+
"""Format metadata as a simple table."""
|
|
71
|
+
lines = []
|
|
72
|
+
|
|
73
|
+
# Handle unified metadata dict directly (from unified command)
|
|
74
|
+
if "unified_metadata" not in data and isinstance(data, dict):
|
|
75
|
+
# Check if this is a unified metadata dict (has UnifiedMetadataKey values)
|
|
76
|
+
unified_keys = {
|
|
77
|
+
"title",
|
|
78
|
+
"artists",
|
|
79
|
+
"album",
|
|
80
|
+
"album_artists",
|
|
81
|
+
"genres_names",
|
|
82
|
+
"release_date",
|
|
83
|
+
"track_number",
|
|
84
|
+
"disc_number",
|
|
85
|
+
"disc_total",
|
|
86
|
+
"rating",
|
|
87
|
+
"bpm",
|
|
88
|
+
"language",
|
|
89
|
+
"composer",
|
|
90
|
+
"publisher",
|
|
91
|
+
"copyright",
|
|
92
|
+
"unsynchronized_lyrics",
|
|
93
|
+
"comment",
|
|
94
|
+
"replaygain",
|
|
95
|
+
"archival_location",
|
|
96
|
+
"isrc",
|
|
97
|
+
}
|
|
98
|
+
if unified_keys.intersection(set(data.keys())):
|
|
99
|
+
# This is a unified metadata dict, wrap it
|
|
100
|
+
data = {"unified_metadata": data}
|
|
101
|
+
|
|
102
|
+
if "unified_metadata" in data:
|
|
103
|
+
lines.append("=== UNIFIED METADATA ===")
|
|
104
|
+
for key, value in data["unified_metadata"].items():
|
|
105
|
+
if value is not None:
|
|
106
|
+
lines.append(f"{key:20}: {value}")
|
|
107
|
+
lines.append("")
|
|
108
|
+
|
|
109
|
+
if "technical_info" in data:
|
|
110
|
+
lines.append("=== TECHNICAL INFO ===")
|
|
111
|
+
for key, value in data["technical_info"].items():
|
|
112
|
+
if value is not None:
|
|
113
|
+
lines.append(f"{key:20}: {value}")
|
|
114
|
+
lines.append("")
|
|
115
|
+
|
|
116
|
+
if "metadata_format" in data:
|
|
117
|
+
lines.append("=== FORMAT METADATA ===")
|
|
118
|
+
for metadata_format_name, format_data in data["metadata_format"].items():
|
|
119
|
+
if format_data:
|
|
120
|
+
lines.append(f"\n{metadata_format_name.upper()}:")
|
|
121
|
+
for key, value in format_data.items():
|
|
122
|
+
if value is not None:
|
|
123
|
+
lines.append(f" {key:18}: {value}")
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _read_metadata(args: argparse.Namespace) -> None:
|
|
129
|
+
"""Read and display metadata from audio file(s)."""
|
|
130
|
+
files = expand_file_patterns(
|
|
131
|
+
args.files, getattr(args, "recursive", False), getattr(args, "continue_on_error", False)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not files:
|
|
135
|
+
return # No files found, but continue_on_error was set
|
|
136
|
+
|
|
137
|
+
for file_path in files:
|
|
138
|
+
try:
|
|
139
|
+
if getattr(args, "format_type", None) == "unified":
|
|
140
|
+
metadata: Any = get_unified_metadata(file_path)
|
|
141
|
+
else:
|
|
142
|
+
metadata = get_full_metadata(
|
|
143
|
+
file_path,
|
|
144
|
+
include_headers=not getattr(args, "no_headers", False),
|
|
145
|
+
include_technical=not getattr(args, "no_technical", False),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
output = format_output(metadata, args.output_format)
|
|
149
|
+
|
|
150
|
+
if args.output:
|
|
151
|
+
try:
|
|
152
|
+
with Path(args.output).open("w") as f:
|
|
153
|
+
f.write(output)
|
|
154
|
+
except (PermissionError, OSError) as e:
|
|
155
|
+
_handle_file_operation_error(e, args.output, args.continue_on_error)
|
|
156
|
+
else:
|
|
157
|
+
sys.stdout.write(output)
|
|
158
|
+
if not output.endswith("\n"):
|
|
159
|
+
sys.stdout.write("\n")
|
|
160
|
+
|
|
161
|
+
except (FileTypeNotSupportedError, FileNotFoundError, PermissionError, OSError, Exception) as e:
|
|
162
|
+
_handle_file_operation_error(e, file_path, args.continue_on_error)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _write_metadata(args: argparse.Namespace) -> None:
|
|
166
|
+
"""Write metadata to audio file(s)."""
|
|
167
|
+
files = expand_file_patterns(
|
|
168
|
+
args.files, getattr(args, "recursive", False), getattr(args, "continue_on_error", False)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Build metadata dictionary from command line arguments
|
|
172
|
+
metadata: UnifiedMetadata = {}
|
|
173
|
+
|
|
174
|
+
# String fields
|
|
175
|
+
if args.title and args.title.strip():
|
|
176
|
+
metadata[UnifiedMetadataKey.TITLE] = args.title
|
|
177
|
+
if args.album and args.album.strip():
|
|
178
|
+
metadata[UnifiedMetadataKey.ALBUM] = args.album
|
|
179
|
+
if args.language and args.language.strip():
|
|
180
|
+
metadata[UnifiedMetadataKey.LANGUAGE] = args.language
|
|
181
|
+
if args.publisher and args.publisher.strip():
|
|
182
|
+
metadata[UnifiedMetadataKey.PUBLISHER] = args.publisher
|
|
183
|
+
if args.copyright and args.copyright.strip():
|
|
184
|
+
metadata[UnifiedMetadataKey.COPYRIGHT] = args.copyright
|
|
185
|
+
if args.lyrics and args.lyrics.strip():
|
|
186
|
+
metadata[UnifiedMetadataKey.UNSYNCHRONIZED_LYRICS] = args.lyrics
|
|
187
|
+
if args.comment and args.comment.strip():
|
|
188
|
+
metadata[UnifiedMetadataKey.COMMENT] = args.comment
|
|
189
|
+
if args.replaygain and args.replaygain.strip():
|
|
190
|
+
metadata[UnifiedMetadataKey.REPLAYGAIN] = args.replaygain
|
|
191
|
+
if args.archival_location and args.archival_location.strip():
|
|
192
|
+
metadata[UnifiedMetadataKey.ARCHIVAL_LOCATION] = args.archival_location
|
|
193
|
+
if args.isrc and args.isrc.strip():
|
|
194
|
+
metadata[UnifiedMetadataKey.ISRC] = args.isrc
|
|
195
|
+
|
|
196
|
+
# List fields (can be specified multiple times)
|
|
197
|
+
if args.artist:
|
|
198
|
+
artists = [a.strip() for a in args.artist if a and a.strip()]
|
|
199
|
+
if artists:
|
|
200
|
+
metadata[UnifiedMetadataKey.ARTISTS] = artists
|
|
201
|
+
if args.album_artists:
|
|
202
|
+
album_artists = [a.strip() for a in args.album_artists if a and a.strip()]
|
|
203
|
+
if album_artists:
|
|
204
|
+
metadata[UnifiedMetadataKey.ALBUM_ARTISTS] = album_artists
|
|
205
|
+
if args.genre:
|
|
206
|
+
genres = [g.strip() for g in args.genre if g and g.strip()]
|
|
207
|
+
if genres:
|
|
208
|
+
metadata[UnifiedMetadataKey.GENRES_NAMES] = genres
|
|
209
|
+
if args.composer:
|
|
210
|
+
composers = [c.strip() for c in args.composer if c and c.strip()]
|
|
211
|
+
if composers:
|
|
212
|
+
metadata[UnifiedMetadataKey.COMPOSERS] = composers
|
|
213
|
+
|
|
214
|
+
# Integer fields
|
|
215
|
+
if args.rating is not None:
|
|
216
|
+
if args.rating < 0:
|
|
217
|
+
sys.stderr.write("Error: rating cannot be negative\n")
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
metadata[UnifiedMetadataKey.RATING] = args.rating
|
|
220
|
+
if args.disc_number is not None:
|
|
221
|
+
if args.disc_number < 0:
|
|
222
|
+
sys.stderr.write("Error: disc-number cannot be negative\n")
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
metadata[UnifiedMetadataKey.DISC_NUMBER] = args.disc_number
|
|
225
|
+
if args.disc_total is not None:
|
|
226
|
+
if args.disc_total < 0:
|
|
227
|
+
sys.stderr.write("Error: disc-total cannot be negative\n")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
metadata[UnifiedMetadataKey.DISC_TOTAL] = args.disc_total
|
|
230
|
+
if args.bpm is not None:
|
|
231
|
+
if args.bpm < 0:
|
|
232
|
+
sys.stderr.write("Error: bpm cannot be negative\n")
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
metadata[UnifiedMetadataKey.BPM] = args.bpm
|
|
235
|
+
|
|
236
|
+
# Release date (year takes precedence over release-date if both specified)
|
|
237
|
+
if args.year is not None:
|
|
238
|
+
if args.year < 0:
|
|
239
|
+
sys.stderr.write("Error: year cannot be negative\n")
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
metadata[UnifiedMetadataKey.RELEASE_DATE] = str(args.year)
|
|
242
|
+
elif args.release_date and args.release_date.strip():
|
|
243
|
+
metadata[UnifiedMetadataKey.RELEASE_DATE] = args.release_date
|
|
244
|
+
|
|
245
|
+
# Track number (string, can be "5" or "5/12")
|
|
246
|
+
if args.track_number and args.track_number.strip():
|
|
247
|
+
metadata[UnifiedMetadataKey.TRACK_NUMBER] = args.track_number
|
|
248
|
+
|
|
249
|
+
# Check if any metadata was provided
|
|
250
|
+
if not metadata:
|
|
251
|
+
sys.stderr.write("Error: No metadata fields specified\n")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
validate_metadata_for_update(metadata)
|
|
256
|
+
except (ValueError, InvalidRatingValueError) as e:
|
|
257
|
+
sys.stderr.write(f"Error: {e}\n")
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
for file_path in files:
|
|
261
|
+
try:
|
|
262
|
+
update_kwargs: dict[str, Any] = {}
|
|
263
|
+
if hasattr(args, "force_format") and args.force_format:
|
|
264
|
+
format_map = {
|
|
265
|
+
"id3v2": MetadataFormat.ID3V2,
|
|
266
|
+
"id3v1": MetadataFormat.ID3V1,
|
|
267
|
+
"vorbis": MetadataFormat.VORBIS,
|
|
268
|
+
"riff": MetadataFormat.RIFF,
|
|
269
|
+
}
|
|
270
|
+
update_kwargs["metadata_format"] = format_map[args.force_format]
|
|
271
|
+
update_metadata(file_path, metadata, **update_kwargs)
|
|
272
|
+
if len(files) > 1:
|
|
273
|
+
sys.stdout.write(f"Updated metadata for: {file_path}\n")
|
|
274
|
+
else:
|
|
275
|
+
sys.stdout.write("Updated metadata\n")
|
|
276
|
+
|
|
277
|
+
except (FileTypeNotSupportedError, FileNotFoundError, PermissionError, OSError, Exception) as e:
|
|
278
|
+
if isinstance(e, MetadataFormatNotSupportedByAudioFormatError):
|
|
279
|
+
sys.stderr.write(f"Error: {e}\n")
|
|
280
|
+
if not args.continue_on_error:
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
else:
|
|
283
|
+
_handle_file_operation_error(e, file_path, args.continue_on_error)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _delete_metadata(args: argparse.Namespace) -> None:
|
|
287
|
+
"""Delete metadata from audio file(s)."""
|
|
288
|
+
files = expand_file_patterns(
|
|
289
|
+
args.files, getattr(args, "recursive", False), getattr(args, "continue_on_error", False)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
for file_path in files:
|
|
293
|
+
try:
|
|
294
|
+
success = delete_all_metadata(file_path)
|
|
295
|
+
if success:
|
|
296
|
+
if len(files) > 1:
|
|
297
|
+
sys.stdout.write(f"Deleted metadata from: {file_path}\n")
|
|
298
|
+
else:
|
|
299
|
+
sys.stdout.write("Deleted metadata\n")
|
|
300
|
+
else:
|
|
301
|
+
sys.stderr.write(f"Warning: No metadata found in: {file_path}\n")
|
|
302
|
+
|
|
303
|
+
except (FileTypeNotSupportedError, FileNotFoundError, PermissionError, OSError, Exception) as e:
|
|
304
|
+
_handle_file_operation_error(e, file_path, args.continue_on_error)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def expand_file_patterns(patterns: list[str], recursive: bool = False, continue_on_error: bool = False) -> list[Path]:
|
|
308
|
+
"""Expand file patterns and globs into a list of Path objects."""
|
|
309
|
+
files = []
|
|
310
|
+
|
|
311
|
+
for pattern in patterns:
|
|
312
|
+
path = Path(pattern)
|
|
313
|
+
|
|
314
|
+
if path.exists():
|
|
315
|
+
if path.is_file():
|
|
316
|
+
files.append(path)
|
|
317
|
+
elif path.is_dir() and recursive:
|
|
318
|
+
# Recursively find audio files
|
|
319
|
+
for ext in [".mp3", ".flac", ".wav"]:
|
|
320
|
+
files.extend(path.rglob(f"*{ext}"))
|
|
321
|
+
else:
|
|
322
|
+
# Try glob pattern
|
|
323
|
+
pattern_path = Path(pattern)
|
|
324
|
+
if "*" in pattern or "?" in pattern or "[" in pattern:
|
|
325
|
+
# Use glob for patterns
|
|
326
|
+
if pattern_path.is_absolute():
|
|
327
|
+
matches = list(pattern_path.parent.glob(pattern_path.name))
|
|
328
|
+
else:
|
|
329
|
+
matches = list(Path().glob(pattern))
|
|
330
|
+
else:
|
|
331
|
+
matches = [pattern_path]
|
|
332
|
+
for match in matches:
|
|
333
|
+
# Skip hidden files (those starting with .)
|
|
334
|
+
if not match.name.startswith(".") and match.is_file():
|
|
335
|
+
files.append(match)
|
|
336
|
+
|
|
337
|
+
if not files:
|
|
338
|
+
if continue_on_error:
|
|
339
|
+
sys.stderr.write("Warning: No valid audio files found\n")
|
|
340
|
+
return []
|
|
341
|
+
error_msg = "Error: No valid audio files found\n"
|
|
342
|
+
sys.stderr.write(error_msg)
|
|
343
|
+
sys.exit(1)
|
|
344
|
+
|
|
345
|
+
return files
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _create_parser() -> argparse.ArgumentParser:
|
|
349
|
+
"""Create and configure the argument parser."""
|
|
350
|
+
parser = argparse.ArgumentParser(
|
|
351
|
+
description="AudioMeta CLI - Command-line interface for audio metadata operations",
|
|
352
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
353
|
+
epilog="""
|
|
354
|
+
Examples:
|
|
355
|
+
audiometa read song.mp3 # Read full metadata
|
|
356
|
+
audiometa unified song.mp3 # Read unified metadata only
|
|
357
|
+
audiometa read *.mp3 --format table # Read multiple files as table
|
|
358
|
+
audiometa write song.mp3 --title "New Title" --artist "Artist"
|
|
359
|
+
audiometa delete song.mp3 # Delete all metadata
|
|
360
|
+
audiometa read music/ --recursive # Process directory recursively
|
|
361
|
+
""",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
365
|
+
|
|
366
|
+
# Read command
|
|
367
|
+
read_parser = subparsers.add_parser("read", help="Read metadata from audio file(s)")
|
|
368
|
+
read_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
|
|
369
|
+
read_parser.add_argument(
|
|
370
|
+
"--format",
|
|
371
|
+
choices=["json", "yaml", "table"],
|
|
372
|
+
default="json",
|
|
373
|
+
dest="output_format",
|
|
374
|
+
help="Output format (default: json)",
|
|
375
|
+
)
|
|
376
|
+
read_parser.add_argument("--output", "-o", help="Output file (default: stdout)")
|
|
377
|
+
read_parser.add_argument("--no-headers", action="store_true", help="Exclude header information")
|
|
378
|
+
read_parser.add_argument("--no-technical", action="store_true", help="Exclude technical information")
|
|
379
|
+
read_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
|
|
380
|
+
read_parser.add_argument(
|
|
381
|
+
"--continue-on-error", action="store_true", help="Continue processing other files on error"
|
|
382
|
+
)
|
|
383
|
+
read_parser.set_defaults(func=_read_metadata)
|
|
384
|
+
|
|
385
|
+
# Unified command
|
|
386
|
+
unified_parser = subparsers.add_parser("unified", help="Read unified metadata only")
|
|
387
|
+
unified_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
|
|
388
|
+
unified_parser.add_argument(
|
|
389
|
+
"--format",
|
|
390
|
+
choices=["json", "yaml", "table"],
|
|
391
|
+
default="json",
|
|
392
|
+
dest="output_format",
|
|
393
|
+
help="Output format (default: json)",
|
|
394
|
+
)
|
|
395
|
+
unified_parser.add_argument("--output", "-o", help="Output file (default: stdout)")
|
|
396
|
+
unified_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
|
|
397
|
+
unified_parser.add_argument(
|
|
398
|
+
"--continue-on-error", action="store_true", help="Continue processing other files on error"
|
|
399
|
+
)
|
|
400
|
+
unified_parser.set_defaults(func=_read_metadata, format_type="unified")
|
|
401
|
+
|
|
402
|
+
# Write command
|
|
403
|
+
write_parser = subparsers.add_parser("write", help="Write metadata to audio file(s)")
|
|
404
|
+
write_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
|
|
405
|
+
write_parser.add_argument("--title", help="Song title")
|
|
406
|
+
write_parser.add_argument(
|
|
407
|
+
"--artist", action="append", help="Artist name (can be specified multiple times for multiple artists)"
|
|
408
|
+
)
|
|
409
|
+
write_parser.add_argument("--album", help="Album name")
|
|
410
|
+
write_parser.add_argument(
|
|
411
|
+
"--album-artist",
|
|
412
|
+
action="append",
|
|
413
|
+
dest="album_artists",
|
|
414
|
+
help="Album artist name (can be specified multiple times)",
|
|
415
|
+
)
|
|
416
|
+
write_parser.add_argument("--year", type=int, help="Release year")
|
|
417
|
+
write_parser.add_argument("--release-date", help="Release date in YYYY or YYYY-MM-DD format")
|
|
418
|
+
write_parser.add_argument(
|
|
419
|
+
"--genre", action="append", help="Genre (can be specified multiple times for multiple genres)"
|
|
420
|
+
)
|
|
421
|
+
write_parser.add_argument("--track-number", help="Track number (e.g., '5' or '5/12')")
|
|
422
|
+
write_parser.add_argument("--disc-number", type=int, help="Disc number")
|
|
423
|
+
write_parser.add_argument("--disc-total", type=int, help="Total number of discs")
|
|
424
|
+
write_parser.add_argument("--rating", type=float, help="Rating value (integer or whole-number float like 196.0)")
|
|
425
|
+
write_parser.add_argument("--bpm", type=int, help="Beats per minute")
|
|
426
|
+
write_parser.add_argument("--language", help="Language code (3 characters, e.g., 'eng')")
|
|
427
|
+
write_parser.add_argument("--composer", action="append", help="Composer name (can be specified multiple times)")
|
|
428
|
+
write_parser.add_argument("--publisher", help="Publisher name")
|
|
429
|
+
write_parser.add_argument("--copyright", help="Copyright information")
|
|
430
|
+
write_parser.add_argument("--lyrics", help="Unsynchronized lyrics text")
|
|
431
|
+
write_parser.add_argument("--comment", help="Comment")
|
|
432
|
+
write_parser.add_argument("--replaygain", help="ReplayGain information")
|
|
433
|
+
write_parser.add_argument("--archival-location", help="Archival location")
|
|
434
|
+
write_parser.add_argument("--isrc", help="International Standard Recording Code (12 characters)")
|
|
435
|
+
write_parser.add_argument(
|
|
436
|
+
"--force-format",
|
|
437
|
+
choices=["id3v2", "id3v1", "vorbis", "riff"],
|
|
438
|
+
help="Force a specific metadata format (id3v2, id3v1, vorbis, or riff)",
|
|
439
|
+
)
|
|
440
|
+
write_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
|
|
441
|
+
write_parser.add_argument(
|
|
442
|
+
"--continue-on-error", action="store_true", help="Continue processing other files on error"
|
|
443
|
+
)
|
|
444
|
+
write_parser.set_defaults(func=_write_metadata)
|
|
445
|
+
|
|
446
|
+
# Delete command
|
|
447
|
+
delete_parser = subparsers.add_parser("delete", help="Delete all metadata from audio file(s)")
|
|
448
|
+
delete_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
|
|
449
|
+
delete_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
|
|
450
|
+
delete_parser.add_argument(
|
|
451
|
+
"--continue-on-error", action="store_true", help="Continue processing other files on error"
|
|
452
|
+
)
|
|
453
|
+
delete_parser.set_defaults(func=_delete_metadata)
|
|
454
|
+
|
|
455
|
+
return parser
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def main() -> None:
|
|
459
|
+
"""Main CLI entry point."""
|
|
460
|
+
parser = _create_parser()
|
|
461
|
+
args = parser.parse_args()
|
|
462
|
+
|
|
463
|
+
if not args.command:
|
|
464
|
+
parser.print_help()
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
args.func(args)
|
|
469
|
+
except KeyboardInterrupt:
|
|
470
|
+
sys.exit(1)
|
|
471
|
+
except Exception:
|
|
472
|
+
sys.exit(1)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if __name__ == "__main__":
|
|
476
|
+
main()
|
audiometa/exceptions.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Custom exceptions for the audiometa library.
|
|
2
|
+
|
|
3
|
+
This module defines all custom exception classes used throughout the audiometa library for handling various error
|
|
4
|
+
conditions related to audio file metadata processing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileCorruptedError(Exception):
|
|
9
|
+
"""Raised when an audio file appears to be corrupted or invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlacMd5CheckFailedError(FileCorruptedError):
|
|
13
|
+
"""Raised when FLAC MD5 checksum verification fails."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileByteMismatchError(FileCorruptedError):
|
|
17
|
+
"""Raised when file bytes do not match expected content."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InvalidChunkDecodeError(FileCorruptedError):
|
|
21
|
+
"""Raised when a chunk cannot be decoded properly."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DurationNotFoundError(FileCorruptedError):
|
|
25
|
+
"""Raised when audio duration cannot be determined."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AudioFileMetadataParseError(FileCorruptedError):
|
|
29
|
+
"""Raised when audio file metadata cannot be parsed from external tools.
|
|
30
|
+
|
|
31
|
+
This error indicates that the output from tools like ffprobe could not be
|
|
32
|
+
parsed as valid JSON or metadata format.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
- ffprobe returns invalid JSON when probing audio files
|
|
36
|
+
- Metadata parsing fails due to unexpected output format
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FileTypeNotSupportedError(Exception):
|
|
41
|
+
"""Raised when the audio file type is not supported by the library."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConfigurationError(Exception):
|
|
45
|
+
"""Raised when there is a configuration error in the metadata manager.
|
|
46
|
+
|
|
47
|
+
This error indicates that the metadata manager was not properly configured or initialized with the required
|
|
48
|
+
parameters.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MetadataFormatNotSupportedByAudioFormatError(Exception):
|
|
53
|
+
"""Raised when attempting to read metadata from a format not supported by the audio format of the file.
|
|
54
|
+
|
|
55
|
+
This error indicates that the requested metadata format is not supported by the audio format of the file.
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
- Trying to read metadata from RIFF format from an MP3 file
|
|
59
|
+
- Trying to read metadata from Vorbis format from a WAV file
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MetadataFieldNotSupportedByMetadataFormatError(Exception):
|
|
64
|
+
"""Raised when attempting to read or write metadata not supported by the format.
|
|
65
|
+
|
|
66
|
+
This error indicates a format limitation (e.g., trying to write BPM to RIFF),
|
|
67
|
+
not a code error. The format simply does not support the requested metadata field.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
- Trying to read/write ratings to RIFF
|
|
71
|
+
- Trying to read/write BPM to ID3v1
|
|
72
|
+
- Trying to read/write album artist to ID3v1
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MetadataFieldNotSupportedByLibError(Exception):
|
|
77
|
+
"""Raised when attempting to read or write a metadata field that is not supported by the library at all.
|
|
78
|
+
|
|
79
|
+
This error indicates that the requested metadata field is not implemented or supported
|
|
80
|
+
by any format in the library, regardless of the audio file format.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
- Trying to read/write a custom field that doesn't exist in UnifiedMetadataKey
|
|
84
|
+
- Trying to read/write a field that is not implemented in any metadata manager
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MetadataWritingConflictParametersError(Exception):
|
|
89
|
+
"""Raised when conflicting metadata writing parameters are specified.
|
|
90
|
+
|
|
91
|
+
This error indicates that the user has provided parameters that cannot
|
|
92
|
+
be used together for metadata writing operations.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
- Specifying both metadata_strategy and metadata_format
|
|
96
|
+
- Other mutually exclusive metadata writing parameters
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class InvalidMetadataFieldTypeError(TypeError):
|
|
101
|
+
"""Raised when a metadata field value has an unexpected type.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
field: str - the unified metadata field name (e.g. 'artists')
|
|
105
|
+
expected_type: str - human-readable expected type (e.g. 'list[str]')
|
|
106
|
+
actual_type: str - name of the actual type received
|
|
107
|
+
value: object - the actual value passed
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, field: str, expected_type: str, actual_value: object):
|
|
111
|
+
"""Initialize the exception with field details.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
field: The unified metadata field name.
|
|
115
|
+
expected_type: Human-readable expected type.
|
|
116
|
+
actual_value: The actual value that was passed.
|
|
117
|
+
"""
|
|
118
|
+
actual_type = type(actual_value).__name__
|
|
119
|
+
message = (
|
|
120
|
+
f"Invalid type for metadata field '{field}': expected {expected_type}, "
|
|
121
|
+
f"got {actual_type} (value={actual_value!r})"
|
|
122
|
+
)
|
|
123
|
+
super().__init__(message)
|
|
124
|
+
self.field = field
|
|
125
|
+
self.expected_type = expected_type
|
|
126
|
+
self.actual_type = actual_type
|
|
127
|
+
self.value = actual_value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class InvalidRatingValueError(Exception):
|
|
131
|
+
"""Raised when an invalid rating value is provided.
|
|
132
|
+
|
|
133
|
+
This error indicates that the rating value cannot be converted to a valid
|
|
134
|
+
numeric rating or is not in the expected format.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
- Non-numeric string values like "invalid" or "abc"
|
|
138
|
+
- Values that cannot be converted to integers
|
|
139
|
+
- None values when a rating is expected
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class InvalidMetadataFieldFormatError(ValueError):
|
|
144
|
+
"""Raised when a metadata field value has an invalid format.
|
|
145
|
+
|
|
146
|
+
This error indicates that the value has the correct type but does not
|
|
147
|
+
match the required format pattern.
|
|
148
|
+
|
|
149
|
+
Attributes:
|
|
150
|
+
field: str - the unified metadata field name (e.g. 'release_date')
|
|
151
|
+
expected_format: str - human-readable expected format (e.g. 'YYYY or YYYY-MM-DD')
|
|
152
|
+
value: object - the actual value passed
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, field: str, expected_format: str, actual_value: object):
|
|
156
|
+
"""Initialize the exception with field format details.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
field: The unified metadata field name.
|
|
160
|
+
expected_format: Human-readable expected format.
|
|
161
|
+
actual_value: The actual value that was passed.
|
|
162
|
+
"""
|
|
163
|
+
message = f"Invalid format for metadata field '{field}': expected {expected_format}, got {actual_value!r}"
|
|
164
|
+
super().__init__(message)
|
|
165
|
+
self.field = field
|
|
166
|
+
self.expected_format = expected_format
|
|
167
|
+
self.value = actual_value
|