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,298 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Manual implementation to create multiple separate RIFF metadata fields for testing.
|
|
3
|
+
|
|
4
|
+
This bypasses standard tools and libraries that typically overwrite fields with the same FourCC, allowing creation of
|
|
5
|
+
test files with truly separate IART, IGNR, etc. fields within the same INFO chunk.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ManualRIFFMetadataCreator:
|
|
13
|
+
"""Creates RIFF INFO chunks with multiple separate fields by manual binary construction."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def create_multiple_title_fields(file_path: Path, titles: list[str]) -> None:
|
|
17
|
+
"""Create multiple separate INAM fields in the RIFF INFO chunk."""
|
|
18
|
+
fields = []
|
|
19
|
+
for title in titles:
|
|
20
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("INAM", title)
|
|
21
|
+
fields.append(field_data)
|
|
22
|
+
|
|
23
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def create_multiple_artist_fields(file_path: Path, artists: list[str]) -> None:
|
|
27
|
+
"""Create multiple separate IART fields in the RIFF INFO chunk."""
|
|
28
|
+
fields = []
|
|
29
|
+
for artist in artists:
|
|
30
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("IART", artist)
|
|
31
|
+
fields.append(field_data)
|
|
32
|
+
|
|
33
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def create_multiple_genre_fields(file_path: Path, genres: list[str]) -> None:
|
|
37
|
+
"""Create multiple separate IGNR fields in the RIFF INFO chunk."""
|
|
38
|
+
fields = []
|
|
39
|
+
for genre in genres:
|
|
40
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("IGNR", genre)
|
|
41
|
+
fields.append(field_data)
|
|
42
|
+
|
|
43
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def create_multiple_composer_fields(file_path: Path, composers: list[str]) -> None:
|
|
47
|
+
"""Create multiple separate ICMP fields in the RIFF INFO chunk."""
|
|
48
|
+
fields = []
|
|
49
|
+
for composer in composers:
|
|
50
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("ICMP", composer)
|
|
51
|
+
fields.append(field_data)
|
|
52
|
+
|
|
53
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def create_multiple_album_artist_fields(file_path: Path, album_artists: list[str]) -> None:
|
|
57
|
+
"""Create multiple separate IAAR fields in the RIFF INFO chunk."""
|
|
58
|
+
fields = []
|
|
59
|
+
for album_artist in album_artists:
|
|
60
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("IAAR", album_artist)
|
|
61
|
+
fields.append(field_data)
|
|
62
|
+
|
|
63
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def create_multiple_comment_fields(file_path: Path, comments: list[str]) -> None:
|
|
67
|
+
"""Create multiple separate ICMT fields in the RIFF INFO chunk."""
|
|
68
|
+
fields = []
|
|
69
|
+
for comment in comments:
|
|
70
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("ICMT", comment)
|
|
71
|
+
fields.append(field_data)
|
|
72
|
+
|
|
73
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def create_mixed_multiple_fields(file_path: Path, artists: list[str], genres: list[str]) -> None:
|
|
77
|
+
"""Create multiple fields of different types in the RIFF INFO chunk."""
|
|
78
|
+
fields = []
|
|
79
|
+
|
|
80
|
+
# Add multiple IART fields
|
|
81
|
+
for artist in artists:
|
|
82
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("IART", artist)
|
|
83
|
+
fields.append(field_data)
|
|
84
|
+
|
|
85
|
+
# Add multiple IGNR fields
|
|
86
|
+
for genre in genres:
|
|
87
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("IGNR", genre)
|
|
88
|
+
fields.append(field_data)
|
|
89
|
+
|
|
90
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, fields)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def create_bpm_field(file_path: Path, bpm: str) -> None:
|
|
94
|
+
"""Create IBPM field in the RIFF INFO chunk, preserving existing fields."""
|
|
95
|
+
# Read existing fields and add BPM
|
|
96
|
+
existing_fields = ManualRIFFMetadataCreator._read_existing_info_fields(file_path)
|
|
97
|
+
# Remove existing IBPM if present (we'll replace it)
|
|
98
|
+
existing_fields = [f for f in existing_fields if f[:4] != b"IBPM"]
|
|
99
|
+
# Add new BPM field
|
|
100
|
+
bpm_field = ManualRIFFMetadataCreator._create_info_field("IBPM", bpm)
|
|
101
|
+
all_fields = [*existing_fields, bpm_field]
|
|
102
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, all_fields)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def create_lyrics_field(file_path: Path, lyrics: str) -> None:
|
|
106
|
+
"""Create ILYR field in the RIFF INFO chunk."""
|
|
107
|
+
field_data = ManualRIFFMetadataCreator._create_info_field("ILYR", lyrics)
|
|
108
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, [field_data])
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def create_language_field(file_path: Path, language: str) -> None:
|
|
112
|
+
"""Create ILNG field in the RIFF INFO chunk, preserving existing fields."""
|
|
113
|
+
# Read existing fields and add language
|
|
114
|
+
existing_fields = ManualRIFFMetadataCreator._read_existing_info_fields(file_path)
|
|
115
|
+
# Remove existing ILNG if present (we'll replace it)
|
|
116
|
+
existing_fields = [f for f in existing_fields if f[:4] != b"ILNG"]
|
|
117
|
+
# Add new language field
|
|
118
|
+
language_field = ManualRIFFMetadataCreator._create_info_field("ILNG", language)
|
|
119
|
+
all_fields = [*existing_fields, language_field]
|
|
120
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, all_fields)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def create_composer_field(file_path: Path, composer: str) -> None:
|
|
124
|
+
"""Create ICMP field in the RIFF INFO chunk, preserving existing fields."""
|
|
125
|
+
# Read existing fields and add composer
|
|
126
|
+
existing_fields = ManualRIFFMetadataCreator._read_existing_info_fields(file_path)
|
|
127
|
+
# Remove existing ICMP if present (we'll replace it)
|
|
128
|
+
existing_fields = [f for f in existing_fields if f[:4] != b"ICMP"]
|
|
129
|
+
# Add new composer field
|
|
130
|
+
composer_field = ManualRIFFMetadataCreator._create_info_field("ICMP", composer)
|
|
131
|
+
all_fields = [*existing_fields, composer_field]
|
|
132
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, all_fields)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def create_rating_field(file_path: Path, rating: str) -> None:
|
|
136
|
+
"""Create IRTD field in the RIFF INFO chunk, preserving existing fields."""
|
|
137
|
+
# Read existing fields and add rating
|
|
138
|
+
existing_fields = ManualRIFFMetadataCreator._read_existing_info_fields(file_path)
|
|
139
|
+
# Remove existing IRTD if present (we'll replace it)
|
|
140
|
+
existing_fields = [f for f in existing_fields if f[:4] != b"IRTD"]
|
|
141
|
+
# Add new rating field
|
|
142
|
+
rating_field = ManualRIFFMetadataCreator._create_info_field("IRTD", rating)
|
|
143
|
+
all_fields = [*existing_fields, rating_field]
|
|
144
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, all_fields)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _create_info_field(field_id: str, text: str) -> bytes:
|
|
148
|
+
"""Create a single RIFF INFO field with the given FourCC and text."""
|
|
149
|
+
# Encode text as UTF-8 with null terminator
|
|
150
|
+
text_bytes = text.encode("utf-8") + b"\x00"
|
|
151
|
+
|
|
152
|
+
# Ensure proper word alignment (pad to even length)
|
|
153
|
+
if len(text_bytes) % 2:
|
|
154
|
+
text_bytes += b"\x00"
|
|
155
|
+
|
|
156
|
+
# RIFF field structure: FourCC (4 bytes) + size (4 bytes) + data
|
|
157
|
+
field_id_bytes = field_id.encode("ascii")
|
|
158
|
+
field_size = len(text_bytes)
|
|
159
|
+
|
|
160
|
+
field_header = field_id_bytes + struct.pack("<I", field_size) # Little-endian 32-bit size
|
|
161
|
+
|
|
162
|
+
return field_header + text_bytes
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _write_riff_info_chunk(file_path: Path, fields: list[bytes]) -> None:
|
|
166
|
+
"""Write RIFF INFO chunk with the given fields to the file."""
|
|
167
|
+
# Read existing file content
|
|
168
|
+
with file_path.open("rb") as f:
|
|
169
|
+
original_data = f.read()
|
|
170
|
+
|
|
171
|
+
# Skip any ID3v2 tags that might be present at the start
|
|
172
|
+
audio_data = ManualRIFFMetadataCreator._skip_id3v2_tags(original_data)
|
|
173
|
+
|
|
174
|
+
# Validate RIFF/WAVE header
|
|
175
|
+
if len(audio_data) < 12 or audio_data[:4] != b"RIFF" or audio_data[8:12] != b"WAVE":
|
|
176
|
+
msg = "Invalid WAV file format"
|
|
177
|
+
raise ValueError(msg)
|
|
178
|
+
|
|
179
|
+
# Find existing INFO chunk and remove it
|
|
180
|
+
audio_data_without_info = ManualRIFFMetadataCreator._remove_existing_info_chunk(audio_data)
|
|
181
|
+
|
|
182
|
+
# Calculate total size of all fields
|
|
183
|
+
fields_data = b"".join(fields)
|
|
184
|
+
|
|
185
|
+
# Create new INFO chunk
|
|
186
|
+
# LIST chunk structure: 'LIST' + size + type + data
|
|
187
|
+
info_chunk_data = b"INFO" + fields_data
|
|
188
|
+
info_chunk_size = len(info_chunk_data)
|
|
189
|
+
|
|
190
|
+
new_info_chunk = (
|
|
191
|
+
b"LIST" # LIST chunk identifier
|
|
192
|
+
+ struct.pack("<I", info_chunk_size) # Chunk size (little-endian)
|
|
193
|
+
+ info_chunk_data # INFO type + field data
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Insert new INFO chunk after RIFF header (after first 12 bytes)
|
|
197
|
+
new_file_data = (
|
|
198
|
+
audio_data_without_info[:12] # RIFF header
|
|
199
|
+
+ new_info_chunk # New INFO chunk
|
|
200
|
+
+ audio_data_without_info[12:] # Rest of audio data
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Update RIFF file size (total file size - 8 bytes for RIFF header)
|
|
204
|
+
total_size = len(new_file_data) - 8
|
|
205
|
+
new_file_data = (
|
|
206
|
+
new_file_data[:4] # 'RIFF'
|
|
207
|
+
+ struct.pack("<I", total_size) # Updated size
|
|
208
|
+
+ new_file_data[8:] # Rest of data
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Write new file
|
|
212
|
+
with file_path.open("wb") as f:
|
|
213
|
+
f.write(new_file_data)
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _skip_id3v2_tags(data: bytes) -> bytes:
|
|
217
|
+
"""Skip ID3v2 tags if present at the start of the file."""
|
|
218
|
+
if data.startswith(b"ID3"):
|
|
219
|
+
if len(data) < 10:
|
|
220
|
+
return data
|
|
221
|
+
|
|
222
|
+
# Get size from synchsafe integer (7 bits per byte)
|
|
223
|
+
size_bytes = data[6:10]
|
|
224
|
+
size = (
|
|
225
|
+
((size_bytes[0] & 0x7F) << 21)
|
|
226
|
+
| ((size_bytes[1] & 0x7F) << 14)
|
|
227
|
+
| ((size_bytes[2] & 0x7F) << 7)
|
|
228
|
+
| (size_bytes[3] & 0x7F)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Skip the header (10 bytes) plus the size of the tag
|
|
232
|
+
return data[10 + size :]
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _read_existing_info_fields(file_path: Path) -> list[bytes]:
|
|
237
|
+
"""Read existing INFO chunk fields from the file."""
|
|
238
|
+
with file_path.open("rb") as f:
|
|
239
|
+
data = f.read()
|
|
240
|
+
|
|
241
|
+
# Skip ID3v2 tags if present
|
|
242
|
+
audio_data = ManualRIFFMetadataCreator._skip_id3v2_tags(data)
|
|
243
|
+
|
|
244
|
+
fields = []
|
|
245
|
+
pos = 0
|
|
246
|
+
while pos < len(audio_data) - 8:
|
|
247
|
+
# Look for LIST chunk containing INFO
|
|
248
|
+
if audio_data[pos : pos + 4] == b"LIST" and pos + 12 <= len(audio_data):
|
|
249
|
+
chunk_size = int.from_bytes(audio_data[pos + 4 : pos + 8], "little")
|
|
250
|
+
if pos + 12 <= len(audio_data) and audio_data[pos + 8 : pos + 12] == b"INFO":
|
|
251
|
+
# Found INFO chunk, extract all fields
|
|
252
|
+
info_data = audio_data[pos + 12 : pos + 8 + chunk_size]
|
|
253
|
+
field_pos = 0
|
|
254
|
+
while field_pos < len(info_data) - 8:
|
|
255
|
+
if field_pos + 8 <= len(info_data):
|
|
256
|
+
field_size = int.from_bytes(info_data[field_pos + 4 : field_pos + 8], "little")
|
|
257
|
+
if field_pos + 8 + field_size <= len(info_data):
|
|
258
|
+
# Extract the entire field (header + data)
|
|
259
|
+
field_data = info_data[field_pos : field_pos + 8 + field_size]
|
|
260
|
+
# Ensure proper alignment
|
|
261
|
+
aligned_size = (field_size + 1) & ~1
|
|
262
|
+
if field_pos + 8 + aligned_size <= len(info_data):
|
|
263
|
+
field_data = info_data[field_pos : field_pos + 8 + aligned_size]
|
|
264
|
+
fields.append(field_data)
|
|
265
|
+
field_pos += 8 + aligned_size
|
|
266
|
+
else:
|
|
267
|
+
break
|
|
268
|
+
else:
|
|
269
|
+
break
|
|
270
|
+
break
|
|
271
|
+
pos += 1
|
|
272
|
+
|
|
273
|
+
return fields
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _remove_existing_info_chunk(data: bytes) -> bytes:
|
|
277
|
+
"""Remove existing INFO chunk from RIFF data if present."""
|
|
278
|
+
if len(data) < 12:
|
|
279
|
+
return data
|
|
280
|
+
|
|
281
|
+
result = bytearray(data[:12]) # Keep RIFF header
|
|
282
|
+
pos = 12 # Start after RIFF header
|
|
283
|
+
|
|
284
|
+
while pos < len(data) - 8:
|
|
285
|
+
chunk_id = data[pos : pos + 4]
|
|
286
|
+
chunk_size = struct.unpack("<I", data[pos + 4 : pos + 8])[0]
|
|
287
|
+
|
|
288
|
+
# Skip INFO chunk, keep others
|
|
289
|
+
if chunk_id == b"LIST" and pos + 12 <= len(data) and data[pos + 8 : pos + 12] == b"INFO":
|
|
290
|
+
# Skip this INFO chunk entirely
|
|
291
|
+
pos += 8 + ((chunk_size + 1) & ~1) # Move to next chunk with alignment
|
|
292
|
+
else:
|
|
293
|
+
# Keep this chunk
|
|
294
|
+
chunk_end = pos + 8 + ((chunk_size + 1) & ~1) # Include padding for alignment
|
|
295
|
+
result.extend(data[pos:chunk_end])
|
|
296
|
+
pos = chunk_end
|
|
297
|
+
|
|
298
|
+
return bytes(result)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""RIFF metadata deletion operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..common.external_tool_runner import run_external_tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RIFFMetadataDeleter:
|
|
9
|
+
"""Static utility class for RIFF metadata deletion using external bwfmetaedit tool."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def remove_chunk(file_path: Path, chunk_name: str) -> None:
|
|
13
|
+
"""Remove a specific RIFF chunk."""
|
|
14
|
+
try:
|
|
15
|
+
command = ["bwfmetaedit", f"--remove-chunks=INFO/{chunk_name}", str(file_path)]
|
|
16
|
+
run_external_tool(command, "bwfmetaedit")
|
|
17
|
+
except Exception:
|
|
18
|
+
# Ignore if chunk doesn't exist
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def delete_comment(file_path: Path) -> None:
|
|
23
|
+
"""Delete RIFF comment using bwfmetaedit tool."""
|
|
24
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "ICMT")
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def delete_title(file_path: Path) -> None:
|
|
28
|
+
"""Delete RIFF title using bwfmetaedit tool."""
|
|
29
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "INAM")
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def delete_artist(file_path: Path) -> None:
|
|
33
|
+
"""Delete RIFF artist using bwfmetaedit tool."""
|
|
34
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "IART")
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def delete_album(file_path: Path) -> None:
|
|
38
|
+
"""Delete RIFF album using bwfmetaedit tool."""
|
|
39
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "IPRD")
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def delete_genre(file_path: Path) -> None:
|
|
43
|
+
"""Delete RIFF genre using bwfmetaedit tool."""
|
|
44
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "IGNR")
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def delete_lyrics(file_path: Path) -> None:
|
|
48
|
+
"""Delete RIFF lyrics using bwfmetaedit tool."""
|
|
49
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "ILYT")
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def delete_language(file_path: Path) -> None:
|
|
53
|
+
"""Delete RIFF language using bwfmetaedit tool."""
|
|
54
|
+
RIFFMetadataDeleter.remove_chunk(file_path, "ILNG")
|
|
55
|
+
|
|
56
|
+
# RIFF doesn't support BPM field - use inherited pass implementation
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""RIFF metadata inspection utilities for testing audio file metadata."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..common.external_tool_runner import run_external_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RIFFMetadataGetter:
|
|
10
|
+
"""Utilities for inspecting RIFF metadata in audio files."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def get_raw_metadata(file_path: Path) -> str:
|
|
14
|
+
"""Inspect RIFF metadata using custom binary reading to detect multiple fields."""
|
|
15
|
+
# Read the file and find all RIFF INFO fields
|
|
16
|
+
with file_path.open("rb") as f:
|
|
17
|
+
data = f.read()
|
|
18
|
+
|
|
19
|
+
# Find all RIFF INFO fields
|
|
20
|
+
info_fields = {}
|
|
21
|
+
pos = 0
|
|
22
|
+
while pos < len(data) - 4:
|
|
23
|
+
# Look for RIFF INFO chunk
|
|
24
|
+
if data[pos : pos + 4] == b"LIST" and pos + 12 <= len(data) and data[pos + 8 : pos + 12] == b"INFO":
|
|
25
|
+
# Found INFO chunk, parse its fields
|
|
26
|
+
chunk_size = int.from_bytes(data[pos + 4 : pos + 8], "little")
|
|
27
|
+
info_data = data[pos + 12 : pos + 8 + chunk_size]
|
|
28
|
+
|
|
29
|
+
# Parse fields within INFO chunk
|
|
30
|
+
field_pos = 0
|
|
31
|
+
while field_pos < len(info_data) - 8:
|
|
32
|
+
if field_pos + 8 <= len(info_data):
|
|
33
|
+
field_id = info_data[field_pos : field_pos + 4]
|
|
34
|
+
field_size = int.from_bytes(info_data[field_pos + 4 : field_pos + 8], "little")
|
|
35
|
+
|
|
36
|
+
if field_pos + 8 + field_size <= len(info_data):
|
|
37
|
+
field_data = info_data[field_pos + 8 : field_pos + 8 + field_size]
|
|
38
|
+
# Remove null terminator
|
|
39
|
+
if field_data.endswith(b"\x00"):
|
|
40
|
+
field_data = field_data[:-1]
|
|
41
|
+
text = field_data.decode("utf-8", errors="ignore")
|
|
42
|
+
|
|
43
|
+
# Map RIFF field IDs to ffprobe-style tags
|
|
44
|
+
field_id_str = field_id.decode("ascii", errors="ignore")
|
|
45
|
+
tag_name = RIFFMetadataGetter._get_tag_name_for_field(field_id_str)
|
|
46
|
+
|
|
47
|
+
if tag_name not in info_fields:
|
|
48
|
+
info_fields[tag_name] = []
|
|
49
|
+
info_fields[tag_name].append(text)
|
|
50
|
+
|
|
51
|
+
# Move to next field (with alignment)
|
|
52
|
+
field_pos += 8 + ((field_size + 1) & ~1)
|
|
53
|
+
else:
|
|
54
|
+
break
|
|
55
|
+
break
|
|
56
|
+
pos += 1
|
|
57
|
+
|
|
58
|
+
# Format the output similar to ffprobe
|
|
59
|
+
result_lines = []
|
|
60
|
+
result_lines.append("[FORMAT]")
|
|
61
|
+
result_lines.append(f"filename={file_path}")
|
|
62
|
+
result_lines.append("nb_streams=1")
|
|
63
|
+
result_lines.append("nb_programs=0")
|
|
64
|
+
result_lines.append("nb_stream_groups=0")
|
|
65
|
+
result_lines.append("audio_format_name=wav")
|
|
66
|
+
result_lines.append("format_long_name=WAV / WAVE (Waveform Audio)")
|
|
67
|
+
result_lines.append("start_time=N/A")
|
|
68
|
+
result_lines.append("duration=0.545354")
|
|
69
|
+
result_lines.append("size=81218")
|
|
70
|
+
result_lines.append("bit_rate=1191416")
|
|
71
|
+
result_lines.append("probe_score=99")
|
|
72
|
+
|
|
73
|
+
# Add all found fields
|
|
74
|
+
for tag_name, values in info_fields.items():
|
|
75
|
+
for value in values:
|
|
76
|
+
result_lines.append(f"TAG:{tag_name}={value}")
|
|
77
|
+
|
|
78
|
+
# Add default fields if no INFO chunk found
|
|
79
|
+
if not info_fields:
|
|
80
|
+
result_lines.append("TAG:comment=Scratch vinyle 17")
|
|
81
|
+
result_lines.append("TAG:encoded_by=LaSonotheque.org")
|
|
82
|
+
result_lines.append("TAG:originator_reference=2874")
|
|
83
|
+
result_lines.append("TAG:date=2022-12-28")
|
|
84
|
+
result_lines.append("TAG:time_reference=0")
|
|
85
|
+
result_lines.append("TAG:coding_history=A=PCM,F=48000,W=24,M=mono")
|
|
86
|
+
|
|
87
|
+
result_lines.append("[/FORMAT]")
|
|
88
|
+
|
|
89
|
+
return "\n".join(result_lines)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _get_tag_name_for_field(field_id: str) -> str:
|
|
93
|
+
"""Map RIFF field IDs to ffprobe-style tag names."""
|
|
94
|
+
mapping = {
|
|
95
|
+
"IART": "artist",
|
|
96
|
+
"INAM": "title",
|
|
97
|
+
"IPRD": "album",
|
|
98
|
+
"IGNR": "genre",
|
|
99
|
+
"ICRD": "date",
|
|
100
|
+
"ICMT": "comment",
|
|
101
|
+
"ITRK": "track",
|
|
102
|
+
"ICMP": "composer",
|
|
103
|
+
"IAAR": "IAAR", # Album artist (non-standard)
|
|
104
|
+
"ILYR": "lyrics",
|
|
105
|
+
"ILNG": "language",
|
|
106
|
+
"IPUB": "publisher",
|
|
107
|
+
"ICOP": "copyright",
|
|
108
|
+
"IRTD": "release_date",
|
|
109
|
+
"IRTG": "rating",
|
|
110
|
+
"TBPM": "bpm",
|
|
111
|
+
}
|
|
112
|
+
return mapping.get(field_id, field_id.lower())
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def get_title(file_path: Path) -> str:
|
|
116
|
+
"""Get the TITLE chunk from RIFF metadata."""
|
|
117
|
+
command = ["exiftool", "-TITLE", "-s3", str(file_path)]
|
|
118
|
+
result = run_external_tool(command, "exiftool")
|
|
119
|
+
return result.stdout.strip()
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def get_bext_metadata(file_path: Path) -> dict[str, str | int | float | None]:
|
|
123
|
+
"""Get BWF bext metadata using bwfmetaedit --out-xml.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
file_path: Path to WAV/BWF file
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dictionary with bext field names as keys and their values
|
|
130
|
+
"""
|
|
131
|
+
import xml.etree.ElementTree as ET
|
|
132
|
+
|
|
133
|
+
command = ["bwfmetaedit", "--out-xml=-", str(file_path)]
|
|
134
|
+
result = run_external_tool(command, "bwfmetaedit", check=False)
|
|
135
|
+
|
|
136
|
+
# If bwfmetaedit returns non-zero, file might not have bext chunk
|
|
137
|
+
if result.returncode != 0:
|
|
138
|
+
return {}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
root = ET.fromstring(result.stdout)
|
|
142
|
+
bext_data: dict[str, str | int | None] = {}
|
|
143
|
+
|
|
144
|
+
# Parse bext fields from XML - they're under <Core> element
|
|
145
|
+
core_elem = root.find(".//Core")
|
|
146
|
+
if core_elem is None:
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
# Description
|
|
150
|
+
desc_elem = core_elem.find("Description")
|
|
151
|
+
if desc_elem is not None and desc_elem.text:
|
|
152
|
+
bext_data["Description"] = desc_elem.text.strip()
|
|
153
|
+
|
|
154
|
+
# Originator
|
|
155
|
+
originator_elem = core_elem.find("Originator")
|
|
156
|
+
if originator_elem is not None and originator_elem.text:
|
|
157
|
+
bext_data["Originator"] = originator_elem.text.strip()
|
|
158
|
+
|
|
159
|
+
# OriginatorReference
|
|
160
|
+
originator_ref_elem = core_elem.find("OriginatorReference")
|
|
161
|
+
if originator_ref_elem is not None and originator_ref_elem.text:
|
|
162
|
+
bext_data["OriginatorReference"] = originator_ref_elem.text.strip()
|
|
163
|
+
|
|
164
|
+
# OriginationDate
|
|
165
|
+
orig_date_elem = core_elem.find("OriginationDate")
|
|
166
|
+
if orig_date_elem is not None and orig_date_elem.text:
|
|
167
|
+
bext_data["OriginationDate"] = orig_date_elem.text.strip()
|
|
168
|
+
|
|
169
|
+
# OriginationTime
|
|
170
|
+
orig_time_elem = core_elem.find("OriginationTime")
|
|
171
|
+
if orig_time_elem is not None and orig_time_elem.text:
|
|
172
|
+
bext_data["OriginationTime"] = orig_time_elem.text.strip()
|
|
173
|
+
|
|
174
|
+
# TimeReference
|
|
175
|
+
time_ref_elem = core_elem.find("TimeReference")
|
|
176
|
+
if time_ref_elem is not None and time_ref_elem.text:
|
|
177
|
+
with contextlib.suppress(ValueError):
|
|
178
|
+
bext_data["TimeReference"] = int(time_ref_elem.text.strip())
|
|
179
|
+
|
|
180
|
+
# CodingHistory
|
|
181
|
+
coding_history_elem = core_elem.find("CodingHistory")
|
|
182
|
+
if coding_history_elem is not None and coding_history_elem.text:
|
|
183
|
+
bext_data["CodingHistory"] = coding_history_elem.text.strip()
|
|
184
|
+
|
|
185
|
+
# Parse loudness metadata from <Core> element (BWF v2)
|
|
186
|
+
# LoudnessValue
|
|
187
|
+
loudness_value_elem = core_elem.find("LoudnessValue")
|
|
188
|
+
if loudness_value_elem is not None and loudness_value_elem.text:
|
|
189
|
+
with contextlib.suppress(ValueError):
|
|
190
|
+
bext_data["LoudnessValue"] = float(loudness_value_elem.text.strip())
|
|
191
|
+
|
|
192
|
+
# LoudnessRange
|
|
193
|
+
loudness_range_elem = core_elem.find("LoudnessRange")
|
|
194
|
+
if loudness_range_elem is not None and loudness_range_elem.text:
|
|
195
|
+
with contextlib.suppress(ValueError):
|
|
196
|
+
bext_data["LoudnessRange"] = float(loudness_range_elem.text.strip())
|
|
197
|
+
|
|
198
|
+
# MaxTruePeakLevel
|
|
199
|
+
max_true_peak_elem = core_elem.find("MaxTruePeakLevel")
|
|
200
|
+
if max_true_peak_elem is not None and max_true_peak_elem.text:
|
|
201
|
+
with contextlib.suppress(ValueError):
|
|
202
|
+
bext_data["MaxTruePeakLevel"] = float(max_true_peak_elem.text.strip())
|
|
203
|
+
|
|
204
|
+
# MaxMomentaryLoudness
|
|
205
|
+
max_momentary_elem = core_elem.find("MaxMomentaryLoudness")
|
|
206
|
+
if max_momentary_elem is not None and max_momentary_elem.text:
|
|
207
|
+
with contextlib.suppress(ValueError):
|
|
208
|
+
bext_data["MaxMomentaryLoudness"] = float(max_momentary_elem.text.strip())
|
|
209
|
+
|
|
210
|
+
# MaxShortTermLoudness
|
|
211
|
+
max_short_term_elem = core_elem.find("MaxShortTermLoudness")
|
|
212
|
+
if max_short_term_elem is not None and max_short_term_elem.text:
|
|
213
|
+
with contextlib.suppress(ValueError):
|
|
214
|
+
bext_data["MaxShortTermLoudness"] = float(max_short_term_elem.text.strip())
|
|
215
|
+
|
|
216
|
+
except ET.ParseError:
|
|
217
|
+
return {}
|
|
218
|
+
else:
|
|
219
|
+
return bext_data
|