audiometa-python 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- audiometa/__init__.py +1297 -0
- audiometa/__main__.py +6 -0
- audiometa/_audio_file.py +607 -0
- audiometa/cli.py +476 -0
- audiometa/exceptions.py +167 -0
- audiometa/manager/_MetadataManager.py +768 -0
- audiometa/manager/__init__.py +1 -0
- audiometa/manager/_rating_supporting/_RatingSupportingMetadataManager.py +250 -0
- audiometa/manager/_rating_supporting/__init__.py +1 -0
- audiometa/manager/_rating_supporting/id3v2/_Id3v2Manager.py +1032 -0
- audiometa/manager/_rating_supporting/id3v2/__init__.py +25 -0
- audiometa/manager/_rating_supporting/id3v2/_id3v2_constants.py +11 -0
- audiometa/manager/_rating_supporting/riff/_RiffManager.py +1002 -0
- audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
- audiometa/manager/_rating_supporting/riff/_riff_constants.py +17 -0
- audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +542 -0
- audiometa/manager/_rating_supporting/vorbis/__init__.py +17 -0
- audiometa/manager/_rating_supporting/vorbis/_vorbis_constants.py +6 -0
- audiometa/manager/id3v1/_Id3v1Manager.py +512 -0
- audiometa/manager/id3v1/__init__.py +1 -0
- audiometa/manager/id3v1/_constants.py +8 -0
- audiometa/manager/id3v1/id3v1_raw_metadata.py +242 -0
- audiometa/manager/id3v1/id3v1_raw_metadata_key.py +13 -0
- audiometa/test/__init__.py +1 -0
- audiometa/test/assets/create_test_files.py +72 -0
- audiometa/test/helpers/__init__.py +51 -0
- audiometa/test/helpers/common/__init__.py +6 -0
- audiometa/test/helpers/common/audio_file_creator.py +68 -0
- audiometa/test/helpers/common/external_tool_runner.py +74 -0
- audiometa/test/helpers/id3v1/__init__.py +8 -0
- audiometa/test/helpers/id3v1/id3v1_header_verifier.py +18 -0
- audiometa/test/helpers/id3v1/id3v1_metadata_deleter.py +37 -0
- audiometa/test/helpers/id3v1/id3v1_metadata_getter.py +61 -0
- audiometa/test/helpers/id3v1/id3v1_metadata_setter.py +82 -0
- audiometa/test/helpers/id3v2/__init__.py +28 -0
- audiometa/test/helpers/id3v2/id3v2_frame_manual_creator.py +349 -0
- audiometa/test/helpers/id3v2/id3v2_header_verifier.py +38 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_deleter.py +56 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_getter.py +189 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +506 -0
- audiometa/test/helpers/riff/__init__.py +8 -0
- audiometa/test/helpers/riff/riff_header_verifier.py +85 -0
- audiometa/test/helpers/riff/riff_manual_metadata_creator.py +298 -0
- audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
- audiometa/test/helpers/riff/riff_metadata_getter.py +219 -0
- audiometa/test/helpers/riff/riff_metadata_setter.py +374 -0
- audiometa/test/helpers/scripts/__init__.py +0 -0
- audiometa/test/helpers/technical_info_inspector.py +115 -0
- audiometa/test/helpers/temp_file_with_metadata.py +82 -0
- audiometa/test/helpers/vorbis/__init__.py +8 -0
- audiometa/test/helpers/vorbis/vorbis_header_verifier.py +31 -0
- audiometa/test/helpers/vorbis/vorbis_metadata_deleter.py +49 -0
- audiometa/test/helpers/vorbis/vorbis_metadata_getter.py +67 -0
- audiometa/test/helpers/vorbis/vorbis_metadata_setter.py +221 -0
- audiometa/test/tests/__init__.py +0 -0
- audiometa/test/tests/conftest.py +276 -0
- audiometa/test/tests/e2e/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/error_handling/__init__.py +1 -0
- audiometa/test/tests/e2e/cli/error_handling/test_command_structure_errors.py +77 -0
- audiometa/test/tests/e2e/cli/error_handling/test_file_access_errors.py +130 -0
- audiometa/test/tests/e2e/cli/error_handling/test_format_output_errors.py +118 -0
- audiometa/test/tests/e2e/cli/error_handling/test_input_validation_errors.py +172 -0
- audiometa/test/tests/e2e/cli/error_handling/test_missing_fields_validation.py +49 -0
- audiometa/test/tests/e2e/cli/error_handling/test_multiple_files_errors.py +160 -0
- audiometa/test/tests/e2e/cli/error_handling/test_rating_validation.py +90 -0
- audiometa/test/tests/e2e/cli/error_handling/test_year_validation.py +51 -0
- audiometa/test/tests/e2e/cli/read/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/read/test_basic.py +58 -0
- audiometa/test/tests/e2e/cli/read/test_comprehensive.py +240 -0
- audiometa/test/tests/e2e/cli/read/test_formats.py +55 -0
- audiometa/test/tests/e2e/cli/read/test_metadata_content.py +164 -0
- audiometa/test/tests/e2e/cli/read/test_multiple_files.py +149 -0
- audiometa/test/tests/e2e/cli/read/test_options.py +88 -0
- audiometa/test/tests/e2e/cli/read/test_unified.py +84 -0
- audiometa/test/tests/e2e/cli/test_delete.py +20 -0
- audiometa/test/tests/e2e/cli/test_formatting.py +31 -0
- audiometa/test/tests/e2e/cli/test_help.py +41 -0
- audiometa/test/tests/e2e/cli/write/__init__.py +0 -0
- audiometa/test/tests/e2e/cli/write/test_basic.py +51 -0
- audiometa/test/tests/e2e/cli/write/test_comprehensive.py +210 -0
- audiometa/test/tests/e2e/cli/write/test_force_format.py +336 -0
- audiometa/test/tests/e2e/cli/write/test_integer_fields.py +145 -0
- audiometa/test/tests/e2e/cli/write/test_list_fields.py +107 -0
- audiometa/test/tests/e2e/cli/write/test_rating.py +74 -0
- audiometa/test/tests/e2e/cli/write/test_string_fields.py +54 -0
- audiometa/test/tests/e2e/cli/write/test_validation.py +85 -0
- audiometa/test/tests/e2e/scenarios/__init__.py +0 -0
- audiometa/test/tests/e2e/scenarios/test_user_scenarios.py +166 -0
- audiometa/test/tests/e2e/workflows/__init__.py +0 -0
- audiometa/test/tests/e2e/workflows/test_core_workflows.py +166 -0
- audiometa/test/tests/e2e/workflows/test_deletion_workflows.py +318 -0
- audiometa/test/tests/e2e/workflows/test_error_handling_workflows.py +165 -0
- audiometa/test/tests/e2e/workflows/test_format_specific_workflows.py +129 -0
- audiometa/test/tests/e2e/workflows/test_rating_workflows.py +124 -0
- audiometa/test/tests/integration/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/flac/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_delete_all.py +108 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_reading_all.py +61 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_reading_field.py +65 -0
- audiometa/test/tests/integration/audio_format/flac/test_flac_writing.py +69 -0
- audiometa/test/tests/integration/audio_format/mp3/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_delete_all.py +79 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_all.py +61 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_field.py +67 -0
- audiometa/test/tests/integration/audio_format/mp3/test_mp3_writing.py +60 -0
- audiometa/test/tests/integration/audio_format/wav/__init__.py +0 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_delete_all.py +87 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_reading_all.py +62 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_reading_field.py +57 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_with_id3v2_tags.py +83 -0
- audiometa/test/tests/integration/audio_format/wav/test_wav_writing.py +62 -0
- audiometa/test/tests/integration/conftest.py +29 -0
- audiometa/test/tests/integration/delete_all_metadata/__init__.py +1 -0
- audiometa/test/tests/integration/delete_all_metadata/test_audio_format_all.py +102 -0
- audiometa/test/tests/integration/delete_all_metadata/test_audio_format_header_deletion.py +77 -0
- audiometa/test/tests/integration/delete_all_metadata/test_basic_functionality.py +47 -0
- audiometa/test/tests/integration/delete_all_metadata/test_error_handling.py +24 -0
- audiometa/test/tests/integration/encoding/__init__.py +1 -0
- audiometa/test/tests/integration/encoding/test_encoding.py +88 -0
- audiometa/test/tests/integration/encoding/test_special_characters_edge_cases.py +223 -0
- audiometa/test/tests/integration/get_full_metadata/__init__.py +0 -0
- audiometa/test/tests/integration/get_full_metadata/test_audio_formats.py +122 -0
- audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +250 -0
- audiometa/test/tests/integration/get_full_metadata/test_consistency.py +67 -0
- audiometa/test/tests/integration/get_full_metadata/test_edge_cases.py +123 -0
- audiometa/test/tests/integration/get_full_metadata/test_error_handling.py +40 -0
- audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +43 -0
- audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
- audiometa/test/tests/integration/get_full_metadata/test_performance.py +95 -0
- audiometa/test/tests/integration/get_full_metadata/test_riff_bext.py +128 -0
- audiometa/test/tests/integration/get_full_metadata/test_structure.py +161 -0
- audiometa/test/tests/integration/metadata_field/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/album/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/album/test_deleting.py +73 -0
- audiometa/test/tests/integration/metadata_field/album/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/album/test_writing.py +50 -0
- audiometa/test/tests/integration/metadata_field/album_artists/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/album_artists/test_deleting.py +83 -0
- audiometa/test/tests/integration/metadata_field/album_artists/test_reading.py +38 -0
- audiometa/test/tests/integration/metadata_field/album_artists/test_writing.py +52 -0
- audiometa/test/tests/integration/metadata_field/artists/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/artists/test_deleting.py +68 -0
- audiometa/test/tests/integration/metadata_field/artists/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/artists/test_writing.py +46 -0
- audiometa/test/tests/integration/metadata_field/bpm/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/bpm/test_deleting.py +75 -0
- audiometa/test/tests/integration/metadata_field/bpm/test_reading.py +32 -0
- audiometa/test/tests/integration/metadata_field/bpm/test_writing.py +56 -0
- audiometa/test/tests/integration/metadata_field/comment/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/comment/test_deleting.py +68 -0
- audiometa/test/tests/integration/metadata_field/comment/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/comment/test_writing.py +49 -0
- audiometa/test/tests/integration/metadata_field/composer/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/composer/test_deleting.py +75 -0
- audiometa/test/tests/integration/metadata_field/composer/test_reading.py +34 -0
- audiometa/test/tests/integration/metadata_field/composer/test_writing.py +41 -0
- audiometa/test/tests/integration/metadata_field/copyright/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/copyright/test_deleting.py +81 -0
- audiometa/test/tests/integration/metadata_field/copyright/test_reading.py +35 -0
- audiometa/test/tests/integration/metadata_field/copyright/test_writing.py +41 -0
- audiometa/test/tests/integration/metadata_field/disc_number/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/disc_number/test_deleting.py +97 -0
- audiometa/test/tests/integration/metadata_field/disc_number/test_reading.py +92 -0
- audiometa/test/tests/integration/metadata_field/disc_number/test_writing.py +153 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/test_deleting.py +56 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/test_reading.py +54 -0
- audiometa/test/tests/integration/metadata_field/field_not_supported/test_writing.py +61 -0
- audiometa/test/tests/integration/metadata_field/genre/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v1_reading.py +65 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v2_reading.py +25 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_riff_reading.py +58 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_vorbis_reading.py +61 -0
- audiometa/test/tests/integration/metadata_field/genre/reading/test_smart_reading.py +191 -0
- audiometa/test/tests/integration/metadata_field/genre/test_deleting.py +62 -0
- audiometa/test/tests/integration/metadata_field/genre/test_writing.py +64 -0
- audiometa/test/tests/integration/metadata_field/isrc/__init__.py +1 -0
- audiometa/test/tests/integration/metadata_field/isrc/test_deleting.py +31 -0
- audiometa/test/tests/integration/metadata_field/isrc/test_reading.py +35 -0
- audiometa/test/tests/integration/metadata_field/isrc/test_writing.py +165 -0
- audiometa/test/tests/integration/metadata_field/language/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/language/test_deleting.py +75 -0
- audiometa/test/tests/integration/metadata_field/language/test_reading.py +39 -0
- audiometa/test/tests/integration/metadata_field/language/test_writing.py +43 -0
- audiometa/test/tests/integration/metadata_field/lyrics/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/lyrics/test_deleting.py +129 -0
- audiometa/test/tests/integration/metadata_field/lyrics/test_reading.py +57 -0
- audiometa/test/tests/integration/metadata_field/lyrics/test_writing.py +59 -0
- audiometa/test/tests/integration/metadata_field/publisher/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/publisher/test_deleting.py +88 -0
- audiometa/test/tests/integration/metadata_field/publisher/test_reading.py +32 -0
- audiometa/test/tests/integration/metadata_field/publisher/test_writing.py +47 -0
- audiometa/test/tests/integration/metadata_field/rating/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/test_base_100_proportional.py +81 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_non_proportional.py +33 -0
- audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_proportional.py +58 -0
- audiometa/test/tests/integration/metadata_field/rating/test_deleting.py +117 -0
- audiometa/test/tests/integration/metadata_field/rating/test_error_handling.py +137 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_id3v2.py +77 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_riff.py +55 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_vorbis.py +57 -0
- audiometa/test/tests/integration/metadata_field/rating/writing/test_comprehensive.py +192 -0
- audiometa/test/tests/integration/metadata_field/release_date/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_deleting.py +74 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_error_handling.py +82 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_reading.py +59 -0
- audiometa/test/tests/integration/metadata_field/release_date/test_writing.py +49 -0
- audiometa/test/tests/integration/metadata_field/test_metadata_field_validation.py +135 -0
- audiometa/test/tests/integration/metadata_field/title/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/title/test_deleting.py +73 -0
- audiometa/test/tests/integration/metadata_field/title/test_error_handling.py +47 -0
- audiometa/test/tests/integration/metadata_field/title/test_reading.py +36 -0
- audiometa/test/tests/integration/metadata_field/title/test_writing.py +64 -0
- audiometa/test/tests/integration/metadata_field/track_number/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/track_number/reading/__init__.py +0 -0
- audiometa/test/tests/integration/metadata_field/track_number/reading/test_edge_cases.py +43 -0
- audiometa/test/tests/integration/metadata_field/track_number/reading/test_metadata_format.py +32 -0
- audiometa/test/tests/integration/metadata_field/track_number/test_deleting.py +59 -0
- audiometa/test/tests/integration/metadata_field/track_number/test_writing.py +73 -0
- audiometa/test/tests/integration/multiple_values/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/reading/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v1.py +23 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_3.py +92 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_4.py +216 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_riff.py +84 -0
- audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_vorbis.py +169 -0
- audiometa/test/tests/integration/multiple_values/reading/test_performance_large_data.py +209 -0
- audiometa/test/tests/integration/multiple_values/reading/test_smart_parsing_scenarios.py +198 -0
- audiometa/test/tests/integration/multiple_values/reading/test_unicode_handling.py +24 -0
- audiometa/test/tests/integration/multiple_values/writing/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/__init__.py +1 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v1.py +62 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_3.py +36 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_4.py +34 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_riff.py +32 -0
- audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_vorbis.py +54 -0
- audiometa/test/tests/integration/multiple_values/writing/test_error_handling.py +42 -0
- audiometa/test/tests/integration/multiple_values/writing/test_large_values.py +98 -0
- audiometa/test/tests/integration/reading/__init__.py +1 -0
- audiometa/test/tests/integration/reading/test_read_multiple_metadata.py +80 -0
- audiometa/test/tests/integration/reading/test_reading_error_handling.py +36 -0
- audiometa/test/tests/integration/real_audio_files/__init__.py +0 -0
- audiometa/test/tests/integration/real_audio_files/test_reading.py +146 -0
- audiometa/test/tests/integration/real_audio_files/test_writing.py +198 -0
- audiometa/test/tests/integration/technical_info/__init__.py +0 -0
- audiometa/test/tests/integration/technical_info/flac_md5/__init__.py +0 -0
- audiometa/test/tests/integration/technical_info/flac_md5/conftest.py +103 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/__init__.py +0 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_audio_data_corruption.py +21 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_flipped_md5.py +29 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_non_flac_error.py +13 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_partial_md5.py +29 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_random_md5.py +29 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_unset_md5.py +56 -0
- audiometa/test/tests/integration/technical_info/flac_md5/test_valid_md5.py +21 -0
- audiometa/test/tests/integration/technical_info/test_bitrate.py +79 -0
- audiometa/test/tests/integration/technical_info/test_channels.py +38 -0
- audiometa/test/tests/integration/technical_info/test_duration_in_sec.py +38 -0
- audiometa/test/tests/integration/technical_info/test_sample_rate.py +40 -0
- audiometa/test/tests/integration/test_audio_file.py +35 -0
- audiometa/test/tests/integration/test_audio_format_readable_after_update_all_metadata_formats.py +95 -0
- audiometa/test/tests/integration/writing/__init__.py +0 -0
- audiometa/test/tests/integration/writing/test_error_handling.py +44 -0
- audiometa/test/tests/integration/writing/test_forced_format.py +224 -0
- audiometa/test/tests/integration/writing/test_multiple_format_preservation.py +223 -0
- audiometa/test/tests/integration/writing/test_partial_update.py +36 -0
- audiometa/test/tests/integration/writing/writing_strategies/__init__.py +0 -0
- audiometa/test/tests/integration/writing/writing_strategies/test_cleanup_strategy.py +79 -0
- audiometa/test/tests/integration/writing/writing_strategies/test_preserve_strategy.py +76 -0
- audiometa/test/tests/integration/writing/writing_strategies/test_sync_strategy.py +215 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/__init__.py +0 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_fail_behavior.py +42 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_no_writing_on_failure.py +93 -0
- audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_strategy_specific.py +99 -0
- audiometa/test/tests/unit/__init__.py +0 -0
- audiometa/test/tests/unit/audio_file/__init__.py +0 -0
- audiometa/test/tests/unit/audio_file/technical_info/__init__.py +0 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_bitrate.py +26 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_channels.py +31 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_duration_in_sec.py +38 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_error_handling.py +190 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_file_size.py +51 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_format_name.py +28 -0
- audiometa/test/tests/unit/audio_file/technical_info/test_sample_rate.py +31 -0
- audiometa/test/tests/unit/audio_file/test_context_manager.py +30 -0
- audiometa/test/tests/unit/audio_file/test_file_validation.py +40 -0
- audiometa/test/tests/unit/audio_file/test_is_audio_file.py +49 -0
- audiometa/test/tests/unit/audio_file/test_operations.py +20 -0
- audiometa/test/tests/unit/audio_file/test_path_handling.py +23 -0
- audiometa/test/tests/unit/cli/__init__.py +0 -0
- audiometa/test/tests/unit/cli/test_expand_file_patterns.py +234 -0
- audiometa/test/tests/unit/metadata_managers/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/conftest.py +142 -0
- audiometa/test/tests/unit/metadata_managers/header_info/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_id3v1.py +49 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_id3v2.py +66 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_riff.py +343 -0
- audiometa/test/tests/unit/metadata_managers/header_info/test_vorbis.py +53 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/test_smart_parsing.py +186 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_separator_selection.py +142 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_value_filtering.py +76 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_normalization.py +152 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_profiles_values.py +23 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/test_rating_validation.py +77 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/__init__.py +0 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_configuration_error.py +43 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_validation.py +151 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_writing_profiles.py +61 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_date_format_validation.py +135 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_disc_number_validation.py +75 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_isrc_format_validation.py +121 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_isrc_type_validation.py +30 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_track_number_validation.py +46 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_type_validation_exception.py +22 -0
- audiometa/test/tests/unit/metadata_managers/metadata_field/test_validation.py +83 -0
- audiometa/test/tests/unit/metadata_managers/test_metadata_format_managers_write_and_read.py +74 -0
- audiometa/test/tests/unit/metadata_managers/test_riff_configuration_error.py +26 -0
- audiometa/utils/__init__.py +1 -0
- audiometa/utils/id3v1_genre_code_map.py +205 -0
- audiometa/utils/metadata_format.py +31 -0
- audiometa/utils/metadata_writing_strategy.py +16 -0
- audiometa/utils/mutagen_exception_handler.py +24 -0
- audiometa/utils/os_dependencies_checker/__init__.py +24 -0
- audiometa/utils/os_dependencies_checker/base.py +62 -0
- audiometa/utils/os_dependencies_checker/config.py +77 -0
- audiometa/utils/os_dependencies_checker/macos.py +236 -0
- audiometa/utils/os_dependencies_checker/ubuntu.py +95 -0
- audiometa/utils/os_dependencies_checker/windows.py +227 -0
- audiometa/utils/rating_profiles.py +110 -0
- audiometa/utils/tool_path_resolver.py +135 -0
- audiometa/utils/types.py +82 -0
- audiometa/utils/unified_metadata_key.py +87 -0
- audiometa_python-0.6.0.dist-info/METADATA +1593 -0
- audiometa_python-0.6.0.dist-info/RECORD +352 -0
- audiometa_python-0.6.0.dist-info/WHEEL +5 -0
- audiometa_python-0.6.0.dist-info/entry_points.txt +2 -0
- audiometa_python-0.6.0.dist-info/licenses/LICENSE +202 -0
- audiometa_python-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import TYPE_CHECKING, TypeVar, Union, cast
|
|
4
|
+
|
|
5
|
+
from mutagen._file import FileType as MutagenMetadata
|
|
6
|
+
|
|
7
|
+
from audiometa.exceptions import InvalidMetadataFieldFormatError
|
|
8
|
+
from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .._audio_file import _AudioFile
|
|
12
|
+
from ..exceptions import MetadataFieldNotSupportedByMetadataFormatError
|
|
13
|
+
from ..utils.id3v1_genre_code_map import ID3V1_GENRE_CODE_MAP
|
|
14
|
+
from ..utils.types import RawMetadataDict, RawMetadataKey, UnifiedMetadata, UnifiedMetadataValue
|
|
15
|
+
|
|
16
|
+
# Separators in order of priority for multi-value metadata fields
|
|
17
|
+
# Note: null bytes (\x00) are only used in ID3v2.4, not included in generic priority list
|
|
18
|
+
METADATA_MULTI_VALUE_SEPARATORS_PRIORITIZED = ("//", "\\\\", "\\", ";", "/", ",")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T", str, int)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _MetadataManager:
|
|
25
|
+
audio_file: "_AudioFile"
|
|
26
|
+
metadata_keys_direct_map_read: dict[UnifiedMetadataKey, RawMetadataKey | None]
|
|
27
|
+
metadata_keys_direct_map_write: dict[UnifiedMetadataKey, RawMetadataKey | None] | None
|
|
28
|
+
raw_mutagen_metadata: MutagenMetadata | None = None
|
|
29
|
+
raw_clean_metadata: RawMetadataDict | None = None
|
|
30
|
+
raw_clean_metadata_uppercase_keys: RawMetadataDict | None = None
|
|
31
|
+
update_using_mutagen_metadata: bool
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
audio_file: "_AudioFile",
|
|
36
|
+
metadata_keys_direct_map_read: dict[UnifiedMetadataKey, RawMetadataKey | None],
|
|
37
|
+
metadata_keys_direct_map_write: dict[UnifiedMetadataKey, RawMetadataKey | None] | None = None,
|
|
38
|
+
update_using_mutagen_metadata: bool = True,
|
|
39
|
+
):
|
|
40
|
+
self.audio_file = audio_file
|
|
41
|
+
self.metadata_keys_direct_map_read = metadata_keys_direct_map_read
|
|
42
|
+
self.metadata_keys_direct_map_write = metadata_keys_direct_map_write
|
|
43
|
+
self.update_using_mutagen_metadata = update_using_mutagen_metadata
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def _get_formatted_metadata_format_name(self) -> str:
|
|
47
|
+
"""Get the formatted metadata format name.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The formatted format name (e.g., 'RIFF', 'ID3v2', 'Vorbis')
|
|
51
|
+
"""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def find_safe_separator(values: list[str]) -> str:
|
|
56
|
+
"""Find a separator that doesn't appear in any of the provided values.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
values: List of string values to check for separator conflicts
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A separator string that doesn't appear in any value, or the last
|
|
63
|
+
separator (comma) as fallback if no separator is safe
|
|
64
|
+
"""
|
|
65
|
+
# Find a separator that doesn't appear in any of the values
|
|
66
|
+
for sep in METADATA_MULTI_VALUE_SEPARATORS_PRIORITIZED:
|
|
67
|
+
if not any(sep in value for value in values):
|
|
68
|
+
return sep
|
|
69
|
+
|
|
70
|
+
# If no separator is safe, use the last one (comma)
|
|
71
|
+
return METADATA_MULTI_VALUE_SEPARATORS_PRIORITIZED[-1]
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _filter_valid_values(values: list[str | None]) -> list[str]:
|
|
75
|
+
"""Filter out None and empty values from a list of strings.
|
|
76
|
+
|
|
77
|
+
This is a generic function used by all metadata managers to ensure
|
|
78
|
+
consistent filtering of empty strings and whitespace.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
values: List of string or None values to filter
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of valid (non-empty) values
|
|
85
|
+
"""
|
|
86
|
+
return [value for value in values if value is not None and value != ""]
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def validate_release_date(release_date: str) -> None:
|
|
90
|
+
"""Validate release date format.
|
|
91
|
+
|
|
92
|
+
Release dates must be in one of the following formats:
|
|
93
|
+
- YYYY (4 digits) - for year-only dates (e.g., "2024")
|
|
94
|
+
- YYYY-MM-DD (ISO-like format) - for full dates (e.g., "2024-01-01")
|
|
95
|
+
- Empty string is allowed (represents no date)
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
release_date: The release date string to validate
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
InvalidMetadataFieldFormatError: If the release date format is invalid
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> _MetadataManager.validate_release_date("2024") # Valid
|
|
105
|
+
>>> _MetadataManager.validate_release_date("2024-01-01") # Valid
|
|
106
|
+
>>> _MetadataManager.validate_release_date("") # Valid (empty string)
|
|
107
|
+
>>> _MetadataManager.validate_release_date("2024/01/01") # Raises InvalidMetadataFieldFormatError
|
|
108
|
+
"""
|
|
109
|
+
if release_date and not (re.match(r"^\d{4}$", release_date) or re.match(r"^\d{4}-\d{2}-\d{2}$", release_date)):
|
|
110
|
+
raise InvalidMetadataFieldFormatError(
|
|
111
|
+
UnifiedMetadataKey.RELEASE_DATE.value, "YYYY (4 digits) or YYYY-MM-DD format", release_date
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def validate_track_number(track_number: str | int) -> None:
|
|
116
|
+
"""Validate track number format.
|
|
117
|
+
|
|
118
|
+
Track numbers must be in one of the following formats:
|
|
119
|
+
- Simple number: "5", "12", "99" (string or int)
|
|
120
|
+
- Number with separator and optional total: "5/12", "5-12", "5/", "5-"
|
|
121
|
+
- Empty string is allowed (represents no track number)
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
track_number: The track number to validate (string or int)
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
InvalidMetadataFieldFormatError: If the track number format is invalid
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
>>> _MetadataManager.validate_track_number("5") # Valid
|
|
131
|
+
>>> _MetadataManager.validate_track_number(5) # Valid
|
|
132
|
+
>>> _MetadataManager.validate_track_number("5/12") # Valid
|
|
133
|
+
>>> _MetadataManager.validate_track_number("5-12") # Valid
|
|
134
|
+
>>> _MetadataManager.validate_track_number("") # Valid (empty string)
|
|
135
|
+
>>> _MetadataManager.validate_track_number("/12") # Raises InvalidMetadataFieldFormatError
|
|
136
|
+
>>> _MetadataManager.validate_track_number("5/12/15") # Raises InvalidMetadataFieldFormatError
|
|
137
|
+
>>> _MetadataManager.validate_track_number("abc") # Raises InvalidMetadataFieldFormatError
|
|
138
|
+
"""
|
|
139
|
+
if isinstance(track_number, int):
|
|
140
|
+
if track_number < 0:
|
|
141
|
+
raise InvalidMetadataFieldFormatError(
|
|
142
|
+
UnifiedMetadataKey.TRACK_NUMBER.value, "non-negative integer or string format", str(track_number)
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if not isinstance(track_number, str):
|
|
147
|
+
raise InvalidMetadataFieldFormatError(
|
|
148
|
+
UnifiedMetadataKey.TRACK_NUMBER.value, "string or int", str(type(track_number).__name__)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if not track_number:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if not re.match(r"^\d+([-/]\d*)?$", track_number):
|
|
155
|
+
raise InvalidMetadataFieldFormatError(
|
|
156
|
+
UnifiedMetadataKey.TRACK_NUMBER.value,
|
|
157
|
+
"simple number (e.g., '5') or number with separator and optional total (e.g., '5/12', '5-12')",
|
|
158
|
+
track_number,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def validate_disc_number(disc_number: int) -> None:
|
|
163
|
+
"""Validate disc number format.
|
|
164
|
+
|
|
165
|
+
Disc numbers must be non-negative integers.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
disc_number: The disc number to validate (int)
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
InvalidMetadataFieldFormatError: If the disc number format is invalid
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
>>> _MetadataManager.validate_disc_number(1) # Valid
|
|
175
|
+
>>> _MetadataManager.validate_disc_number(0) # Valid
|
|
176
|
+
>>> _MetadataManager.validate_disc_number(-1) # Raises InvalidMetadataFieldFormatError
|
|
177
|
+
"""
|
|
178
|
+
if not isinstance(disc_number, int):
|
|
179
|
+
raise InvalidMetadataFieldFormatError(
|
|
180
|
+
UnifiedMetadataKey.DISC_NUMBER.value, "integer", str(type(disc_number).__name__)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if disc_number < 0:
|
|
184
|
+
raise InvalidMetadataFieldFormatError(
|
|
185
|
+
UnifiedMetadataKey.DISC_NUMBER.value, "non-negative integer", str(disc_number)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def validate_disc_total(disc_total: int | None) -> None:
|
|
190
|
+
"""Validate disc total format.
|
|
191
|
+
|
|
192
|
+
Disc totals must be non-negative integers or None.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
disc_total: The disc total to validate (int or None)
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
InvalidMetadataFieldFormatError: If the disc total format is invalid
|
|
199
|
+
|
|
200
|
+
Examples:
|
|
201
|
+
>>> _MetadataManager.validate_disc_total(2) # Valid
|
|
202
|
+
>>> _MetadataManager.validate_disc_total(None) # Valid
|
|
203
|
+
>>> _MetadataManager.validate_disc_total(-1) # Raises InvalidMetadataFieldFormatError
|
|
204
|
+
"""
|
|
205
|
+
if disc_total is None:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if not isinstance(disc_total, int):
|
|
209
|
+
raise InvalidMetadataFieldFormatError(
|
|
210
|
+
UnifiedMetadataKey.DISC_TOTAL.value, "integer or None", str(type(disc_total).__name__)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if disc_total < 0:
|
|
214
|
+
raise InvalidMetadataFieldFormatError(
|
|
215
|
+
UnifiedMetadataKey.DISC_TOTAL.value, "non-negative integer", str(disc_total)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def validate_isrc(isrc: str) -> None:
|
|
220
|
+
"""Validate ISRC (International Standard Recording Code) format.
|
|
221
|
+
|
|
222
|
+
ISRC must be in one of the following formats:
|
|
223
|
+
- 12 alphanumeric characters without hyphens (e.g., "USRC17607839")
|
|
224
|
+
- 15 characters with hyphens in format CC-XXX-YY-NNNNN (e.g., "US-RC1-76-07839")
|
|
225
|
+
where CC=country code, XXX=registrant, YY=year, NNNNN=unique ID
|
|
226
|
+
- Empty string is allowed (represents no ISRC)
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
isrc: The ISRC string to validate
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
InvalidMetadataFieldFormatError: If the ISRC format is invalid
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
>>> _MetadataManager.validate_isrc("USRC17607839") # Valid (12 chars)
|
|
236
|
+
>>> _MetadataManager.validate_isrc("US-RC1-76-07839") # Valid (with hyphens)
|
|
237
|
+
>>> _MetadataManager.validate_isrc("") # Valid (empty string)
|
|
238
|
+
>>> _MetadataManager.validate_isrc("ABC") # Raises InvalidMetadataFieldFormatError
|
|
239
|
+
"""
|
|
240
|
+
if not isrc:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# 12 alphanumeric characters without hyphens
|
|
244
|
+
if re.match(r"^[A-Za-z0-9]{12}$", isrc):
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# 15 characters with hyphens: CC-XXX-YY-NNNNN
|
|
248
|
+
if re.match(r"^[A-Za-z]{2}-[A-Za-z0-9]{3}-\d{2}-\d{5}$", isrc):
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
raise InvalidMetadataFieldFormatError(
|
|
252
|
+
UnifiedMetadataKey.ISRC.value,
|
|
253
|
+
"12 alphanumeric characters (e.g., 'USRC17607839') or 15 characters with hyphens "
|
|
254
|
+
"(e.g., 'US-RC1-76-07839')",
|
|
255
|
+
isrc,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
@abstractmethod
|
|
259
|
+
def _extract_mutagen_metadata(self) -> MutagenMetadata:
|
|
260
|
+
raise NotImplementedError
|
|
261
|
+
|
|
262
|
+
@abstractmethod
|
|
263
|
+
def _convert_raw_mutagen_metadata_to_dict_with_potential_duplicate_keys(
|
|
264
|
+
self, raw_mutagen_metadata: MutagenMetadata
|
|
265
|
+
) -> RawMetadataDict:
|
|
266
|
+
raise NotImplementedError
|
|
267
|
+
|
|
268
|
+
@abstractmethod
|
|
269
|
+
def _get_undirectly_mapped_metadata_value_from_raw_clean_metadata(
|
|
270
|
+
self, raw_clean_metadata_uppercase_keys: RawMetadataDict, unified_metadata_key: UnifiedMetadataKey
|
|
271
|
+
) -> UnifiedMetadataValue:
|
|
272
|
+
raise NotImplementedError
|
|
273
|
+
|
|
274
|
+
@abstractmethod
|
|
275
|
+
def _update_undirectly_mapped_metadata(
|
|
276
|
+
self,
|
|
277
|
+
raw_mutagen_metadata: MutagenMetadata,
|
|
278
|
+
app_metadata_value: UnifiedMetadataValue,
|
|
279
|
+
unified_metadata_key: UnifiedMetadataKey,
|
|
280
|
+
) -> None:
|
|
281
|
+
raise NotImplementedError
|
|
282
|
+
|
|
283
|
+
@abstractmethod
|
|
284
|
+
def _update_formatted_value_in_raw_mutagen_metadata(
|
|
285
|
+
self,
|
|
286
|
+
raw_mutagen_metadata: MutagenMetadata,
|
|
287
|
+
raw_metadata_key: RawMetadataKey,
|
|
288
|
+
app_metadata_value: UnifiedMetadataValue,
|
|
289
|
+
) -> None:
|
|
290
|
+
raise NotImplementedError
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def _update_not_using_mutagen_metadata(self, unified_metadata: UnifiedMetadata) -> None:
|
|
294
|
+
raise NotImplementedError
|
|
295
|
+
|
|
296
|
+
def _extract_cleaned_raw_metadata_from_file(self) -> RawMetadataDict:
|
|
297
|
+
self.raw_mutagen_metadata = self._extract_mutagen_metadata()
|
|
298
|
+
raw_metadata_with_potential_duplicate_keys = (
|
|
299
|
+
self._convert_raw_mutagen_metadata_to_dict_with_potential_duplicate_keys(self.raw_mutagen_metadata)
|
|
300
|
+
)
|
|
301
|
+
return self._extract_and_regroup_raw_metadata_unique_entries(raw_metadata_with_potential_duplicate_keys)
|
|
302
|
+
|
|
303
|
+
def _extract_raw_clean_metadata_uppercase_keys_from_file(self) -> None:
|
|
304
|
+
if self.raw_clean_metadata is None:
|
|
305
|
+
self.raw_clean_metadata = self._extract_cleaned_raw_metadata_from_file()
|
|
306
|
+
|
|
307
|
+
# raw_clean_metadata is RawMetadataDict, so all keys are RawMetadataKey enum members
|
|
308
|
+
# Note: VorbisManager overrides this method to handle case-insensitive key merging
|
|
309
|
+
# since Vorbis comments preserve original key case as strings
|
|
310
|
+
self.raw_clean_metadata_uppercase_keys = dict(self.raw_clean_metadata)
|
|
311
|
+
|
|
312
|
+
def _should_apply_smart_parsing(self, values_list_str: list[str]) -> bool:
|
|
313
|
+
"""Determine if smart parsing should be applied based on entry count and null separators.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
values_list_str: List of string values from the metadata
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if parsing should be applied, False otherwise
|
|
320
|
+
"""
|
|
321
|
+
if not values_list_str:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
# Count non-empty entries
|
|
325
|
+
non_empty_entries = [val.strip() for val in values_list_str if val.strip()]
|
|
326
|
+
|
|
327
|
+
if len(non_empty_entries) == 0:
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
# Check if any entry contains null separators
|
|
331
|
+
has_null_separators = any("\x00" in entry for entry in non_empty_entries)
|
|
332
|
+
|
|
333
|
+
# If null separators are present, always apply parsing (null separation logic)
|
|
334
|
+
if has_null_separators:
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
# If we have multiple entries without null separators, don't parse (preserve separators)
|
|
338
|
+
# If we have a single entry without null separators, parse it (legacy data detection)
|
|
339
|
+
return len(non_empty_entries) <= 1
|
|
340
|
+
|
|
341
|
+
def _apply_smart_parsing(self, values_list_str: list[str]) -> list[str]:
|
|
342
|
+
"""Apply smart parsing to split values.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
values_list_str: List of string values to parse
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List of parsed values
|
|
349
|
+
"""
|
|
350
|
+
if not values_list_str:
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
# Get non-empty values
|
|
354
|
+
non_empty_values = [val.strip() for val in values_list_str if val.strip()]
|
|
355
|
+
if not non_empty_values:
|
|
356
|
+
return []
|
|
357
|
+
|
|
358
|
+
# Check if any entry contains null separators
|
|
359
|
+
has_null_separators = any("\x00" in entry for entry in non_empty_values)
|
|
360
|
+
|
|
361
|
+
if has_null_separators:
|
|
362
|
+
# Apply null separation logic across all entries
|
|
363
|
+
result = []
|
|
364
|
+
for entry in non_empty_values:
|
|
365
|
+
if "\x00" in entry:
|
|
366
|
+
# Split on null separator and add non-empty parts
|
|
367
|
+
parts = [p.strip() for p in entry.split("\x00") if p.strip()]
|
|
368
|
+
result.extend(parts)
|
|
369
|
+
else:
|
|
370
|
+
# Entry without null separator, add as-is
|
|
371
|
+
result.append(entry)
|
|
372
|
+
return result
|
|
373
|
+
|
|
374
|
+
# No null separators - use logic for single entry
|
|
375
|
+
first_value = non_empty_values[0]
|
|
376
|
+
|
|
377
|
+
# Find the highest-priority separator that actually exists in the value.
|
|
378
|
+
# We should only split on that separator (single-entry fields that
|
|
379
|
+
# used one specific separator) rather than splitting on every known
|
|
380
|
+
# separator sequentially which can produce incorrect fragmentation when
|
|
381
|
+
# lower-priority separators appear inside values.
|
|
382
|
+
for separator in METADATA_MULTI_VALUE_SEPARATORS_PRIORITIZED:
|
|
383
|
+
if separator in first_value:
|
|
384
|
+
return [p.strip() for p in first_value.split(separator) if p.strip()]
|
|
385
|
+
|
|
386
|
+
# No known separator found; return the single trimmed value as list
|
|
387
|
+
return [first_value.strip()]
|
|
388
|
+
|
|
389
|
+
def _extract_and_regroup_raw_metadata_unique_entries(
|
|
390
|
+
self, raw_metadata_with_potential_duplicate_keys: RawMetadataDict
|
|
391
|
+
) -> RawMetadataDict:
|
|
392
|
+
raw_clean_metadata: RawMetadataDict = {}
|
|
393
|
+
|
|
394
|
+
for raw_metadata_key, raw_metadata_value in raw_metadata_with_potential_duplicate_keys.items():
|
|
395
|
+
if raw_metadata_value is None:
|
|
396
|
+
raw_clean_metadata[raw_metadata_key] = None
|
|
397
|
+
elif isinstance(raw_metadata_value, list):
|
|
398
|
+
raw_clean_metadata[raw_metadata_key] = raw_metadata_value
|
|
399
|
+
|
|
400
|
+
return raw_clean_metadata
|
|
401
|
+
|
|
402
|
+
def _get_genre_name_from_raw_clean_metadata_id3v1(
|
|
403
|
+
self, raw_clean_metadata: RawMetadataDict, raw_metadata_ket: RawMetadataKey
|
|
404
|
+
) -> UnifiedMetadataValue:
|
|
405
|
+
"""RIFF and ID3v1 files typically contain a genre code.
|
|
406
|
+
|
|
407
|
+
that corresponds to the ID3v1 genre list. This method converts the code to a human-readable genre name.
|
|
408
|
+
"""
|
|
409
|
+
if raw_metadata_ket in raw_clean_metadata:
|
|
410
|
+
raw_value_list = raw_clean_metadata.get(raw_metadata_ket)
|
|
411
|
+
if not raw_value_list or len(raw_value_list) == 0:
|
|
412
|
+
return None
|
|
413
|
+
raw_value = raw_value_list[0]
|
|
414
|
+
try:
|
|
415
|
+
genre_code_or_name = int(cast(int, raw_value))
|
|
416
|
+
genre_name = ID3V1_GENRE_CODE_MAP.get(genre_code_or_name)
|
|
417
|
+
except ValueError:
|
|
418
|
+
genre_name = cast(str, raw_value)
|
|
419
|
+
|
|
420
|
+
# Return as list since GENRES_NAMES is a multi-value field
|
|
421
|
+
return [genre_name] if genre_name else None
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
def _get_genres_from_raw_clean_metadata_uppercase_keys(
|
|
425
|
+
self, raw_clean_metadata: RawMetadataDict, raw_metadata_key: RawMetadataKey
|
|
426
|
+
) -> UnifiedMetadataValue:
|
|
427
|
+
"""Extract and process genre entries from raw metadata according to the intelligent genre reading logic.
|
|
428
|
+
|
|
429
|
+
This method implements the comprehensive genre reading strategy that handles:
|
|
430
|
+
1. Multiple genre entries from the file
|
|
431
|
+
2. Separator parsing for single entries (text with separators, codes, code+text)
|
|
432
|
+
3. ID3v1 genre code conversion
|
|
433
|
+
4. Consistent list output of genre names
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
raw_clean_metadata: Dictionary of raw metadata values
|
|
437
|
+
raw_metadata_key: The raw metadata key for genres
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
List of genre names, or None if no genres found
|
|
441
|
+
"""
|
|
442
|
+
if raw_metadata_key not in raw_clean_metadata:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
raw_value_list = raw_clean_metadata.get(raw_metadata_key)
|
|
446
|
+
if not raw_value_list:
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
# Step 1: Extract all genre entries from the file
|
|
450
|
+
genre_entries = [str(entry).strip() for entry in raw_value_list if str(entry).strip()]
|
|
451
|
+
|
|
452
|
+
if not genre_entries:
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
# Step 2: Process entries based on count
|
|
456
|
+
if len(genre_entries) == 1:
|
|
457
|
+
# Single entry - apply separator parsing if needed
|
|
458
|
+
single_entry = genre_entries[0]
|
|
459
|
+
|
|
460
|
+
# Check for codes or code+text without separators (e.g., "(17)(6)", "(17)Rock(6)Blues")
|
|
461
|
+
if self._has_genre_separators(single_entry):
|
|
462
|
+
parsed_genres = self._parse_genre_separators(single_entry)
|
|
463
|
+
elif self._has_genre_codes_without_separators(single_entry):
|
|
464
|
+
parsed_genres = self._parse_genre_codes_and_text(single_entry)
|
|
465
|
+
# Check for text with separators (e.g., "Rock/Blues", "Rock; Alternative")
|
|
466
|
+
else:
|
|
467
|
+
# No special parsing needed
|
|
468
|
+
parsed_genres = [single_entry]
|
|
469
|
+
else:
|
|
470
|
+
# Multiple entries - use as-is (no separator parsing)
|
|
471
|
+
parsed_genres = genre_entries
|
|
472
|
+
|
|
473
|
+
# Step 3: Convert any genre codes or codes + names to names using ID3v1 genre code map
|
|
474
|
+
converted_genres = []
|
|
475
|
+
for genre in parsed_genres:
|
|
476
|
+
converted = self._convert_genre_code_or_text_to_name(genre)
|
|
477
|
+
if converted:
|
|
478
|
+
converted_genres.append(converted)
|
|
479
|
+
|
|
480
|
+
# Step 4: Return list of genre names (remove duplicates while preserving order)
|
|
481
|
+
unique_genres = []
|
|
482
|
+
for genre in converted_genres:
|
|
483
|
+
if genre not in unique_genres:
|
|
484
|
+
unique_genres.append(genre)
|
|
485
|
+
return unique_genres if unique_genres else None
|
|
486
|
+
|
|
487
|
+
def _has_genre_codes_without_separators(self, genre_string: str) -> bool:
|
|
488
|
+
"""Check if a genre string contains genre codes without separators.
|
|
489
|
+
|
|
490
|
+
Examples: "(17)(6)", "(17)Rock(6)Blues", "(17)Rock(6)"
|
|
491
|
+
"""
|
|
492
|
+
import re
|
|
493
|
+
|
|
494
|
+
# Pattern matches parentheses with digits, optionally followed by text, repeated
|
|
495
|
+
pattern = r"^\(\d+\)(?:\w*\(\d+\))*\w*$"
|
|
496
|
+
return bool(re.match(pattern, genre_string))
|
|
497
|
+
|
|
498
|
+
def _has_genre_separators(self, genre_string: str) -> bool:
|
|
499
|
+
"""Check if a genre string contains separators for multiple genres.
|
|
500
|
+
|
|
501
|
+
Examples: "Rock/Blues", "Rock; Alternative", "(17)Rock/(6)Blues"
|
|
502
|
+
"""
|
|
503
|
+
# Check for common separators
|
|
504
|
+
|
|
505
|
+
return any(sep in genre_string for sep in METADATA_MULTI_VALUE_SEPARATORS_PRIORITIZED)
|
|
506
|
+
|
|
507
|
+
def _parse_genre_codes_and_text(self, genre_string: str) -> list[str]:
|
|
508
|
+
"""Parse genre codes and code+text combinations without separators.
|
|
509
|
+
|
|
510
|
+
Examples: "(17)(6)" -> ["(17)", "(6)"]
|
|
511
|
+
"(17)Rock(6)Blues" -> ["(17)Rock", "(6)Blues"]
|
|
512
|
+
"""
|
|
513
|
+
import re
|
|
514
|
+
|
|
515
|
+
# Find all consecutive (number)text patterns
|
|
516
|
+
# Each match is a complete code or code+text unit
|
|
517
|
+
pattern = r"\(\d+\)[^(\d]*"
|
|
518
|
+
matches = re.findall(pattern, genre_string)
|
|
519
|
+
|
|
520
|
+
if matches:
|
|
521
|
+
# Filter out any empty matches
|
|
522
|
+
return [match for match in matches if match]
|
|
523
|
+
|
|
524
|
+
# Fallback: if no matches, return the original string
|
|
525
|
+
return [genre_string]
|
|
526
|
+
|
|
527
|
+
def _parse_genre_separators(self, genre_string: str) -> list[str]:
|
|
528
|
+
"""Parse genre strings with separators using smart separator logic.
|
|
529
|
+
|
|
530
|
+
Examples: "Rock/Blues" -> ["Rock", "Blues"]
|
|
531
|
+
"(17)Rock/(6)Blues" -> ["(17)Rock", "(6)Blues"]
|
|
532
|
+
"""
|
|
533
|
+
# Use the same separator priority as multi-value parsing
|
|
534
|
+
for separator in METADATA_MULTI_VALUE_SEPARATORS_PRIORITIZED:
|
|
535
|
+
if separator in genre_string:
|
|
536
|
+
return [part.strip() for part in genre_string.split(separator) if part.strip()]
|
|
537
|
+
# No separator found
|
|
538
|
+
return [genre_string]
|
|
539
|
+
|
|
540
|
+
def _convert_genre_code_or_text_to_name(self, genre_entry: str) -> str | None:
|
|
541
|
+
"""Convert a genre code or code+text entry to a genre name using ID3V1_GENRE_CODE_MAP.
|
|
542
|
+
|
|
543
|
+
For code + text entries, use text part only for more flexibility.
|
|
544
|
+
|
|
545
|
+
Examples:
|
|
546
|
+
- "(17)" -> "Rock"
|
|
547
|
+
- "(17)Rock" -> "Rock" (text part preferred)
|
|
548
|
+
- "17" -> "Rock" (bare numeric code)
|
|
549
|
+
- "Rock" -> "Rock"
|
|
550
|
+
- "(999)" -> None (invalid code)
|
|
551
|
+
"""
|
|
552
|
+
import re
|
|
553
|
+
|
|
554
|
+
# Check for code + text pattern: (number)text
|
|
555
|
+
code_text_match = re.match(r"^\((\d+)\)(.+)$", genre_entry)
|
|
556
|
+
if code_text_match:
|
|
557
|
+
code = int(code_text_match.group(1))
|
|
558
|
+
text_part = code_text_match.group(2).strip()
|
|
559
|
+
# For code + text entries, use text part only for more flexibility
|
|
560
|
+
if text_part:
|
|
561
|
+
return text_part
|
|
562
|
+
|
|
563
|
+
# Check for code only pattern: (number)
|
|
564
|
+
code_only_match = re.match(r"^\((\d+)\)$", genre_entry)
|
|
565
|
+
if code_only_match:
|
|
566
|
+
code = int(code_only_match.group(1))
|
|
567
|
+
return ID3V1_GENRE_CODE_MAP.get(code)
|
|
568
|
+
|
|
569
|
+
# Check for bare numeric code: number (without parentheses)
|
|
570
|
+
if genre_entry.isdigit():
|
|
571
|
+
code = int(genre_entry)
|
|
572
|
+
return ID3V1_GENRE_CODE_MAP.get(code)
|
|
573
|
+
|
|
574
|
+
# No code found, return as-is
|
|
575
|
+
return genre_entry if genre_entry else None
|
|
576
|
+
|
|
577
|
+
def get_unified_metadata(self) -> UnifiedMetadata:
|
|
578
|
+
unified_metadata: UnifiedMetadata = {}
|
|
579
|
+
for metadata_key in self.metadata_keys_direct_map_read:
|
|
580
|
+
unified_metadata_value = self.get_unified_metadata_field(metadata_key)
|
|
581
|
+
if unified_metadata_value is not None:
|
|
582
|
+
unified_metadata[metadata_key] = unified_metadata_value
|
|
583
|
+
return unified_metadata
|
|
584
|
+
|
|
585
|
+
def get_unified_metadata_field(self, unified_metadata_key: UnifiedMetadataKey) -> UnifiedMetadataValue:
|
|
586
|
+
if unified_metadata_key not in self.metadata_keys_direct_map_read:
|
|
587
|
+
metadata_format_name = self._get_formatted_metadata_format_name()
|
|
588
|
+
msg = f"{unified_metadata_key} metadata not supported by {metadata_format_name} format"
|
|
589
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
590
|
+
|
|
591
|
+
if self.raw_clean_metadata_uppercase_keys is None:
|
|
592
|
+
self._extract_raw_clean_metadata_uppercase_keys_from_file()
|
|
593
|
+
|
|
594
|
+
raw_metadata_key = self.metadata_keys_direct_map_read[unified_metadata_key]
|
|
595
|
+
if not raw_metadata_key:
|
|
596
|
+
if self.raw_clean_metadata_uppercase_keys is None:
|
|
597
|
+
return None
|
|
598
|
+
return self._get_undirectly_mapped_metadata_value_from_raw_clean_metadata(
|
|
599
|
+
raw_clean_metadata_uppercase_keys=self.raw_clean_metadata_uppercase_keys,
|
|
600
|
+
unified_metadata_key=unified_metadata_key,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if self.raw_clean_metadata_uppercase_keys is None:
|
|
604
|
+
return None
|
|
605
|
+
if raw_metadata_key not in self.raw_clean_metadata_uppercase_keys:
|
|
606
|
+
return None
|
|
607
|
+
value = self.raw_clean_metadata_uppercase_keys[raw_metadata_key]
|
|
608
|
+
|
|
609
|
+
if not value or not len(value):
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
# For string types, we need to distinguish between None (not present) and empty string (present but empty)
|
|
613
|
+
unified_metadata_key_optional_type = unified_metadata_key.get_optional_type()
|
|
614
|
+
if unified_metadata_key_optional_type is str and value[0] == "":
|
|
615
|
+
return ""
|
|
616
|
+
|
|
617
|
+
if not value[0]:
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
# Special handling for TRACK_NUMBER parsing
|
|
621
|
+
if unified_metadata_key == UnifiedMetadataKey.TRACK_NUMBER:
|
|
622
|
+
track_str = str(value[0])
|
|
623
|
+
if re.match(r"^\d+([-/]\d*)?$", track_str):
|
|
624
|
+
return track_str
|
|
625
|
+
return None
|
|
626
|
+
|
|
627
|
+
from typing import get_args, get_origin
|
|
628
|
+
|
|
629
|
+
origin = get_origin(unified_metadata_key_optional_type)
|
|
630
|
+
if unified_metadata_key_optional_type is int:
|
|
631
|
+
return int(value[0]) if value else None
|
|
632
|
+
if unified_metadata_key_optional_type is float:
|
|
633
|
+
return float(value[0]) if value else None
|
|
634
|
+
if origin is not None and (origin == Union or (hasattr(origin, "__name__") and origin.__name__ == "UnionType")):
|
|
635
|
+
# Handle union types like int | float or int | None
|
|
636
|
+
arg_types = get_args(unified_metadata_key_optional_type)
|
|
637
|
+
if int in arg_types and float in arg_types:
|
|
638
|
+
# For int | float, prefer int if it's a whole number, otherwise float
|
|
639
|
+
try:
|
|
640
|
+
num_value = float(value[0]) if value else None
|
|
641
|
+
if num_value is not None and num_value.is_integer():
|
|
642
|
+
return int(num_value)
|
|
643
|
+
else: # noqa: RET505
|
|
644
|
+
return num_value
|
|
645
|
+
except (ValueError, TypeError):
|
|
646
|
+
return None
|
|
647
|
+
if int in arg_types and type(None) in arg_types:
|
|
648
|
+
# For int | None, convert to int if value exists, otherwise None
|
|
649
|
+
try:
|
|
650
|
+
return int(value[0]) if value else None
|
|
651
|
+
except (ValueError, TypeError):
|
|
652
|
+
return None
|
|
653
|
+
if unified_metadata_key_optional_type is str:
|
|
654
|
+
return str(value[0]) if value else None
|
|
655
|
+
if unified_metadata_key_optional_type == list[str]:
|
|
656
|
+
return self._get_value_from_multi_values_data(
|
|
657
|
+
unified_metadata_key, cast(list[str], value), raw_metadata_key
|
|
658
|
+
)
|
|
659
|
+
msg = f"Unsupported metadata type: {unified_metadata_key_optional_type}"
|
|
660
|
+
raise ValueError(msg)
|
|
661
|
+
|
|
662
|
+
def _get_value_from_multi_values_data(
|
|
663
|
+
self, unified_metadata_key: UnifiedMetadataKey, value: list[str], raw_metadata_key: RawMetadataKey
|
|
664
|
+
) -> UnifiedMetadataValue:
|
|
665
|
+
if not value:
|
|
666
|
+
return None
|
|
667
|
+
values_list_str = value
|
|
668
|
+
if unified_metadata_key == UnifiedMetadataKey.GENRES_NAMES:
|
|
669
|
+
# Use specialized genre reading logic
|
|
670
|
+
if self.raw_clean_metadata_uppercase_keys is None:
|
|
671
|
+
return None
|
|
672
|
+
return self._get_genres_from_raw_clean_metadata_uppercase_keys(
|
|
673
|
+
self.raw_clean_metadata_uppercase_keys, raw_metadata_key
|
|
674
|
+
)
|
|
675
|
+
if unified_metadata_key.can_semantically_have_multiple_values():
|
|
676
|
+
# Apply smart parsing logic for semantically multi-value fields
|
|
677
|
+
if self._should_apply_smart_parsing(values_list_str):
|
|
678
|
+
# Apply parsing for single entry (legacy data detection)
|
|
679
|
+
parsed_values = self._apply_smart_parsing(values_list_str)
|
|
680
|
+
return parsed_values if parsed_values else None
|
|
681
|
+
# No parsing - return as-is but filter empty/whitespace values
|
|
682
|
+
filtered_values = [val.strip() for val in values_list_str if val.strip()]
|
|
683
|
+
return filtered_values if filtered_values else None
|
|
684
|
+
return values_list_str
|
|
685
|
+
|
|
686
|
+
def get_header_info(self) -> dict:
|
|
687
|
+
"""Get header information for this metadata format.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Dictionary containing header information:
|
|
691
|
+
{
|
|
692
|
+
'present': boolean
|
|
693
|
+
'version': str | None,
|
|
694
|
+
'size_bytes': int | None,
|
|
695
|
+
'position': int | None,
|
|
696
|
+
'flags': dict,
|
|
697
|
+
'extended_header': dict
|
|
698
|
+
}
|
|
699
|
+
"""
|
|
700
|
+
msg = "Not implemented for this format"
|
|
701
|
+
raise NotImplementedError(msg)
|
|
702
|
+
|
|
703
|
+
def get_raw_metadata_info(self) -> dict:
|
|
704
|
+
"""Get raw metadata information for this format.
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
Dictionary containing raw metadata details:
|
|
708
|
+
{
|
|
709
|
+
'raw_data': bytes | None,
|
|
710
|
+
'parsed_fields': dict,
|
|
711
|
+
'frames': dict,
|
|
712
|
+
'comments': dict,
|
|
713
|
+
'chunk_structure': dict
|
|
714
|
+
}
|
|
715
|
+
"""
|
|
716
|
+
msg = "Not implemented for this format"
|
|
717
|
+
raise NotImplementedError(msg)
|
|
718
|
+
|
|
719
|
+
def update_metadata(self, unified_metadata: UnifiedMetadata) -> None:
|
|
720
|
+
if not self.metadata_keys_direct_map_write:
|
|
721
|
+
msg = "This format does not support metadata modification"
|
|
722
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
723
|
+
|
|
724
|
+
if not self.update_using_mutagen_metadata:
|
|
725
|
+
self._update_not_using_mutagen_metadata(unified_metadata)
|
|
726
|
+
else:
|
|
727
|
+
if self.raw_mutagen_metadata is None:
|
|
728
|
+
self.raw_mutagen_metadata = self._extract_mutagen_metadata()
|
|
729
|
+
|
|
730
|
+
for unified_metadata_key in list(unified_metadata.keys()):
|
|
731
|
+
app_metadata_value = unified_metadata[unified_metadata_key]
|
|
732
|
+
|
|
733
|
+
# Filter out empty values for list-type metadata before processing
|
|
734
|
+
if isinstance(app_metadata_value, list):
|
|
735
|
+
app_metadata_value = self._filter_valid_values(cast(list[str | None], app_metadata_value))
|
|
736
|
+
# If all values were filtered out, set to None to remove the field
|
|
737
|
+
if not app_metadata_value:
|
|
738
|
+
app_metadata_value = None
|
|
739
|
+
|
|
740
|
+
if unified_metadata_key not in self.metadata_keys_direct_map_write:
|
|
741
|
+
metadata_format_name = self._get_formatted_metadata_format_name()
|
|
742
|
+
msg = f"{unified_metadata_key} metadata not supported by {metadata_format_name} format"
|
|
743
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
744
|
+
raw_metadata_key = self.metadata_keys_direct_map_write[unified_metadata_key]
|
|
745
|
+
if raw_metadata_key:
|
|
746
|
+
self._update_formatted_value_in_raw_mutagen_metadata(
|
|
747
|
+
raw_mutagen_metadata=self.raw_mutagen_metadata,
|
|
748
|
+
raw_metadata_key=raw_metadata_key,
|
|
749
|
+
app_metadata_value=app_metadata_value,
|
|
750
|
+
)
|
|
751
|
+
else:
|
|
752
|
+
self._update_undirectly_mapped_metadata(
|
|
753
|
+
raw_mutagen_metadata=self.raw_mutagen_metadata,
|
|
754
|
+
app_metadata_value=app_metadata_value,
|
|
755
|
+
unified_metadata_key=unified_metadata_key,
|
|
756
|
+
)
|
|
757
|
+
self.raw_mutagen_metadata.save(self.audio_file.file_path)
|
|
758
|
+
|
|
759
|
+
def delete_metadata(self) -> bool:
|
|
760
|
+
if self.raw_mutagen_metadata is None:
|
|
761
|
+
self.raw_mutagen_metadata = self._extract_mutagen_metadata()
|
|
762
|
+
|
|
763
|
+
try:
|
|
764
|
+
self.raw_mutagen_metadata.delete()
|
|
765
|
+
except Exception:
|
|
766
|
+
return False
|
|
767
|
+
else:
|
|
768
|
+
return True
|