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/__init__.py
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
"""Audio metadata handling module.
|
|
2
|
+
|
|
3
|
+
A comprehensive Python library for reading and writing audio metadata across multiple formats
|
|
4
|
+
including MP3, FLAC, WAV, and more. Supports ID3v1, ID3v2, Vorbis (FLAC), and RIFF (WAV) formats
|
|
5
|
+
with 15+ metadata fields including title, artist, album, rating, BPM, and more.
|
|
6
|
+
|
|
7
|
+
Note: OGG file support is planned but not yet implemented.
|
|
8
|
+
|
|
9
|
+
For detailed metadata support information, see the README.md file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import warnings
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Union, cast
|
|
16
|
+
|
|
17
|
+
from ._audio_file import _AudioFile
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
FileCorruptedError,
|
|
20
|
+
FileTypeNotSupportedError,
|
|
21
|
+
InvalidMetadataFieldTypeError,
|
|
22
|
+
MetadataFieldNotSupportedByLibError,
|
|
23
|
+
MetadataFieldNotSupportedByMetadataFormatError,
|
|
24
|
+
MetadataFormatNotSupportedByAudioFormatError,
|
|
25
|
+
MetadataWritingConflictParametersError,
|
|
26
|
+
)
|
|
27
|
+
from .manager._MetadataManager import _MetadataManager
|
|
28
|
+
from .manager._rating_supporting._RatingSupportingMetadataManager import _RatingSupportingMetadataManager
|
|
29
|
+
from .manager._rating_supporting.id3v2._Id3v2Manager import _Id3v2Manager
|
|
30
|
+
from .manager._rating_supporting.riff._RiffManager import _RiffManager
|
|
31
|
+
from .manager._rating_supporting.vorbis._VorbisManager import _VorbisManager
|
|
32
|
+
from .manager.id3v1._Id3v1Manager import _Id3v1Manager
|
|
33
|
+
from .utils.metadata_format import MetadataFormat
|
|
34
|
+
from .utils.metadata_writing_strategy import MetadataWritingStrategy
|
|
35
|
+
from .utils.types import UnifiedMetadata, UnifiedMetadataValue
|
|
36
|
+
from .utils.unified_metadata_key import UnifiedMetadataKey
|
|
37
|
+
|
|
38
|
+
FILE_EXTENSION_NOT_HANDLED_MESSAGE = "The file's format is not handled by the service."
|
|
39
|
+
|
|
40
|
+
METADATA_FORMAT_MANAGER_CLASS_MAP: dict[MetadataFormat, type] = {
|
|
41
|
+
MetadataFormat.ID3V1: _Id3v1Manager,
|
|
42
|
+
MetadataFormat.ID3V2: _Id3v2Manager,
|
|
43
|
+
MetadataFormat.VORBIS: _VorbisManager,
|
|
44
|
+
MetadataFormat.RIFF: _RiffManager,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Public API: only accepts standard file path types (not _AudioFile)
|
|
48
|
+
type PublicFileType = str | Path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_metadata_manager(
|
|
52
|
+
audio_file: _AudioFile,
|
|
53
|
+
metadata_format: MetadataFormat | None = None,
|
|
54
|
+
normalized_rating_max_value: int | None = None,
|
|
55
|
+
id3v2_version: tuple[int, int, int] | None = None,
|
|
56
|
+
) -> _MetadataManager:
|
|
57
|
+
audio_file_prioritized_tag_formats = MetadataFormat.get_priorities().get(audio_file.file_extension)
|
|
58
|
+
if not audio_file_prioritized_tag_formats:
|
|
59
|
+
raise FileTypeNotSupportedError(FILE_EXTENSION_NOT_HANDLED_MESSAGE)
|
|
60
|
+
|
|
61
|
+
if not metadata_format:
|
|
62
|
+
metadata_format = audio_file_prioritized_tag_formats[0]
|
|
63
|
+
elif metadata_format not in audio_file_prioritized_tag_formats:
|
|
64
|
+
msg = f"Tag format {metadata_format} not supported for file extension {audio_file.file_extension}"
|
|
65
|
+
raise MetadataFormatNotSupportedByAudioFormatError(msg)
|
|
66
|
+
|
|
67
|
+
manager_class: type[_MetadataManager] = cast(Any, METADATA_FORMAT_MANAGER_CLASS_MAP[metadata_format])
|
|
68
|
+
if issubclass(manager_class, _RatingSupportingMetadataManager):
|
|
69
|
+
if manager_class is _Id3v2Manager:
|
|
70
|
+
# Determine ID3v2 version based on provided version or use default
|
|
71
|
+
version = id3v2_version if id3v2_version is not None else (2, 3, 0) # Default to ID3v2.3
|
|
72
|
+
id3v2_manager_class = cast(type[_Id3v2Manager], manager_class)
|
|
73
|
+
return cast(
|
|
74
|
+
_MetadataManager,
|
|
75
|
+
id3v2_manager_class(
|
|
76
|
+
audio_file=audio_file,
|
|
77
|
+
normalized_rating_max_value=normalized_rating_max_value,
|
|
78
|
+
id3v2_version=version,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
return manager_class(audio_file=audio_file, normalized_rating_max_value=normalized_rating_max_value) # type: ignore[call-arg]
|
|
82
|
+
return manager_class(audio_file=audio_file) # type: ignore[call-arg]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_metadata_managers(
|
|
86
|
+
audio_file: _AudioFile,
|
|
87
|
+
tag_formats: list[MetadataFormat] | None = None,
|
|
88
|
+
normalized_rating_max_value: int | None = None,
|
|
89
|
+
id3v2_version: tuple[int, int, int] | None = None,
|
|
90
|
+
) -> dict[MetadataFormat, _MetadataManager]:
|
|
91
|
+
managers = {}
|
|
92
|
+
|
|
93
|
+
if not tag_formats:
|
|
94
|
+
tag_formats = MetadataFormat.get_priorities().get(audio_file.file_extension)
|
|
95
|
+
if not tag_formats:
|
|
96
|
+
raise FileTypeNotSupportedError(FILE_EXTENSION_NOT_HANDLED_MESSAGE)
|
|
97
|
+
|
|
98
|
+
for metadata_format in tag_formats:
|
|
99
|
+
managers[metadata_format] = _get_metadata_manager(
|
|
100
|
+
audio_file=audio_file,
|
|
101
|
+
metadata_format=metadata_format,
|
|
102
|
+
normalized_rating_max_value=normalized_rating_max_value,
|
|
103
|
+
id3v2_version=id3v2_version,
|
|
104
|
+
)
|
|
105
|
+
return managers
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_unified_metadata(
|
|
109
|
+
file: PublicFileType,
|
|
110
|
+
normalized_rating_max_value: int | None = None,
|
|
111
|
+
id3v2_version: tuple[int, int, int] | None = None,
|
|
112
|
+
metadata_format: MetadataFormat | None = None,
|
|
113
|
+
) -> UnifiedMetadata:
|
|
114
|
+
"""Get metadata from a file, either unified across all formats or from a specific format only.
|
|
115
|
+
|
|
116
|
+
When metadata_format is None (default), this function reads metadata from all available
|
|
117
|
+
formats (ID3v1, ID3v2, Vorbis, RIFF) and returns a unified dictionary with the best
|
|
118
|
+
available data for each field.
|
|
119
|
+
|
|
120
|
+
When metadata_format is specified, this function reads metadata from only the specified
|
|
121
|
+
format, returning data from that format only.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
file: Audio file path (str or Path)
|
|
125
|
+
normalized_rating_max_value: Maximum value for rating normalization (0-10 scale).
|
|
126
|
+
When provided, ratings are normalized to this scale. Defaults to None (raw values).
|
|
127
|
+
id3v2_version: ID3v2 version tuple for ID3v2-specific operations
|
|
128
|
+
metadata_format: Specific metadata format to read from. If None, reads from all available formats.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary containing metadata fields
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
135
|
+
FileNotFoundError: If the file does not exist
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
# Get all metadata with raw rating values (unified)
|
|
139
|
+
metadata = get_unified_metadata("song.mp3")
|
|
140
|
+
print(metadata.get(UnifiedMetadataKey.TITLE))
|
|
141
|
+
|
|
142
|
+
# Get all metadata with normalized ratings (unified)
|
|
143
|
+
metadata = get_unified_metadata("song.mp3", normalized_rating_max_value=100)
|
|
144
|
+
print(metadata.get(UnifiedMetadataKey.RATING)) # Returns 0-100
|
|
145
|
+
|
|
146
|
+
# Get metadata from FLAC file (unified)
|
|
147
|
+
metadata = get_unified_metadata("song.flac")
|
|
148
|
+
print(metadata.get(UnifiedMetadataKey.ARTISTS))
|
|
149
|
+
|
|
150
|
+
# Get only ID3v2 metadata
|
|
151
|
+
metadata = get_unified_metadata("song.mp3", metadata_format=MetadataFormat.ID3V2)
|
|
152
|
+
print(metadata.get(UnifiedMetadataKey.TITLE))
|
|
153
|
+
|
|
154
|
+
# Get only Vorbis metadata from FLAC
|
|
155
|
+
metadata = get_unified_metadata("song.flac", metadata_format=MetadataFormat.VORBIS)
|
|
156
|
+
print(metadata.get(UnifiedMetadataKey.ARTISTS))
|
|
157
|
+
|
|
158
|
+
# Get ID3v2 metadata with normalized ratings
|
|
159
|
+
metadata = get_unified_metadata(
|
|
160
|
+
"song.mp3", metadata_format=MetadataFormat.ID3V2, normalized_rating_max_value=100
|
|
161
|
+
)
|
|
162
|
+
print(metadata.get(UnifiedMetadataKey.RATING)) # Returns 0-100
|
|
163
|
+
"""
|
|
164
|
+
audio_file = _AudioFile(file)
|
|
165
|
+
|
|
166
|
+
# If specific format requested, return data from that format only
|
|
167
|
+
if metadata_format is not None:
|
|
168
|
+
manager = _get_metadata_manager(
|
|
169
|
+
audio_file=audio_file,
|
|
170
|
+
metadata_format=metadata_format,
|
|
171
|
+
normalized_rating_max_value=normalized_rating_max_value,
|
|
172
|
+
id3v2_version=id3v2_version,
|
|
173
|
+
)
|
|
174
|
+
return manager.get_unified_metadata()
|
|
175
|
+
|
|
176
|
+
# Get all available managers for this file type
|
|
177
|
+
all_managers = _get_metadata_managers(
|
|
178
|
+
audio_file=audio_file, normalized_rating_max_value=normalized_rating_max_value, id3v2_version=id3v2_version
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Get file-specific format priorities
|
|
182
|
+
available_formats = MetadataFormat.get_priorities().get(audio_file.file_extension, [])
|
|
183
|
+
managers_by_precedence = []
|
|
184
|
+
|
|
185
|
+
for format_type in available_formats:
|
|
186
|
+
if format_type in all_managers:
|
|
187
|
+
managers_by_precedence.append((format_type, all_managers[format_type]))
|
|
188
|
+
|
|
189
|
+
result: dict[UnifiedMetadataKey, UnifiedMetadataValue] = {}
|
|
190
|
+
for unified_metadata_key in UnifiedMetadataKey:
|
|
191
|
+
for _format_type, manager in managers_by_precedence:
|
|
192
|
+
try:
|
|
193
|
+
unified_metadata = manager.get_unified_metadata()
|
|
194
|
+
if unified_metadata_key in unified_metadata:
|
|
195
|
+
value = unified_metadata[unified_metadata_key]
|
|
196
|
+
if value is not None:
|
|
197
|
+
result[unified_metadata_key] = value
|
|
198
|
+
break
|
|
199
|
+
except Exception:
|
|
200
|
+
# If this manager fails, continue to the next one
|
|
201
|
+
continue
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_unified_metadata_field(
|
|
206
|
+
file: PublicFileType,
|
|
207
|
+
unified_metadata_key: str | UnifiedMetadataKey,
|
|
208
|
+
normalized_rating_max_value: int | None = None,
|
|
209
|
+
id3v2_version: tuple[int, int, int] | None = None,
|
|
210
|
+
metadata_format: MetadataFormat | None = None,
|
|
211
|
+
) -> UnifiedMetadataValue:
|
|
212
|
+
"""Get a specific unified metadata field from an audio file.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
file: Audio file path (str or Path)
|
|
216
|
+
unified_metadata_key: The metadata field to retrieve. Can be a UnifiedMetadataKey enum instance
|
|
217
|
+
or a string matching an enum value (e.g., "title").
|
|
218
|
+
normalized_rating_max_value: Maximum value for rating normalization (0-10 scale).
|
|
219
|
+
Only used when unified_metadata_key is RATING. For other metadata fields,
|
|
220
|
+
this parameter is ignored. Defaults to None (no normalization).
|
|
221
|
+
id3v2_version: ID3v2 version tuple for ID3v2-specific operations
|
|
222
|
+
metadata_format: Specific metadata format to read from. If None, uses priority order.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The metadata value or None if not found
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
MetadataFieldNotSupportedByMetadataFormatError: When metadata_format is specified and the field
|
|
229
|
+
is not supported by that format
|
|
230
|
+
MetadataFieldNotSupportedByLibError: When the field is not supported by any format in the library
|
|
231
|
+
(only when metadata_format is None and all formats raise MetadataFieldNotSupportedByMetadataFormatError)
|
|
232
|
+
|
|
233
|
+
Examples:
|
|
234
|
+
# Get title from any format (priority order)
|
|
235
|
+
title = get_unified_metadata_field("song.mp3", UnifiedMetadataKey.TITLE)
|
|
236
|
+
|
|
237
|
+
# Get title specifically from ID3v2
|
|
238
|
+
title = get_unified_metadata_field("song.mp3", UnifiedMetadataKey.TITLE, metadata_format=MetadataFormat.ID3V2)
|
|
239
|
+
|
|
240
|
+
# Get rating without normalization
|
|
241
|
+
rating = get_unified_metadata_field("song.mp3", UnifiedMetadataKey.RATING)
|
|
242
|
+
|
|
243
|
+
# Get rating with 0-100 normalization
|
|
244
|
+
rating = get_unified_metadata_field("song.mp3", UnifiedMetadataKey.RATING, normalized_rating_max_value=100)
|
|
245
|
+
|
|
246
|
+
# Handle format-specific errors
|
|
247
|
+
try:
|
|
248
|
+
bpm = get_unified_metadata_field("song.wav", UnifiedMetadataKey.BPM, metadata_format=MetadataFormat.RIFF)
|
|
249
|
+
except MetadataFieldNotSupportedByMetadataFormatError:
|
|
250
|
+
print("BPM not supported by RIFF format")
|
|
251
|
+
|
|
252
|
+
# Handle library-wide errors
|
|
253
|
+
try:
|
|
254
|
+
value = get_unified_metadata_field("song.mp3", UnifiedMetadataKey.SOME_FIELD)
|
|
255
|
+
except MetadataFieldNotSupportedByLibError:
|
|
256
|
+
print("Field not supported by any format in the library")
|
|
257
|
+
"""
|
|
258
|
+
unified_metadata_key = _ensure_unified_metadata_key(unified_metadata_key)
|
|
259
|
+
|
|
260
|
+
audio_file = _AudioFile(file)
|
|
261
|
+
|
|
262
|
+
if metadata_format is not None:
|
|
263
|
+
# Get metadata from specific format
|
|
264
|
+
manager = _get_metadata_manager(
|
|
265
|
+
audio_file=audio_file,
|
|
266
|
+
metadata_format=metadata_format,
|
|
267
|
+
normalized_rating_max_value=normalized_rating_max_value,
|
|
268
|
+
id3v2_version=id3v2_version,
|
|
269
|
+
)
|
|
270
|
+
try:
|
|
271
|
+
return manager.get_unified_metadata_field(unified_metadata_key=unified_metadata_key)
|
|
272
|
+
except MetadataFieldNotSupportedByMetadataFormatError:
|
|
273
|
+
# Re-raise format-specific errors to let the user know the field is not supported
|
|
274
|
+
raise
|
|
275
|
+
except Exception:
|
|
276
|
+
return None
|
|
277
|
+
else:
|
|
278
|
+
# Use priority order across all formats
|
|
279
|
+
managers_prioritized = _get_metadata_managers(
|
|
280
|
+
audio_file=audio_file, normalized_rating_max_value=normalized_rating_max_value, id3v2_version=id3v2_version
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Try each manager in priority order until we find a value
|
|
284
|
+
format_errors = []
|
|
285
|
+
for format_type, manager in managers_prioritized.items():
|
|
286
|
+
try:
|
|
287
|
+
value = manager.get_unified_metadata_field(unified_metadata_key=unified_metadata_key)
|
|
288
|
+
if value is not None:
|
|
289
|
+
return value
|
|
290
|
+
except MetadataFieldNotSupportedByMetadataFormatError as e:
|
|
291
|
+
# Track format-specific errors to determine if field is supported by library at all
|
|
292
|
+
format_errors.append((format_type, e))
|
|
293
|
+
except Exception:
|
|
294
|
+
# If this manager fails for other reasons, try the next one
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# If ALL managers raised MetadataFieldNotSupportedByMetadataFormatError,
|
|
298
|
+
# the field is not supported by the library at all
|
|
299
|
+
if len(format_errors) == len(managers_prioritized) and len(format_errors) > 0:
|
|
300
|
+
msg = f"{unified_metadata_key} metadata field is not supported by any format in the library"
|
|
301
|
+
raise MetadataFieldNotSupportedByLibError(msg)
|
|
302
|
+
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _ensure_unified_metadata_key(key: str | UnifiedMetadataKey) -> UnifiedMetadataKey:
|
|
307
|
+
"""Ensure a key is a UnifiedMetadataKey enum instance.
|
|
308
|
+
|
|
309
|
+
This function accepts both UnifiedMetadataKey enum instances and string values that match
|
|
310
|
+
enum values. Converts string keys to enum instances when they match. This provides runtime
|
|
311
|
+
validation since Python doesn't enforce type hints at runtime, allowing the function to catch
|
|
312
|
+
invalid inputs (e.g., invalid strings) that would otherwise cause confusing errors later in
|
|
313
|
+
the code.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
key: The metadata key to ensure. Can be a UnifiedMetadataKey enum instance or a string
|
|
317
|
+
matching an enum value (e.g., "title", "artist").
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The normalized UnifiedMetadataKey enum instance.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
MetadataFieldNotSupportedByLibError: When the key is not a valid UnifiedMetadataKey
|
|
324
|
+
(neither an enum instance nor a string matching an enum value).
|
|
325
|
+
"""
|
|
326
|
+
if isinstance(key, UnifiedMetadataKey):
|
|
327
|
+
return key
|
|
328
|
+
if isinstance(key, str):
|
|
329
|
+
for enum_member in UnifiedMetadataKey:
|
|
330
|
+
if enum_member.value == key:
|
|
331
|
+
return enum_member
|
|
332
|
+
msg = f"{key} metadata not supported by the library."
|
|
333
|
+
raise MetadataFieldNotSupportedByLibError(msg)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _validate_unified_metadata_types(unified_metadata: UnifiedMetadata) -> None:
|
|
337
|
+
"""Validate types of values in unified_metadata against UnifiedMetadataKey.get_optional_type().
|
|
338
|
+
|
|
339
|
+
Raises InvalidMetadataFieldTypeError when a value does not match the expected type. None values are allowed (used to
|
|
340
|
+
indicate removal of a field).
|
|
341
|
+
|
|
342
|
+
Note: This function only validates types, not formats. Format validation (e.g., release date, track number)
|
|
343
|
+
is handled separately.
|
|
344
|
+
"""
|
|
345
|
+
if not unified_metadata:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
from typing import get_args, get_origin
|
|
349
|
+
|
|
350
|
+
for raw_key, value in unified_metadata.items():
|
|
351
|
+
key = _ensure_unified_metadata_key(raw_key)
|
|
352
|
+
|
|
353
|
+
# Allow None to mean "remove this field"
|
|
354
|
+
if value is None:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
expected_type = key.get_optional_type()
|
|
359
|
+
except Exception as err:
|
|
360
|
+
msg = f"Cannot determine expected type for key: {key.value}"
|
|
361
|
+
raise TypeError(msg) from err
|
|
362
|
+
|
|
363
|
+
origin = get_origin(expected_type)
|
|
364
|
+
if origin is list:
|
|
365
|
+
# Expect a list of a particular type (e.g., list[str]). Do NOT allow
|
|
366
|
+
# single values of the inner type; callers must provide a list.
|
|
367
|
+
arg_types = get_args(expected_type)
|
|
368
|
+
item_type = arg_types[0] if arg_types else str
|
|
369
|
+
# Value must be a list and all items must be of the expected inner type
|
|
370
|
+
if not isinstance(value, list):
|
|
371
|
+
raise InvalidMetadataFieldTypeError(
|
|
372
|
+
key.value, f"list[{getattr(item_type, '__name__', str(item_type))}]", value
|
|
373
|
+
)
|
|
374
|
+
# Allow None values in lists - they will be filtered out automatically during writing
|
|
375
|
+
if not all(item is None or isinstance(item, item_type) for item in value):
|
|
376
|
+
raise InvalidMetadataFieldTypeError(
|
|
377
|
+
key.value, f"list[{getattr(item_type, '__name__', str(item_type))}]", value
|
|
378
|
+
)
|
|
379
|
+
elif origin == Union or (origin is not None and hasattr(origin, "__name__") and origin.__name__ == "UnionType"):
|
|
380
|
+
# Handle Union types (e.g., Union[int, str] or int | float)
|
|
381
|
+
arg_types = get_args(expected_type)
|
|
382
|
+
if not isinstance(value, arg_types):
|
|
383
|
+
type_names = ", ".join(getattr(t, "__name__", str(t)) if t is not None else "None" for t in arg_types)
|
|
384
|
+
raise InvalidMetadataFieldTypeError(key.value, f"Union[{type_names}]", value)
|
|
385
|
+
# expected_type is a plain type like str or int
|
|
386
|
+
elif not isinstance(value, expected_type):
|
|
387
|
+
# Special case for TRACK_NUMBER: allow int for writing convenience (returns string when reading)
|
|
388
|
+
if key == UnifiedMetadataKey.TRACK_NUMBER and isinstance(value, int | str):
|
|
389
|
+
continue
|
|
390
|
+
raise InvalidMetadataFieldTypeError(
|
|
391
|
+
key.value, getattr(expected_type, "__name__", str(expected_type)), value
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _validate_rating_value(unified_metadata: UnifiedMetadata, normalized_rating_max_value: int | None) -> None:
|
|
396
|
+
"""Validate rating value if present.
|
|
397
|
+
|
|
398
|
+
This is a shared helper used by both validate_metadata_for_update() and update_metadata().
|
|
399
|
+
"""
|
|
400
|
+
if UnifiedMetadataKey.RATING not in unified_metadata:
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
rating_value = unified_metadata[UnifiedMetadataKey.RATING]
|
|
404
|
+
if rating_value is None:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
if isinstance(rating_value, int | float):
|
|
408
|
+
# In raw mode (no normalization), only accept floats that can be parsed to int
|
|
409
|
+
# This allows the library to accept values like 196.0 as 196
|
|
410
|
+
if normalized_rating_max_value is None and isinstance(rating_value, float):
|
|
411
|
+
if rating_value.is_integer():
|
|
412
|
+
# Note: We can't modify the original dict here, caller handles this if needed
|
|
413
|
+
pass
|
|
414
|
+
else:
|
|
415
|
+
from .exceptions import InvalidRatingValueError
|
|
416
|
+
|
|
417
|
+
msg = (
|
|
418
|
+
f"Rating value {rating_value} is invalid. In raw mode, float values must be whole numbers "
|
|
419
|
+
f"(e.g., 196.0). Half-star values like {rating_value} require normalization."
|
|
420
|
+
)
|
|
421
|
+
raise InvalidRatingValueError(msg)
|
|
422
|
+
from .manager._rating_supporting._RatingSupportingMetadataManager import _RatingSupportingMetadataManager
|
|
423
|
+
|
|
424
|
+
_RatingSupportingMetadataManager.validate_rating_value(rating_value, normalized_rating_max_value)
|
|
425
|
+
else:
|
|
426
|
+
from .exceptions import InvalidRatingValueError
|
|
427
|
+
|
|
428
|
+
msg = f"Rating value must be numeric, got {type(rating_value).__name__}"
|
|
429
|
+
raise InvalidRatingValueError(msg)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _validate_metadata_field_formats(unified_metadata: UnifiedMetadata) -> None:
|
|
433
|
+
"""Validate format of metadata fields that have specific format requirements.
|
|
434
|
+
|
|
435
|
+
Validates release_date, track_number, disc_number, disc_total, and isrc formats.
|
|
436
|
+
This is a shared helper used by both validate_metadata_for_update() and update_metadata().
|
|
437
|
+
"""
|
|
438
|
+
# Validate release date if present and non-empty
|
|
439
|
+
if UnifiedMetadataKey.RELEASE_DATE in unified_metadata:
|
|
440
|
+
release_date_value = unified_metadata[UnifiedMetadataKey.RELEASE_DATE]
|
|
441
|
+
if release_date_value is not None and isinstance(release_date_value, str) and release_date_value:
|
|
442
|
+
_MetadataManager.validate_release_date(release_date_value)
|
|
443
|
+
|
|
444
|
+
# Validate track number if present and non-empty
|
|
445
|
+
if UnifiedMetadataKey.TRACK_NUMBER in unified_metadata:
|
|
446
|
+
track_number_value = unified_metadata[UnifiedMetadataKey.TRACK_NUMBER]
|
|
447
|
+
if track_number_value is not None and isinstance(track_number_value, str | int):
|
|
448
|
+
_MetadataManager.validate_track_number(track_number_value)
|
|
449
|
+
|
|
450
|
+
# Validate disc number if present and non-empty
|
|
451
|
+
if UnifiedMetadataKey.DISC_NUMBER in unified_metadata:
|
|
452
|
+
disc_number_value = unified_metadata[UnifiedMetadataKey.DISC_NUMBER]
|
|
453
|
+
if disc_number_value is not None and isinstance(disc_number_value, int):
|
|
454
|
+
_MetadataManager.validate_disc_number(disc_number_value)
|
|
455
|
+
|
|
456
|
+
# Validate disc total if present
|
|
457
|
+
if UnifiedMetadataKey.DISC_TOTAL in unified_metadata:
|
|
458
|
+
disc_total_value = unified_metadata[UnifiedMetadataKey.DISC_TOTAL]
|
|
459
|
+
if disc_total_value is None or isinstance(disc_total_value, int):
|
|
460
|
+
_MetadataManager.validate_disc_total(disc_total_value)
|
|
461
|
+
|
|
462
|
+
# Validate ISRC format if present and non-empty
|
|
463
|
+
if UnifiedMetadataKey.ISRC in unified_metadata:
|
|
464
|
+
isrc_value = unified_metadata[UnifiedMetadataKey.ISRC]
|
|
465
|
+
if isrc_value is not None and isinstance(isrc_value, str) and isrc_value:
|
|
466
|
+
_MetadataManager.validate_isrc(isrc_value)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def validate_metadata_for_update(
|
|
470
|
+
unified_metadata: dict[UnifiedMetadataKey, Any] | UnifiedMetadata,
|
|
471
|
+
normalized_rating_max_value: int | None = None,
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Validate unified metadata values before updating metadata in a file.
|
|
474
|
+
|
|
475
|
+
This function validates that a metadata dictionary contains at least one field and validates
|
|
476
|
+
the types and formats of values. None values (which indicate field removal), empty strings,
|
|
477
|
+
empty lists, and lists containing None values are all considered valid metadata values.
|
|
478
|
+
|
|
479
|
+
Additionally validates rating, release date, and track number values if present (and non-empty):
|
|
480
|
+
- Rating values are validated using the same validation logic as the rating-supporting
|
|
481
|
+
metadata managers
|
|
482
|
+
- Release date values are validated for correct format (YYYY or YYYY-MM-DD)
|
|
483
|
+
- Track number values are validated for correct format (simple number or number with separator)
|
|
484
|
+
|
|
485
|
+
Note: For list-type fields (e.g., ARTISTS, GENRES), lists containing None values like
|
|
486
|
+
[None, None] are allowed. During writing, None values are automatically filtered out,
|
|
487
|
+
and if all values are filtered out, the field is removed (set to None).
|
|
488
|
+
|
|
489
|
+
String keys that match UnifiedMetadataKey enum values are automatically converted to
|
|
490
|
+
enum instances and validated. This allows using both string keys (e.g., "title") and
|
|
491
|
+
enum keys (e.g., UnifiedMetadataKey.TITLE) for validation.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
unified_metadata: Dictionary containing metadata to validate. Keys can be strings
|
|
495
|
+
matching UnifiedMetadataKey enum values or UnifiedMetadataKey enum instances.
|
|
496
|
+
normalized_rating_max_value: Maximum value for rating normalization (0-10 scale).
|
|
497
|
+
When provided, ratings are validated against this scale. Defaults to None (raw values).
|
|
498
|
+
|
|
499
|
+
Raises:
|
|
500
|
+
ValueError: If no metadata fields are specified (empty dict)
|
|
501
|
+
InvalidRatingValueError: If rating value is invalid
|
|
502
|
+
InvalidMetadataFieldFormatError: If release date or track number format is invalid
|
|
503
|
+
MetadataFieldNotSupportedByLibError: If a string key doesn't match any UnifiedMetadataKey enum value
|
|
504
|
+
|
|
505
|
+
Examples:
|
|
506
|
+
>>> from audiometa import validate_metadata_for_update, UnifiedMetadataKey
|
|
507
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TITLE: "Song Title"})
|
|
508
|
+
>>> validate_metadata_for_update({"title": "Song Title"}) # Valid
|
|
509
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TITLE: ""})
|
|
510
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.ARTISTS: []})
|
|
511
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.ARTISTS: [None, None]})
|
|
512
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TITLE: None})
|
|
513
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: 50}, normalized_rating_max_value=100)
|
|
514
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: 1.5}, normalized_rating_max_value=10)
|
|
515
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: 0}, normalized_rating_max_value=100)
|
|
516
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: 100}, normalized_rating_max_value=100)
|
|
517
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: -1}) # Error
|
|
518
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: 101}, normalized_rating_max_value=100) # Error
|
|
519
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RATING: 33}, normalized_rating_max_value=100) # Error
|
|
520
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RELEASE_DATE: "2024-01-01"})
|
|
521
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.RELEASE_DATE: "2024/01/01"}) # Error
|
|
522
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TRACK_NUMBER: "5"}) # Valid
|
|
523
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TRACK_NUMBER: 5}) # Valid
|
|
524
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TRACK_NUMBER: "5/12"}) # Valid
|
|
525
|
+
>>> validate_metadata_for_update({UnifiedMetadataKey.TRACK_NUMBER: "/12"}) # Error
|
|
526
|
+
"""
|
|
527
|
+
if not unified_metadata:
|
|
528
|
+
msg = "no metadata fields specified"
|
|
529
|
+
raise ValueError(msg)
|
|
530
|
+
|
|
531
|
+
# Convert string keys to UnifiedMetadataKey enum instances
|
|
532
|
+
normalized_metadata: dict[UnifiedMetadataKey, Any] = {}
|
|
533
|
+
for key, value in unified_metadata.items():
|
|
534
|
+
normalized_key = _ensure_unified_metadata_key(key)
|
|
535
|
+
normalized_metadata[normalized_key] = value
|
|
536
|
+
|
|
537
|
+
# Validate types
|
|
538
|
+
_validate_unified_metadata_types(normalized_metadata)
|
|
539
|
+
|
|
540
|
+
# Validate rating if present
|
|
541
|
+
_validate_rating_value(normalized_metadata, normalized_rating_max_value)
|
|
542
|
+
|
|
543
|
+
# Validate field formats (release_date, track_number, disc_number, disc_total, isrc)
|
|
544
|
+
_validate_metadata_field_formats(normalized_metadata)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def update_metadata(
|
|
548
|
+
file: PublicFileType,
|
|
549
|
+
unified_metadata: dict[UnifiedMetadataKey, Any] | UnifiedMetadata,
|
|
550
|
+
normalized_rating_max_value: int | None = None,
|
|
551
|
+
id3v2_version: tuple[int, int, int] | None = None,
|
|
552
|
+
metadata_strategy: MetadataWritingStrategy | None = None,
|
|
553
|
+
metadata_format: MetadataFormat | None = None,
|
|
554
|
+
fail_on_unsupported_field: bool = False,
|
|
555
|
+
) -> None:
|
|
556
|
+
"""Update metadata in an audio file.
|
|
557
|
+
|
|
558
|
+
This function writes metadata to the specified audio file using the appropriate
|
|
559
|
+
format manager. It supports multiple writing strategies and format selection.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
file: Audio file path (str or Path)
|
|
563
|
+
unified_metadata: Dictionary containing metadata to write
|
|
564
|
+
normalized_rating_max_value: Maximum value for rating normalization (0-10 scale).
|
|
565
|
+
When provided, ratings are normalized to this scale. Defaults to None (raw values).
|
|
566
|
+
Half-star ratings (e.g., 1.5, 2.5, 3.5) are supported to be consistent with classic star rating
|
|
567
|
+
systems that allow half-star increments.
|
|
568
|
+
id3v2_version: ID3v2 version tuple for ID3v2-specific operations
|
|
569
|
+
metadata_strategy: Writing strategy (SYNC, PRESERVE, CLEANUP). Defaults to SYNC.
|
|
570
|
+
Ignored when metadata_format is specified.
|
|
571
|
+
metadata_format: Specific format to write to. If None, uses the file's native format.
|
|
572
|
+
When specified, strategy is ignored and metadata is written only to this format.
|
|
573
|
+
fail_on_unsupported_field: If True, fails when any metadata field is not supported by the target format.
|
|
574
|
+
Applies to all strategies (SYNC, PRESERVE, CLEANUP). Defaults to False (graceful handling with warnings).
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
None
|
|
578
|
+
|
|
579
|
+
Raises:
|
|
580
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
581
|
+
FileNotFoundError: If the file does not exist
|
|
582
|
+
MetadataFieldNotSupportedByMetadataFormatError: If the metadata field is not supported by
|
|
583
|
+
the format (only for PRESERVE, CLEANUP strategies)
|
|
584
|
+
MetadataFieldNotSupportedByLibError: If any key in unified_metadata is not a valid UnifiedMetadataKey enum value
|
|
585
|
+
MetadataWritingConflictParametersError: If both metadata_strategy and metadata_format are specified
|
|
586
|
+
InvalidRatingValueError: If invalid rating values are provided
|
|
587
|
+
InvalidMetadataFieldFormatError: If release date or track number format is invalid
|
|
588
|
+
|
|
589
|
+
Note:
|
|
590
|
+
Cannot specify both metadata_strategy and metadata_format simultaneously. Choose one approach:
|
|
591
|
+
|
|
592
|
+
- Use metadata_strategy for multi-format management (SYNC, PRESERVE, CLEANUP)
|
|
593
|
+
- Use metadata_format for single-format writing (writes only to specified format)
|
|
594
|
+
|
|
595
|
+
When metadata_format is specified, metadata is written only to that format and unsupported
|
|
596
|
+
fields will raise MetadataFieldNotSupportedByMetadataFormatError.
|
|
597
|
+
|
|
598
|
+
When metadata_strategy is used, unsupported metadata fields are handled based on the
|
|
599
|
+
fail_on_unsupported_field parameter: True raises MetadataFieldNotSupportedByMetadataFormatError, False (default)
|
|
600
|
+
handles gracefully with warnings.
|
|
601
|
+
|
|
602
|
+
Data Filtering:
|
|
603
|
+
For list-type metadata fields (e.g., ARTISTS, GENRES), empty strings and None values
|
|
604
|
+
are automatically filtered out before writing. If all values in a list are filtered out,
|
|
605
|
+
the field is removed entirely (set to None). This ensures clean metadata without empty
|
|
606
|
+
or invalid entries across all supported formats.
|
|
607
|
+
|
|
608
|
+
Examples:
|
|
609
|
+
# Basic metadata update
|
|
610
|
+
metadata = {
|
|
611
|
+
UnifiedMetadataKey.TITLE: "New Title",
|
|
612
|
+
UnifiedMetadataKey.ARTISTS: ["Artist Name"]
|
|
613
|
+
}
|
|
614
|
+
update_metadata("song.mp3", metadata)
|
|
615
|
+
|
|
616
|
+
# Update with rating normalization
|
|
617
|
+
metadata = {
|
|
618
|
+
UnifiedMetadataKey.TITLE: "New Title",
|
|
619
|
+
UnifiedMetadataKey.RATING: 75 # Will be normalized to 0-100 scale
|
|
620
|
+
}
|
|
621
|
+
update_metadata("song.mp3", metadata, normalized_rating_max_value=100)
|
|
622
|
+
|
|
623
|
+
# Clean up other formats (remove ID3v1, keep only ID3v2)
|
|
624
|
+
update_metadata("song.mp3", metadata, metadata_strategy=MetadataWritingStrategy.CLEANUP)
|
|
625
|
+
|
|
626
|
+
# Write to specific format
|
|
627
|
+
update_metadata("song.mp3", metadata, metadata_format=MetadataFormat.ID3V2)
|
|
628
|
+
|
|
629
|
+
# Remove specific fields by setting them to None
|
|
630
|
+
update_metadata("song.mp3", {
|
|
631
|
+
UnifiedMetadataKey.TITLE: None, # Removes title field
|
|
632
|
+
UnifiedMetadataKey.ARTISTS: None # Removes artist field
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
# Automatic filtering of empty values
|
|
636
|
+
metadata = {
|
|
637
|
+
UnifiedMetadataKey.ARTISTS: ["", "Artist 1", " ", "Artist 2", None]
|
|
638
|
+
}
|
|
639
|
+
# Results in: ["Artist 1", "Artist 2"] - empty strings and None filtered out
|
|
640
|
+
update_metadata("song.mp3", metadata)
|
|
641
|
+
"""
|
|
642
|
+
audio_file = _AudioFile(file)
|
|
643
|
+
|
|
644
|
+
# Validate that both parameters are not specified simultaneously
|
|
645
|
+
if metadata_strategy is not None and metadata_format is not None:
|
|
646
|
+
msg = (
|
|
647
|
+
"Cannot specify both metadata_strategy and metadata_format. "
|
|
648
|
+
"When metadata_format is specified, strategy is not applicable. "
|
|
649
|
+
"Choose either: use metadata_strategy for multi-format management, "
|
|
650
|
+
"or metadata_format for single-format writing."
|
|
651
|
+
)
|
|
652
|
+
raise MetadataWritingConflictParametersError(msg)
|
|
653
|
+
|
|
654
|
+
# Default to SYNC strategy if not specified
|
|
655
|
+
if metadata_strategy is None:
|
|
656
|
+
metadata_strategy = MetadataWritingStrategy.SYNC
|
|
657
|
+
|
|
658
|
+
# Handle strategy-specific behavior before writing
|
|
659
|
+
# Validate provided unified_metadata value types before attempting any writes
|
|
660
|
+
_validate_unified_metadata_types(unified_metadata)
|
|
661
|
+
|
|
662
|
+
# Validate rating if present
|
|
663
|
+
_validate_rating_value(unified_metadata, normalized_rating_max_value)
|
|
664
|
+
|
|
665
|
+
# Validate field formats (release_date, track_number, disc_number, disc_total, isrc)
|
|
666
|
+
_validate_metadata_field_formats(unified_metadata)
|
|
667
|
+
|
|
668
|
+
_handle_metadata_strategy(
|
|
669
|
+
audio_file,
|
|
670
|
+
unified_metadata,
|
|
671
|
+
metadata_strategy,
|
|
672
|
+
normalized_rating_max_value,
|
|
673
|
+
id3v2_version,
|
|
674
|
+
metadata_format,
|
|
675
|
+
fail_on_unsupported_field,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _handle_metadata_strategy(
|
|
680
|
+
audio_file: _AudioFile,
|
|
681
|
+
unified_metadata: UnifiedMetadata,
|
|
682
|
+
strategy: MetadataWritingStrategy,
|
|
683
|
+
normalized_rating_max_value: int | None,
|
|
684
|
+
id3v2_version: tuple[int, int, int] | None,
|
|
685
|
+
target_format: MetadataFormat | None = None,
|
|
686
|
+
fail_on_unsupported_field: bool = False,
|
|
687
|
+
) -> None:
|
|
688
|
+
"""Handle metadata strategy-specific behavior for all strategies."""
|
|
689
|
+
|
|
690
|
+
# Get the target format (specified format or native format)
|
|
691
|
+
if target_format:
|
|
692
|
+
target_format_actual = target_format
|
|
693
|
+
else:
|
|
694
|
+
available_formats = MetadataFormat.get_priorities().get(audio_file.file_extension)
|
|
695
|
+
if not available_formats:
|
|
696
|
+
msg = f"File extension {audio_file.file_extension} is not supported"
|
|
697
|
+
raise FileTypeNotSupportedError(msg)
|
|
698
|
+
target_format_actual = available_formats[0]
|
|
699
|
+
|
|
700
|
+
# When a specific format is forced, ignore strategy and write only to that format
|
|
701
|
+
if target_format:
|
|
702
|
+
all_managers = _get_metadata_managers(
|
|
703
|
+
audio_file=audio_file,
|
|
704
|
+
tag_formats=[target_format_actual],
|
|
705
|
+
normalized_rating_max_value=normalized_rating_max_value,
|
|
706
|
+
id3v2_version=id3v2_version,
|
|
707
|
+
)
|
|
708
|
+
target_manager = all_managers[target_format_actual]
|
|
709
|
+
target_manager.update_metadata(unified_metadata)
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
# Get all available managers for this file type
|
|
713
|
+
all_managers = _get_metadata_managers(
|
|
714
|
+
audio_file=audio_file, normalized_rating_max_value=normalized_rating_max_value, id3v2_version=id3v2_version
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Get other formats (non-target)
|
|
718
|
+
other_managers = {fmt: mgr for fmt, mgr in all_managers.items() if fmt != target_format_actual}
|
|
719
|
+
|
|
720
|
+
if strategy == MetadataWritingStrategy.CLEANUP:
|
|
721
|
+
# First, clean up non-target formats
|
|
722
|
+
for _fmt, manager in other_managers.items():
|
|
723
|
+
with contextlib.suppress(Exception):
|
|
724
|
+
manager.delete_metadata()
|
|
725
|
+
# Some managers might not support deletion or might fail
|
|
726
|
+
|
|
727
|
+
# Check for unsupported fields by target format
|
|
728
|
+
target_manager = all_managers[target_format_actual]
|
|
729
|
+
unsupported_fields = []
|
|
730
|
+
for field in unified_metadata:
|
|
731
|
+
if (
|
|
732
|
+
hasattr(target_manager, "metadata_keys_direct_map_write")
|
|
733
|
+
and target_manager.metadata_keys_direct_map_write
|
|
734
|
+
) and field not in target_manager.metadata_keys_direct_map_write:
|
|
735
|
+
unsupported_fields.append(field)
|
|
736
|
+
|
|
737
|
+
if unsupported_fields:
|
|
738
|
+
if fail_on_unsupported_field:
|
|
739
|
+
msg = f"Fields not supported by {target_format_actual.value} format: {unsupported_fields}"
|
|
740
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
741
|
+
warnings.warn(
|
|
742
|
+
f"Fields not supported by {target_format_actual.value} format will be skipped: {unsupported_fields}",
|
|
743
|
+
stacklevel=2,
|
|
744
|
+
)
|
|
745
|
+
# Create filtered metadata without unsupported fields
|
|
746
|
+
filtered_metadata = {k: v for k, v in unified_metadata.items() if k not in unsupported_fields}
|
|
747
|
+
unified_metadata = filtered_metadata
|
|
748
|
+
|
|
749
|
+
# Then write to target format
|
|
750
|
+
target_manager.update_metadata(unified_metadata)
|
|
751
|
+
|
|
752
|
+
elif strategy == MetadataWritingStrategy.SYNC:
|
|
753
|
+
# For SYNC, we need to write to all available formats
|
|
754
|
+
# Check if any fields are unsupported by the target format when fail_on_unsupported_field is True
|
|
755
|
+
if fail_on_unsupported_field:
|
|
756
|
+
target_manager = all_managers[target_format_actual]
|
|
757
|
+
unsupported_fields = []
|
|
758
|
+
for field in unified_metadata:
|
|
759
|
+
if (
|
|
760
|
+
hasattr(target_manager, "metadata_keys_direct_map_write")
|
|
761
|
+
and target_manager.metadata_keys_direct_map_write
|
|
762
|
+
) and field not in target_manager.metadata_keys_direct_map_write:
|
|
763
|
+
unsupported_fields.append(field)
|
|
764
|
+
if unsupported_fields:
|
|
765
|
+
unsupported_error_msg = (
|
|
766
|
+
f"Fields not supported by {target_format_actual.value} format: {unsupported_fields}"
|
|
767
|
+
)
|
|
768
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(unsupported_error_msg)
|
|
769
|
+
else:
|
|
770
|
+
# Filter out unsupported fields when fail_on_unsupported_field is False
|
|
771
|
+
target_manager = all_managers[target_format_actual]
|
|
772
|
+
unsupported_fields = []
|
|
773
|
+
for field in unified_metadata:
|
|
774
|
+
if (
|
|
775
|
+
hasattr(target_manager, "metadata_keys_direct_map_write")
|
|
776
|
+
and target_manager.metadata_keys_direct_map_write
|
|
777
|
+
) and field not in target_manager.metadata_keys_direct_map_write:
|
|
778
|
+
unsupported_fields.append(field)
|
|
779
|
+
if unsupported_fields:
|
|
780
|
+
unsupported_warn_msg = (
|
|
781
|
+
f"Fields not supported by {target_format_actual.value} format will be skipped: {unsupported_fields}"
|
|
782
|
+
)
|
|
783
|
+
warnings.warn(unsupported_warn_msg, stacklevel=2)
|
|
784
|
+
# Create filtered metadata without unsupported fields
|
|
785
|
+
filtered_metadata = {k: v for k, v in unified_metadata.items() if k not in unsupported_fields}
|
|
786
|
+
unified_metadata = filtered_metadata
|
|
787
|
+
|
|
788
|
+
# Write to target format first
|
|
789
|
+
target_manager = all_managers[target_format_actual]
|
|
790
|
+
try:
|
|
791
|
+
target_manager.update_metadata(unified_metadata)
|
|
792
|
+
except MetadataFieldNotSupportedByMetadataFormatError as e:
|
|
793
|
+
# For SYNC strategy, log warning but continue with other formats
|
|
794
|
+
format_warn_msg = f"Format {target_format_actual} doesn't support some metadata fields: {e}"
|
|
795
|
+
warnings.warn(format_warn_msg, stacklevel=2)
|
|
796
|
+
except Exception as e:
|
|
797
|
+
# Re-raise user errors (like InvalidRatingValueError) immediately
|
|
798
|
+
from .exceptions import ConfigurationError, InvalidRatingValueError
|
|
799
|
+
|
|
800
|
+
if isinstance(e, InvalidRatingValueError | ConfigurationError):
|
|
801
|
+
raise
|
|
802
|
+
# Some managers might not support writing or might fail for other reasons
|
|
803
|
+
|
|
804
|
+
# Then sync all other available formats
|
|
805
|
+
# Note: We need to be careful about the order to avoid conflicts
|
|
806
|
+
for fmt_name, manager in other_managers.items():
|
|
807
|
+
try:
|
|
808
|
+
manager.update_metadata(unified_metadata)
|
|
809
|
+
except MetadataFieldNotSupportedByMetadataFormatError as e:
|
|
810
|
+
# For SYNC strategy, log warning but continue with other formats
|
|
811
|
+
format_warn_msg = f"Format {fmt_name} doesn't support some metadata fields: {e}"
|
|
812
|
+
warnings.warn(format_warn_msg, stacklevel=2)
|
|
813
|
+
continue
|
|
814
|
+
except Exception:
|
|
815
|
+
# Some managers might not support writing or might fail for other reasons
|
|
816
|
+
pass
|
|
817
|
+
|
|
818
|
+
elif strategy == MetadataWritingStrategy.PRESERVE:
|
|
819
|
+
# For PRESERVE, we need to save existing metadata from other formats first
|
|
820
|
+
preserved_metadata: dict[MetadataFormat, UnifiedMetadata] = {}
|
|
821
|
+
for fmt, manager in other_managers.items():
|
|
822
|
+
try:
|
|
823
|
+
existing_metadata = manager.get_unified_metadata()
|
|
824
|
+
if existing_metadata:
|
|
825
|
+
preserved_metadata[fmt] = existing_metadata
|
|
826
|
+
except Exception:
|
|
827
|
+
pass
|
|
828
|
+
|
|
829
|
+
# Check for unsupported fields by target format
|
|
830
|
+
target_manager = all_managers[target_format_actual]
|
|
831
|
+
unsupported_fields = []
|
|
832
|
+
for field in unified_metadata:
|
|
833
|
+
if (
|
|
834
|
+
hasattr(target_manager, "metadata_keys_direct_map_write")
|
|
835
|
+
and target_manager.metadata_keys_direct_map_write
|
|
836
|
+
) and field not in target_manager.metadata_keys_direct_map_write:
|
|
837
|
+
unsupported_fields.append(field)
|
|
838
|
+
|
|
839
|
+
if unsupported_fields:
|
|
840
|
+
if fail_on_unsupported_field:
|
|
841
|
+
unsupported_error_msg = (
|
|
842
|
+
f"Fields not supported by {target_format_actual.value} format: {unsupported_fields}"
|
|
843
|
+
)
|
|
844
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(unsupported_error_msg)
|
|
845
|
+
unsupported_warn_msg = (
|
|
846
|
+
f"Fields not supported by {target_format_actual.value} format will be skipped: {unsupported_fields}"
|
|
847
|
+
)
|
|
848
|
+
warnings.warn(unsupported_warn_msg, stacklevel=2)
|
|
849
|
+
# Create filtered metadata without unsupported fields
|
|
850
|
+
filtered_metadata = {k: v for k, v in unified_metadata.items() if k not in unsupported_fields}
|
|
851
|
+
unified_metadata = filtered_metadata
|
|
852
|
+
|
|
853
|
+
# Write to target format
|
|
854
|
+
target_manager.update_metadata(unified_metadata)
|
|
855
|
+
|
|
856
|
+
# Restore preserved metadata from other formats
|
|
857
|
+
for fmt, metadata in preserved_metadata.items():
|
|
858
|
+
try:
|
|
859
|
+
manager = other_managers[fmt]
|
|
860
|
+
manager.update_metadata(metadata)
|
|
861
|
+
except Exception:
|
|
862
|
+
# Some managers might not support writing or might fail for other reasons
|
|
863
|
+
pass
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def delete_all_metadata(
|
|
867
|
+
file: PublicFileType,
|
|
868
|
+
metadata_format: MetadataFormat | None = None,
|
|
869
|
+
id3v2_version: tuple[int, int, int] | None = None,
|
|
870
|
+
) -> bool:
|
|
871
|
+
"""Delete all metadata from an audio file, including metadata headers.
|
|
872
|
+
|
|
873
|
+
This function completely removes all metadata tags and their container structures
|
|
874
|
+
from the specified audio file. This is a destructive operation that removes
|
|
875
|
+
metadata headers entirely, not just the content.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
file: Audio file path (str or Path)
|
|
879
|
+
metadata_format: Specific format to delete metadata from. If None, deletes from ALL supported formats.
|
|
880
|
+
id3v2_version: ID3v2 version tuple for ID3v2-specific operations
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
True if metadata was successfully deleted from at least one format, False otherwise
|
|
884
|
+
|
|
885
|
+
Raises:
|
|
886
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
887
|
+
FileNotFoundError: If the file does not exist
|
|
888
|
+
|
|
889
|
+
Examples:
|
|
890
|
+
# Delete ALL metadata from ALL supported formats (removes headers completely)
|
|
891
|
+
success = delete_all_metadata("song.mp3")
|
|
892
|
+
|
|
893
|
+
# Delete only ID3v2 metadata (keep ID3v1, removes ID3v2 headers)
|
|
894
|
+
success = delete_all_metadata("song.mp3", metadata_format=MetadataFormat.ID3V2)
|
|
895
|
+
|
|
896
|
+
# Delete Vorbis metadata from FLAC (removes Vorbis comment blocks)
|
|
897
|
+
success = delete_all_metadata("song.flac", metadata_format=MetadataFormat.VORBIS)
|
|
898
|
+
|
|
899
|
+
Note:
|
|
900
|
+
This function removes metadata headers entirely, significantly reducing file size.
|
|
901
|
+
This is different from setting individual fields to None, which only removes
|
|
902
|
+
specific fields while preserving the metadata structure and other fields.
|
|
903
|
+
|
|
904
|
+
When no metadata_format is specified, the function attempts to delete metadata from
|
|
905
|
+
ALL supported formats for the file type. Some formats may not support deletion
|
|
906
|
+
and will be skipped silently.
|
|
907
|
+
|
|
908
|
+
Use cases:
|
|
909
|
+
- Complete privacy cleanup (remove all metadata)
|
|
910
|
+
- File size optimization (remove all metadata headers)
|
|
911
|
+
- Format cleanup (remove specific format metadata)
|
|
912
|
+
|
|
913
|
+
For selective field removal, use update_metadata with None values instead.
|
|
914
|
+
"""
|
|
915
|
+
audio_file = _AudioFile(file)
|
|
916
|
+
|
|
917
|
+
# If specific format requested, delete only that format
|
|
918
|
+
if metadata_format:
|
|
919
|
+
manager = _get_metadata_manager(
|
|
920
|
+
audio_file=audio_file, metadata_format=metadata_format, id3v2_version=id3v2_version
|
|
921
|
+
)
|
|
922
|
+
result: bool = manager.delete_metadata()
|
|
923
|
+
return result
|
|
924
|
+
|
|
925
|
+
# Delete from all supported formats for this file type
|
|
926
|
+
all_managers = _get_metadata_managers(
|
|
927
|
+
audio_file=audio_file, normalized_rating_max_value=None, id3v2_version=id3v2_version
|
|
928
|
+
)
|
|
929
|
+
success_count = 0
|
|
930
|
+
|
|
931
|
+
for _format_type, manager in all_managers.items():
|
|
932
|
+
try:
|
|
933
|
+
if manager.delete_metadata():
|
|
934
|
+
success_count += 1
|
|
935
|
+
except Exception:
|
|
936
|
+
# Some formats may not support deletion (e.g., ID3v1) or may fail
|
|
937
|
+
# Continue with other formats
|
|
938
|
+
pass
|
|
939
|
+
|
|
940
|
+
# Return True if at least one format was successfully deleted
|
|
941
|
+
return success_count > 0
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def get_bitrate(file: PublicFileType) -> int:
|
|
945
|
+
"""Get the bitrate of an audio file.
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
file: Audio file path (str or Path)
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
Bitrate in bits per second (bps)
|
|
952
|
+
|
|
953
|
+
Raises:
|
|
954
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
955
|
+
FileNotFoundError: If the file does not exist
|
|
956
|
+
|
|
957
|
+
Examples:
|
|
958
|
+
bitrate = get_bitrate("song.mp3")
|
|
959
|
+
print(f"Bitrate: {bitrate} bps")
|
|
960
|
+
print(f"Bitrate: {bitrate // 1000} kbps")
|
|
961
|
+
"""
|
|
962
|
+
audio_file = _AudioFile(file)
|
|
963
|
+
return audio_file.get_bitrate()
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def get_channels(file: PublicFileType) -> int:
|
|
967
|
+
"""Get the number of channels in an audio file.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
file: Audio file path (str or Path)
|
|
971
|
+
|
|
972
|
+
Returns:
|
|
973
|
+
Number of audio channels (e.g., 1 for mono, 2 for stereo)
|
|
974
|
+
|
|
975
|
+
Raises:
|
|
976
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
977
|
+
FileNotFoundError: If the file does not exist
|
|
978
|
+
|
|
979
|
+
Examples:
|
|
980
|
+
channels = get_channels("song.mp3")
|
|
981
|
+
print(f"Channels: {channels}")
|
|
982
|
+
"""
|
|
983
|
+
audio_file = _AudioFile(file)
|
|
984
|
+
return audio_file.get_channels()
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def get_file_size(file: PublicFileType) -> int:
|
|
988
|
+
"""Get the file size of an audio file in bytes.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
file: Audio file path (str or Path)
|
|
992
|
+
|
|
993
|
+
Returns:
|
|
994
|
+
File size in bytes
|
|
995
|
+
|
|
996
|
+
Raises:
|
|
997
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
998
|
+
FileNotFoundError: If the file does not exist
|
|
999
|
+
|
|
1000
|
+
Examples:
|
|
1001
|
+
size = get_file_size("song.mp3")
|
|
1002
|
+
print(f"File size: {size} bytes")
|
|
1003
|
+
"""
|
|
1004
|
+
audio_file = _AudioFile(file)
|
|
1005
|
+
return audio_file.get_file_size()
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def get_sample_rate(file: PublicFileType) -> int:
|
|
1009
|
+
"""Get the sample rate of an audio file in Hz.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
file: Audio file path (str or Path)
|
|
1013
|
+
|
|
1014
|
+
Returns:
|
|
1015
|
+
Sample rate in Hz
|
|
1016
|
+
|
|
1017
|
+
Raises:
|
|
1018
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
1019
|
+
FileNotFoundError: If the file does not exist
|
|
1020
|
+
|
|
1021
|
+
Examples:
|
|
1022
|
+
sample_rate = get_sample_rate("song.mp3")
|
|
1023
|
+
print(f"Sample rate: {sample_rate} Hz")
|
|
1024
|
+
"""
|
|
1025
|
+
audio_file = _AudioFile(file)
|
|
1026
|
+
return audio_file.get_sample_rate()
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def is_audio_file(file: PublicFileType) -> bool:
|
|
1030
|
+
"""Check if a file is a valid audio file supported by the library.
|
|
1031
|
+
|
|
1032
|
+
This function validates that the file exists, has a supported extension (.mp3, .flac, .wav),
|
|
1033
|
+
and contains valid audio content for that format.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
file: File path (str or Path) to check
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
True if the file is a valid audio file, False otherwise
|
|
1040
|
+
|
|
1041
|
+
Examples:
|
|
1042
|
+
# Check if a file is a valid audio file
|
|
1043
|
+
if is_audio_file("song.mp3"):
|
|
1044
|
+
print("Valid audio file")
|
|
1045
|
+
else:
|
|
1046
|
+
print("Not a valid audio file")
|
|
1047
|
+
|
|
1048
|
+
# Check before processing
|
|
1049
|
+
if is_audio_file("unknown.txt"):
|
|
1050
|
+
metadata = get_unified_metadata("unknown.txt")
|
|
1051
|
+
else:
|
|
1052
|
+
print("File is not a supported audio format")
|
|
1053
|
+
"""
|
|
1054
|
+
try:
|
|
1055
|
+
_AudioFile(file)
|
|
1056
|
+
except (FileNotFoundError, FileTypeNotSupportedError, FileCorruptedError):
|
|
1057
|
+
return False
|
|
1058
|
+
else:
|
|
1059
|
+
return True
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def get_duration_in_sec(file: PublicFileType) -> float:
|
|
1063
|
+
"""Get the duration of an audio file in seconds.
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
file: Audio file path (str or Path)
|
|
1067
|
+
|
|
1068
|
+
Returns:
|
|
1069
|
+
Duration in seconds as a float
|
|
1070
|
+
|
|
1071
|
+
Raises:
|
|
1072
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
1073
|
+
FileNotFoundError: If the file does not exist
|
|
1074
|
+
|
|
1075
|
+
Examples:
|
|
1076
|
+
duration = get_duration_in_sec("song.mp3")
|
|
1077
|
+
print(f"Duration: {duration:.2f} seconds")
|
|
1078
|
+
|
|
1079
|
+
# Convert to minutes
|
|
1080
|
+
minutes = duration / 60
|
|
1081
|
+
print(f"Duration: {minutes:.2f} minutes")
|
|
1082
|
+
"""
|
|
1083
|
+
audio_file = _AudioFile(file)
|
|
1084
|
+
return audio_file.get_duration_in_sec()
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def is_flac_md5_valid(file: PublicFileType) -> bool:
|
|
1088
|
+
"""Check if a FLAC file's MD5 signature is valid.
|
|
1089
|
+
|
|
1090
|
+
This function verifies the integrity of a FLAC file by checking its MD5 signature.
|
|
1091
|
+
Only works with FLAC files.
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
file: Audio file path (str or Path; must be FLAC)
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
True if MD5 signature is valid, False otherwise
|
|
1098
|
+
|
|
1099
|
+
Raises:
|
|
1100
|
+
FileTypeNotSupportedError: If the file is not a FLAC file
|
|
1101
|
+
FileNotFoundError: If the file does not exist
|
|
1102
|
+
|
|
1103
|
+
Examples:
|
|
1104
|
+
# Check FLAC file integrity
|
|
1105
|
+
is_valid = is_flac_md5_valid("song.flac")
|
|
1106
|
+
if is_valid:
|
|
1107
|
+
print("FLAC file is intact")
|
|
1108
|
+
else:
|
|
1109
|
+
print("FLAC file may be corrupted")
|
|
1110
|
+
"""
|
|
1111
|
+
audio_file = _AudioFile(file)
|
|
1112
|
+
try:
|
|
1113
|
+
return audio_file.is_flac_file_md5_valid()
|
|
1114
|
+
except FileCorruptedError:
|
|
1115
|
+
return False
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def fix_md5_checking(file: PublicFileType) -> str:
|
|
1119
|
+
"""Return a temporary file with corrected MD5 signature.
|
|
1120
|
+
|
|
1121
|
+
Args:
|
|
1122
|
+
file: Audio file path (str or Path)
|
|
1123
|
+
|
|
1124
|
+
Returns:
|
|
1125
|
+
str: Path to a temporary file containing the corrected audio data.
|
|
1126
|
+
|
|
1127
|
+
Raises:
|
|
1128
|
+
FileTypeNotSupportedError: If the file is not a FLAC file
|
|
1129
|
+
FileCorruptedError: If the FLAC file is corrupted or cannot be corrected
|
|
1130
|
+
RuntimeError: If the FLAC command fails to execute
|
|
1131
|
+
"""
|
|
1132
|
+
audio_file = _AudioFile(file)
|
|
1133
|
+
return audio_file.get_file_with_corrected_md5(delete_original=True)
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def get_full_metadata(
|
|
1137
|
+
file: PublicFileType, include_headers: bool = True, include_technical: bool = True
|
|
1138
|
+
) -> dict[str, Any]:
|
|
1139
|
+
"""Get comprehensive metadata including all available information from a file.
|
|
1140
|
+
|
|
1141
|
+
Includes headers and technical details even when no metadata is present.
|
|
1142
|
+
|
|
1143
|
+
This function provides the most complete view of an audio file by combining:
|
|
1144
|
+
- All metadata from all supported formats (ID3v1, ID3v2, Vorbis, RIFF)
|
|
1145
|
+
- Technical information (duration, bitrate, sample rate, channels, file size)
|
|
1146
|
+
- Format-specific headers and structure information
|
|
1147
|
+
- Raw metadata details from each format
|
|
1148
|
+
|
|
1149
|
+
Args:
|
|
1150
|
+
file: Audio file path (str or Path)
|
|
1151
|
+
include_headers: Whether to include format-specific header information (default: True)
|
|
1152
|
+
include_technical: Whether to include technical audio information (default: True)
|
|
1153
|
+
|
|
1154
|
+
Returns:
|
|
1155
|
+
Comprehensive dictionary containing all available metadata and technical information
|
|
1156
|
+
|
|
1157
|
+
Raises:
|
|
1158
|
+
FileTypeNotSupportedError: If the file format is not supported
|
|
1159
|
+
FileNotFoundError: If the file does not exist
|
|
1160
|
+
FileCorruptedError: If the file content is corrupted or not a valid audio file
|
|
1161
|
+
|
|
1162
|
+
Examples:
|
|
1163
|
+
# Get complete metadata including headers and technical info
|
|
1164
|
+
full_metadata = get_full_metadata("song.mp3")
|
|
1165
|
+
|
|
1166
|
+
# Access unified metadata (same as get_unified_metadata)
|
|
1167
|
+
print(f"Title: {full_metadata['unified_metadata']['title']}")
|
|
1168
|
+
|
|
1169
|
+
# Access technical information
|
|
1170
|
+
print(f"Duration: {full_metadata['technical_info']['duration_seconds']} seconds")
|
|
1171
|
+
bitrate_bps = full_metadata['technical_info']['bitrate_bps']
|
|
1172
|
+
print(f"Bitrate: {bitrate_bps} bps ({bitrate_bps // 1000} kbps)")
|
|
1173
|
+
|
|
1174
|
+
# Access format-specific metadata
|
|
1175
|
+
print(f"ID3v2 Title: {full_metadata['metadata_format']['id3v2']['title']}")
|
|
1176
|
+
|
|
1177
|
+
# Access header information
|
|
1178
|
+
print(f"ID3v2 Version: {full_metadata['headers']['id3v2']['version']}")
|
|
1179
|
+
print(f"Has ID3v1 Header: {full_metadata['headers']['id3v1']['present']}")
|
|
1180
|
+
"""
|
|
1181
|
+
audio_file = _AudioFile(file)
|
|
1182
|
+
|
|
1183
|
+
# Get all available managers for this file type
|
|
1184
|
+
all_managers = _get_metadata_managers(audio_file=audio_file, normalized_rating_max_value=None, id3v2_version=None)
|
|
1185
|
+
|
|
1186
|
+
# Get file-specific format priorities
|
|
1187
|
+
available_formats = MetadataFormat.get_priorities().get(audio_file.file_extension, [])
|
|
1188
|
+
|
|
1189
|
+
# Initialize result structure
|
|
1190
|
+
result: dict[str, Any] = {
|
|
1191
|
+
"unified_metadata": {},
|
|
1192
|
+
"technical_info": {},
|
|
1193
|
+
"metadata_format": {},
|
|
1194
|
+
"headers": {},
|
|
1195
|
+
"raw_metadata": {},
|
|
1196
|
+
"format_priorities": {
|
|
1197
|
+
"file_extension": audio_file.file_extension,
|
|
1198
|
+
"reading_order": [fmt.value for fmt in available_formats],
|
|
1199
|
+
"writing_format": available_formats[0].value if available_formats else None,
|
|
1200
|
+
},
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
# Get unified metadata (same as get_unified_metadata)
|
|
1204
|
+
result["unified_metadata"] = get_unified_metadata(file)
|
|
1205
|
+
|
|
1206
|
+
# Get technical information
|
|
1207
|
+
if include_technical:
|
|
1208
|
+
try:
|
|
1209
|
+
result["technical_info"] = {
|
|
1210
|
+
"duration_seconds": audio_file.get_duration_in_sec(),
|
|
1211
|
+
"bitrate_bps": audio_file.get_bitrate(),
|
|
1212
|
+
"sample_rate_hz": audio_file.get_sample_rate(),
|
|
1213
|
+
"channels": audio_file.get_channels(),
|
|
1214
|
+
"file_size_bytes": get_file_size(file),
|
|
1215
|
+
"file_extension": audio_file.file_extension,
|
|
1216
|
+
"audio_format_name": audio_file.get_audio_format_name(),
|
|
1217
|
+
"is_flac_md5_valid": (
|
|
1218
|
+
audio_file.is_flac_file_md5_valid() if audio_file.file_extension == ".flac" else None
|
|
1219
|
+
),
|
|
1220
|
+
}
|
|
1221
|
+
except Exception:
|
|
1222
|
+
result["technical_info"] = {
|
|
1223
|
+
"duration_seconds": 0,
|
|
1224
|
+
"bitrate_bps": 0,
|
|
1225
|
+
"sample_rate_hz": 0,
|
|
1226
|
+
"channels": 0,
|
|
1227
|
+
"file_size_bytes": 0,
|
|
1228
|
+
"file_extension": audio_file.file_extension,
|
|
1229
|
+
"audio_format_name": audio_file.get_audio_format_name(),
|
|
1230
|
+
"is_flac_md5_valid": None,
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
# Get format-specific metadata and headers
|
|
1234
|
+
metadata_format_dict: dict[str, Any] = result["metadata_format"]
|
|
1235
|
+
headers_dict: dict[str, Any] = result["headers"]
|
|
1236
|
+
raw_metadata_dict: dict[str, Any] = result["raw_metadata"]
|
|
1237
|
+
|
|
1238
|
+
for format_type in available_formats:
|
|
1239
|
+
format_key = format_type.value
|
|
1240
|
+
manager = all_managers.get(format_type)
|
|
1241
|
+
|
|
1242
|
+
if manager:
|
|
1243
|
+
# Get format-specific metadata
|
|
1244
|
+
try:
|
|
1245
|
+
metadata_format = manager.get_unified_metadata()
|
|
1246
|
+
metadata_format_dict[format_key] = metadata_format
|
|
1247
|
+
except Exception:
|
|
1248
|
+
metadata_format_dict[format_key] = {}
|
|
1249
|
+
|
|
1250
|
+
# Get header information
|
|
1251
|
+
if include_headers:
|
|
1252
|
+
try:
|
|
1253
|
+
header_info = manager.get_header_info()
|
|
1254
|
+
headers_dict[format_key] = header_info
|
|
1255
|
+
except Exception:
|
|
1256
|
+
headers_dict[format_key] = {
|
|
1257
|
+
"present": False,
|
|
1258
|
+
"version": None,
|
|
1259
|
+
"size_bytes": 0,
|
|
1260
|
+
"position": None,
|
|
1261
|
+
"flags": {},
|
|
1262
|
+
"extended_header": {},
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
# Get raw metadata information
|
|
1266
|
+
try:
|
|
1267
|
+
raw_info = manager.get_raw_metadata_info()
|
|
1268
|
+
raw_metadata_dict[format_key] = raw_info
|
|
1269
|
+
except Exception:
|
|
1270
|
+
raw_metadata_dict[format_key] = {
|
|
1271
|
+
"raw_data": None,
|
|
1272
|
+
"parsed_fields": {},
|
|
1273
|
+
"frames": {},
|
|
1274
|
+
"comments": {},
|
|
1275
|
+
"chunk_structure": {},
|
|
1276
|
+
}
|
|
1277
|
+
else:
|
|
1278
|
+
# Format not available for this file type
|
|
1279
|
+
metadata_format_dict[format_key] = {}
|
|
1280
|
+
if include_headers:
|
|
1281
|
+
headers_dict[format_key] = {
|
|
1282
|
+
"present": False,
|
|
1283
|
+
"version": None,
|
|
1284
|
+
"size_bytes": 0,
|
|
1285
|
+
"position": None,
|
|
1286
|
+
"flags": {},
|
|
1287
|
+
"extended_header": {},
|
|
1288
|
+
}
|
|
1289
|
+
raw_metadata_dict[format_key] = {
|
|
1290
|
+
"raw_data": None,
|
|
1291
|
+
"parsed_fields": {},
|
|
1292
|
+
"frames": {},
|
|
1293
|
+
"comments": {},
|
|
1294
|
+
"chunk_structure": {},
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return result
|