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,374 @@
|
|
|
1
|
+
"""RIFF metadata setting operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..common.external_tool_runner import ExternalMetadataToolError, run_external_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RIFFMetadataSetter:
|
|
10
|
+
"""Static utility class for RIFF metadata setting using external tools."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def set_metadata(file_path: Path, metadata: dict[str, Any]) -> None:
|
|
14
|
+
"""Set WAV metadata using bwfmetaedit tool."""
|
|
15
|
+
cmd = ["bwfmetaedit"]
|
|
16
|
+
|
|
17
|
+
# Map common metadata keys to bwfmetaedit arguments
|
|
18
|
+
key_mapping = {
|
|
19
|
+
"title": "--INAM",
|
|
20
|
+
"artist": "--IART",
|
|
21
|
+
"album": "--IPRD",
|
|
22
|
+
"genre": "--IGNR",
|
|
23
|
+
"date": "--ICRD",
|
|
24
|
+
"year": "--ICRD",
|
|
25
|
+
"release_date": "--ICRD",
|
|
26
|
+
"comment": "--ICMT",
|
|
27
|
+
"track": "--ITRK",
|
|
28
|
+
"track_number": "--ITRK",
|
|
29
|
+
"bpm": "--TBPM",
|
|
30
|
+
"composer": "--ICMP",
|
|
31
|
+
"lyrics": "--ILYR",
|
|
32
|
+
"language": "--ILNG",
|
|
33
|
+
"album_artist": "--IAAR",
|
|
34
|
+
"rating": "--IRTD",
|
|
35
|
+
"copyright": "--ICOP",
|
|
36
|
+
"isrc": "--ISRC",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Handle list values - include first value in main command to avoid overwriting
|
|
40
|
+
artist_value = None
|
|
41
|
+
genre_value = None
|
|
42
|
+
composer_value = None
|
|
43
|
+
for key, value in metadata.items():
|
|
44
|
+
if isinstance(value, list) and value:
|
|
45
|
+
if key.lower() == "artist":
|
|
46
|
+
artist_value = value[0] # Store first artist for main command
|
|
47
|
+
elif key.lower() == "genre":
|
|
48
|
+
genre_value = value[0] # Store first genre for main command
|
|
49
|
+
elif key.lower() == "composer":
|
|
50
|
+
composer_value = value[0] # Store first composer for main command
|
|
51
|
+
|
|
52
|
+
# Handle non-list values and include list values in main command
|
|
53
|
+
# Note: Rating, BPM, Language, and Composer need to be set AFTER bwfmetaedit to avoid being overwritten
|
|
54
|
+
rating_value = None
|
|
55
|
+
bpm_value = None
|
|
56
|
+
language_value = None
|
|
57
|
+
composer_single_value = None
|
|
58
|
+
metadata_added = False
|
|
59
|
+
for key, value in metadata.items():
|
|
60
|
+
if key.lower() in key_mapping and not isinstance(value, list):
|
|
61
|
+
if key.lower() == "bpm":
|
|
62
|
+
bpm_value = str(value) # Store for later
|
|
63
|
+
elif key.lower() == "rating":
|
|
64
|
+
rating_value = str(value) # Store for later
|
|
65
|
+
elif key.lower() == "language":
|
|
66
|
+
language_value = str(value) # Store for later
|
|
67
|
+
elif key.lower() == "composer":
|
|
68
|
+
composer_single_value = str(value) # Store for later
|
|
69
|
+
else:
|
|
70
|
+
cmd.extend([f"{key_mapping[key.lower()]}={value}"])
|
|
71
|
+
metadata_added = True
|
|
72
|
+
|
|
73
|
+
# Add artist if it was provided as a list
|
|
74
|
+
if artist_value is not None:
|
|
75
|
+
cmd.extend([f"{key_mapping['artist']}={artist_value}"])
|
|
76
|
+
metadata_added = True
|
|
77
|
+
|
|
78
|
+
# Add genre if it was provided as a list
|
|
79
|
+
if genre_value is not None:
|
|
80
|
+
cmd.extend([f"{key_mapping['genre']}={genre_value}"])
|
|
81
|
+
metadata_added = True
|
|
82
|
+
|
|
83
|
+
# Run bwfmetaedit first if metadata was actually added
|
|
84
|
+
if metadata_added:
|
|
85
|
+
cmd.append(str(file_path))
|
|
86
|
+
run_external_tool(cmd, "bwfmetaedit")
|
|
87
|
+
|
|
88
|
+
# Set BPM, rating, and language AFTER bwfmetaedit to avoid being overwritten
|
|
89
|
+
if bpm_value is not None:
|
|
90
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
91
|
+
|
|
92
|
+
ManualRIFFMetadataCreator.create_bpm_field(file_path, bpm_value)
|
|
93
|
+
|
|
94
|
+
if rating_value is not None:
|
|
95
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
96
|
+
|
|
97
|
+
ManualRIFFMetadataCreator.create_rating_field(file_path, rating_value)
|
|
98
|
+
|
|
99
|
+
if language_value is not None:
|
|
100
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
101
|
+
|
|
102
|
+
ManualRIFFMetadataCreator.create_language_field(file_path, language_value)
|
|
103
|
+
|
|
104
|
+
# Set composer (from list or single value) AFTER bwfmetaedit
|
|
105
|
+
if composer_value is not None:
|
|
106
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
107
|
+
|
|
108
|
+
ManualRIFFMetadataCreator.create_composer_field(file_path, composer_value)
|
|
109
|
+
elif composer_single_value is not None:
|
|
110
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
111
|
+
|
|
112
|
+
ManualRIFFMetadataCreator.create_composer_field(file_path, composer_single_value)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def set_comment(file_path: Path, comment: str) -> None:
|
|
116
|
+
command = ["bwfmetaedit", f"--ICMT={comment}", str(file_path)]
|
|
117
|
+
run_external_tool(command, "bwfmetaedit")
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def set_title(file_path: Path, title: str) -> None:
|
|
121
|
+
command = ["bwfmetaedit", f"--INAM={title}", str(file_path)]
|
|
122
|
+
run_external_tool(command, "bwfmetaedit")
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def set_multiple_titles(file_path: Path, titles: list[str], in_separate_frames: bool = False):
|
|
126
|
+
"""Set multiple titles, optionally in separate INAM frames."""
|
|
127
|
+
if in_separate_frames:
|
|
128
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
129
|
+
|
|
130
|
+
ManualRIFFMetadataCreator.create_multiple_title_fields(file_path, titles)
|
|
131
|
+
# For now, just set the first title
|
|
132
|
+
elif titles:
|
|
133
|
+
command = ["bwfmetaedit", f"--INAM={titles[0]}", str(file_path)]
|
|
134
|
+
run_external_tool(command, "bwfmetaedit")
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def set_artist(file_path: Path, artist: str) -> None:
|
|
138
|
+
command = ["bwfmetaedit", f"--IART={artist}", str(file_path)]
|
|
139
|
+
run_external_tool(command, "bwfmetaedit")
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def set_album(file_path: Path, album: str) -> None:
|
|
143
|
+
command = ["bwfmetaedit", f"--IPRD={album}", str(file_path)]
|
|
144
|
+
run_external_tool(command, "bwfmetaedit")
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def set_genres(file_path: Path, genres: list[str]) -> None:
|
|
148
|
+
command = ["bwfmetaedit", f"--IGNR={','.join(genres)}", str(file_path)]
|
|
149
|
+
run_external_tool(command, "bwfmetaedit")
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def set_genre_text(file_path: Path, genre_text: str) -> None:
|
|
153
|
+
"""Set RIFF genre using external exiftool or bwfmetaedit tool."""
|
|
154
|
+
try:
|
|
155
|
+
# Try exiftool first
|
|
156
|
+
RIFFMetadataSetter.set_riff_genre(file_path, genre_text)
|
|
157
|
+
except ExternalMetadataToolError:
|
|
158
|
+
try:
|
|
159
|
+
# Fallback to bwfmetaedit - split genre_text by semicolon and strip whitespace
|
|
160
|
+
genres = [genre.strip() for genre in genre_text.split(";") if genre.strip()]
|
|
161
|
+
RIFFMetadataSetter.set_genres(file_path, genres)
|
|
162
|
+
except ExternalMetadataToolError as e:
|
|
163
|
+
msg = f"Failed to set RIFF genre: {e}"
|
|
164
|
+
raise RuntimeError(msg) from e
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def set_lyrics(file_path: Path, lyrics: str) -> None:
|
|
168
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
169
|
+
|
|
170
|
+
ManualRIFFMetadataCreator.create_lyrics_field(file_path, lyrics)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def set_language(file_path: Path, language: str) -> None:
|
|
174
|
+
import tempfile
|
|
175
|
+
|
|
176
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
|
|
177
|
+
tmp_path = Path(tmp_file.name)
|
|
178
|
+
try:
|
|
179
|
+
command = [
|
|
180
|
+
"ffmpeg",
|
|
181
|
+
"-i",
|
|
182
|
+
str(file_path),
|
|
183
|
+
"-c",
|
|
184
|
+
"copy",
|
|
185
|
+
"-metadata",
|
|
186
|
+
f"language={language}",
|
|
187
|
+
"-y",
|
|
188
|
+
str(tmp_path),
|
|
189
|
+
]
|
|
190
|
+
run_external_tool(command, "ffmpeg")
|
|
191
|
+
tmp_path.replace(file_path)
|
|
192
|
+
finally:
|
|
193
|
+
if tmp_path.exists():
|
|
194
|
+
tmp_path.unlink()
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def set_max_metadata(file_path: Path) -> None:
|
|
198
|
+
from pathlib import Path
|
|
199
|
+
|
|
200
|
+
from ..common.external_tool_runner import run_script
|
|
201
|
+
|
|
202
|
+
scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
|
|
203
|
+
run_script("set-riff-max-metadata.sh", file_path, scripts_dir)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def set_riff_genre(file_path: Path, genre: str) -> None:
|
|
207
|
+
command = ["exiftool", "-overwrite_original", f"-Genre={genre}", str(file_path)]
|
|
208
|
+
run_external_tool(command, "exiftool")
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def set_artists(file_path: Path, artists: list[str], in_separate_frames: bool = False):
|
|
212
|
+
"""Set multiple artists, optionally in separate IART frames."""
|
|
213
|
+
if in_separate_frames:
|
|
214
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
215
|
+
|
|
216
|
+
ManualRIFFMetadataCreator.create_multiple_artist_fields(file_path, artists)
|
|
217
|
+
# For testing multiple instances, we'd need to use a more sophisticated approach
|
|
218
|
+
# For now, just set the first artist
|
|
219
|
+
elif artists:
|
|
220
|
+
command = ["bwfmetaedit", f"--IART={artists[0]}", str(file_path)]
|
|
221
|
+
run_external_tool(command, "bwfmetaedit")
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def set_multiple_genres(file_path: Path, genres: list[str], in_separate_frames: bool = False):
|
|
225
|
+
"""Set multiple genres, optionally in separate IGNR frames."""
|
|
226
|
+
if in_separate_frames:
|
|
227
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
228
|
+
|
|
229
|
+
ManualRIFFMetadataCreator.create_multiple_genre_fields(file_path, genres)
|
|
230
|
+
# For now, just set the first genre
|
|
231
|
+
elif genres:
|
|
232
|
+
command = ["bwfmetaedit", f"--IGNR={genres[0]}", str(file_path)]
|
|
233
|
+
run_external_tool(command, "bwfmetaedit")
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def set_multiple_composers(file_path: Path, composers: list[str], in_separate_frames: bool = False):
|
|
237
|
+
"""Set multiple composers, optionally in separate ICMP frames."""
|
|
238
|
+
if in_separate_frames:
|
|
239
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
240
|
+
|
|
241
|
+
ManualRIFFMetadataCreator.create_multiple_composer_fields(file_path, composers)
|
|
242
|
+
# For now, just set the first composer
|
|
243
|
+
elif composers:
|
|
244
|
+
command = ["bwfmetaedit", f"--ICMP={composers[0]}", str(file_path)]
|
|
245
|
+
run_external_tool(command, "bwfmetaedit")
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def set_multiple_album_artists(file_path: Path, album_artists: list[str], _in_separate_frames: bool = False):
|
|
249
|
+
"""Set multiple album artists, optionally in separate IAAR frames."""
|
|
250
|
+
# IAAR is not a standard RIFF INFO chunk field, so external tools don't support it.
|
|
251
|
+
# Use the manual metadata creator which can create non-standard RIFF fields.
|
|
252
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
253
|
+
|
|
254
|
+
ManualRIFFMetadataCreator.create_multiple_album_artist_fields(file_path, album_artists)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def set_release_date(file_path: Path, release_date: str) -> None:
|
|
258
|
+
command = ["bwfmetaedit", f"--ICRD={release_date}", str(file_path)]
|
|
259
|
+
run_external_tool(command, "bwfmetaedit")
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def set_bext_description(file_path: Path, description: str) -> None:
|
|
263
|
+
"""Set BWF bext Description field."""
|
|
264
|
+
command = ["bwfmetaedit", f"--Description={description}", str(file_path)]
|
|
265
|
+
run_external_tool(command, "bwfmetaedit")
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def set_bext_originator(file_path: Path, originator: str) -> None:
|
|
269
|
+
"""Set BWF bext Originator field."""
|
|
270
|
+
command = ["bwfmetaedit", f"--Originator={originator}", str(file_path)]
|
|
271
|
+
run_external_tool(command, "bwfmetaedit")
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def set_bext_originator_reference(file_path: Path, originator_reference: str) -> None:
|
|
275
|
+
"""Set BWF bext OriginatorReference field."""
|
|
276
|
+
command = ["bwfmetaedit", f"--OriginatorReference={originator_reference}", str(file_path)]
|
|
277
|
+
run_external_tool(command, "bwfmetaedit")
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def set_bext_origination_date(file_path: Path, origination_date: str) -> None:
|
|
281
|
+
"""Set BWF bext OriginationDate field (YYYY-MM-DD format)."""
|
|
282
|
+
command = ["bwfmetaedit", f"--OriginationDate={origination_date}", str(file_path)]
|
|
283
|
+
run_external_tool(command, "bwfmetaedit")
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def set_bext_origination_time(file_path: Path, origination_time: str) -> None:
|
|
287
|
+
"""Set BWF bext OriginationTime field (HH:MM:SS format)."""
|
|
288
|
+
command = ["bwfmetaedit", f"--OriginationTime={origination_time}", str(file_path)]
|
|
289
|
+
run_external_tool(command, "bwfmetaedit")
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def set_bext_time_reference(file_path: Path, time_reference: int) -> None:
|
|
293
|
+
"""Set BWF bext TimeReference field."""
|
|
294
|
+
command = ["bwfmetaedit", f"--Timereference={time_reference}", str(file_path)]
|
|
295
|
+
run_external_tool(command, "bwfmetaedit")
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def set_bext_coding_history(file_path: Path, coding_history: str) -> None:
|
|
299
|
+
"""Set BWF bext CodingHistory field."""
|
|
300
|
+
command = ["bwfmetaedit", f"--History={coding_history}", str(file_path)]
|
|
301
|
+
run_external_tool(command, "bwfmetaedit")
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def set_bext_metadata(file_path: Path, bext_metadata: dict[str, Any]) -> None:
|
|
305
|
+
"""Set multiple BWF bext metadata fields at once.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
file_path: Path to WAV file
|
|
309
|
+
bext_metadata: Dictionary with bext field names as keys:
|
|
310
|
+
- Description: str
|
|
311
|
+
- Originator: str
|
|
312
|
+
- OriginatorReference: str
|
|
313
|
+
- OriginationDate: str (YYYY-MM-DD format)
|
|
314
|
+
- OriginationTime: str (HH:MM:SS format)
|
|
315
|
+
- TimeReference: int (uint64)
|
|
316
|
+
- UMID: str (hex string)
|
|
317
|
+
- CodingHistory: str
|
|
318
|
+
- LoudnessValue: float (LU, converted to 0.1 LU units)
|
|
319
|
+
- LoudnessRange: float (LU, converted to 0.1 LU units)
|
|
320
|
+
- MaxTruePeakLevel: float (dB, converted to 0.1 dB units)
|
|
321
|
+
- MaxMomentaryLoudness: float (LU, converted to 0.1 LU units)
|
|
322
|
+
- MaxShortTermLoudness: float (LU, converted to 0.1 LU units)
|
|
323
|
+
"""
|
|
324
|
+
# Map bext field names to bwfmetaedit arguments
|
|
325
|
+
field_mapping = {
|
|
326
|
+
"Description": "--Description",
|
|
327
|
+
"Originator": "--Originator",
|
|
328
|
+
"OriginatorReference": "--OriginatorReference",
|
|
329
|
+
"OriginationDate": "--OriginationDate",
|
|
330
|
+
"OriginationTime": "--OriginationTime",
|
|
331
|
+
"TimeReference": "--Timereference",
|
|
332
|
+
"UMID": "--UMID",
|
|
333
|
+
"CodingHistory": "--History",
|
|
334
|
+
"LoudnessValue": "--LoudnessValue",
|
|
335
|
+
"LoudnessRange": "--LoudnessRange",
|
|
336
|
+
"MaxTruePeakLevel": "--MaxTruePeakLevel",
|
|
337
|
+
"MaxMomentaryLoudness": "--MaxMomentaryLoudness",
|
|
338
|
+
"MaxShortTermLoudness": "--MaxShortTermLoudness",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Separate loudness fields from other fields
|
|
342
|
+
# Loudness fields need to be set after other fields to ensure BWF v2 is created
|
|
343
|
+
loudness_fields = [
|
|
344
|
+
"LoudnessValue",
|
|
345
|
+
"LoudnessRange",
|
|
346
|
+
"MaxTruePeakLevel",
|
|
347
|
+
"MaxMomentaryLoudness",
|
|
348
|
+
"MaxShortTermLoudness",
|
|
349
|
+
]
|
|
350
|
+
regular_fields = {}
|
|
351
|
+
loudness_metadata = {}
|
|
352
|
+
|
|
353
|
+
for field_name, value in bext_metadata.items():
|
|
354
|
+
if field_name in field_mapping and value is not None:
|
|
355
|
+
if field_name in loudness_fields:
|
|
356
|
+
loudness_metadata[field_name] = value
|
|
357
|
+
else:
|
|
358
|
+
regular_fields[field_name] = value
|
|
359
|
+
|
|
360
|
+
# Set regular fields first
|
|
361
|
+
if regular_fields:
|
|
362
|
+
cmd_regular = ["bwfmetaedit"]
|
|
363
|
+
for field_name, value in regular_fields.items():
|
|
364
|
+
cmd_regular.extend([f"{field_mapping[field_name]}={value}"])
|
|
365
|
+
cmd_regular.append(str(file_path))
|
|
366
|
+
run_external_tool(cmd_regular, "bwfmetaedit")
|
|
367
|
+
|
|
368
|
+
# Set loudness fields in separate command (ensures BWF v2 is properly created)
|
|
369
|
+
if loudness_metadata:
|
|
370
|
+
cmd_loudness = ["bwfmetaedit"]
|
|
371
|
+
for field_name, value in loudness_metadata.items():
|
|
372
|
+
cmd_loudness.extend([f"{field_mapping[field_name]}={value}"])
|
|
373
|
+
cmd_loudness.append(str(file_path))
|
|
374
|
+
run_external_tool(cmd_loudness, "bwfmetaedit")
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TechnicalInfoInspector:
|
|
6
|
+
"""Helper class for inspecting technical audio file information using mediainfo."""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def _run_mediainfo(file_path: str | Path, output_format: str = "JSON") -> dict:
|
|
10
|
+
"""Run mediainfo on a file and return parsed output."""
|
|
11
|
+
cmd = ["mediainfo", f"--Output={output_format}", str(file_path)]
|
|
12
|
+
try:
|
|
13
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
14
|
+
if output_format == "JSON":
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
return json.loads(result.stdout)
|
|
18
|
+
except subprocess.CalledProcessError as e:
|
|
19
|
+
msg = f"Failed to run mediainfo on {file_path}: {e}"
|
|
20
|
+
raise RuntimeError(msg) from e
|
|
21
|
+
except json.JSONDecodeError as e:
|
|
22
|
+
msg = f"Failed to parse mediainfo output: {e}"
|
|
23
|
+
raise RuntimeError(msg) from e
|
|
24
|
+
else:
|
|
25
|
+
return {"text": result.stdout}
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def get_bitrate(file_path: str | Path) -> int | None:
|
|
29
|
+
"""Get the bitrate of an audio file in kb/s using mediainfo."""
|
|
30
|
+
try:
|
|
31
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
32
|
+
tracks = data.get("media", {}).get("track", [])
|
|
33
|
+
for track in tracks:
|
|
34
|
+
if track.get("@type") == "Audio":
|
|
35
|
+
bitrate_str = track.get("BitRate")
|
|
36
|
+
if bitrate_str:
|
|
37
|
+
# Handle formats like "128 kb/s" or "128000"
|
|
38
|
+
if "kb/s" in str(bitrate_str):
|
|
39
|
+
return int(str(bitrate_str).split()[0])
|
|
40
|
+
if str(bitrate_str).isdigit():
|
|
41
|
+
return int(bitrate_str) // 1000
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
else:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def get_duration(file_path: str | Path) -> float | None:
|
|
49
|
+
"""Get the duration of an audio file in seconds using mediainfo."""
|
|
50
|
+
try:
|
|
51
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
52
|
+
tracks = data.get("media", {}).get("track", [])
|
|
53
|
+
for track in tracks:
|
|
54
|
+
if track.get("@type") == "Audio":
|
|
55
|
+
duration_str = track.get("Duration")
|
|
56
|
+
if duration_str:
|
|
57
|
+
# Handle formats like "1.025 s" or just numbers
|
|
58
|
+
if "s" in duration_str:
|
|
59
|
+
return float(duration_str.split()[0])
|
|
60
|
+
return float(duration_str)
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
else:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def get_sample_rate(file_path: str | Path) -> int | None:
|
|
68
|
+
"""Get the sample rate of an audio file in Hz using mediainfo."""
|
|
69
|
+
try:
|
|
70
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
71
|
+
tracks = data.get("media", {}).get("track", [])
|
|
72
|
+
for track in tracks:
|
|
73
|
+
if track.get("@type") == "Audio":
|
|
74
|
+
sample_rate_str = track.get("SamplingRate")
|
|
75
|
+
if sample_rate_str:
|
|
76
|
+
# Handle formats like "44100 Hz"
|
|
77
|
+
if "Hz" in sample_rate_str:
|
|
78
|
+
return int(sample_rate_str.split()[0])
|
|
79
|
+
return int(sample_rate_str)
|
|
80
|
+
except Exception:
|
|
81
|
+
return None
|
|
82
|
+
else:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def get_channels(file_path: str | Path) -> int | None:
|
|
87
|
+
"""Get the number of channels of an audio file using mediainfo."""
|
|
88
|
+
try:
|
|
89
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
90
|
+
tracks = data.get("media", {}).get("track", [])
|
|
91
|
+
for track in tracks:
|
|
92
|
+
if track.get("@type") == "Audio":
|
|
93
|
+
channels_str = track.get("Channels")
|
|
94
|
+
if channels_str:
|
|
95
|
+
return int(channels_str)
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
else:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def get_file_size(file_path: str | Path) -> int | None:
|
|
103
|
+
"""Get the file size of an audio file in bytes using mediainfo."""
|
|
104
|
+
try:
|
|
105
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
106
|
+
tracks = data.get("media", {}).get("track", [])
|
|
107
|
+
for track in tracks:
|
|
108
|
+
if track.get("@type") == "General":
|
|
109
|
+
file_size_str = track.get("FileSize")
|
|
110
|
+
if file_size_str:
|
|
111
|
+
return int(file_size_str)
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
114
|
+
else:
|
|
115
|
+
return None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Consolidated temporary file with metadata utilities for testing.
|
|
2
|
+
|
|
3
|
+
This module provides a context manager for test files with metadata using contextlib.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .common import AudioFileCreator
|
|
12
|
+
from .id3v1 import ID3v1MetadataSetter
|
|
13
|
+
from .id3v2 import ID3v2MetadataSetter
|
|
14
|
+
from .riff import RIFFMetadataSetter
|
|
15
|
+
from .vorbis import VorbisMetadataSetter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def temp_file_with_metadata(metadata: dict, format_type: str) -> Generator[Path, None, None]:
|
|
20
|
+
"""Context manager for creating temporary test files with metadata.
|
|
21
|
+
|
|
22
|
+
This function creates a temporary audio file with the specified metadata,
|
|
23
|
+
yields its path for use in tests, and automatically cleans up the file.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
metadata: Dictionary of metadata to set on the test file
|
|
27
|
+
format_type: Audio format ('mp3', 'id3v1', 'id3v2.3', 'id3v2.4', 'flac', 'wav')
|
|
28
|
+
|
|
29
|
+
Yields:
|
|
30
|
+
Path to the created test file with metadata
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
with temp_file_with_metadata({"title": "Test Song"}, "mp3") as test_file:
|
|
34
|
+
metadata = get_unified_metadata(test_file)
|
|
35
|
+
"""
|
|
36
|
+
target_file = _create_test_file_with_metadata(metadata, format_type)
|
|
37
|
+
try:
|
|
38
|
+
yield target_file
|
|
39
|
+
finally:
|
|
40
|
+
if target_file.exists():
|
|
41
|
+
target_file.unlink()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _create_test_file_with_metadata(metadata: dict, format_type: str) -> Path:
|
|
45
|
+
"""Create a test file with specific metadata values.
|
|
46
|
+
|
|
47
|
+
This function uses external tools to set specific metadata values
|
|
48
|
+
without using the app's update functions, improving test isolation.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
metadata: Dictionary of metadata to set
|
|
52
|
+
format_type: Audio format ('mp3', 'id3v1', 'flac', 'wav')
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to the created file with metadata
|
|
56
|
+
"""
|
|
57
|
+
# Create temporary file with correct extension
|
|
58
|
+
# For id3v1, id3v2.3, id3v2.4, use .mp3 extension since they're still MP3 files
|
|
59
|
+
actual_extension = "mp3" if format_type.lower() in ["id3v1", "id3v2.3", "id3v2.4"] else format_type.lower()
|
|
60
|
+
with tempfile.NamedTemporaryFile(suffix=f".{actual_extension}", delete=False) as tmp_file:
|
|
61
|
+
target_file = Path(tmp_file.name)
|
|
62
|
+
|
|
63
|
+
assets_dir = Path(__file__).parent.parent.parent / "test" / "assets"
|
|
64
|
+
AudioFileCreator.create_minimal_audio_file(target_file, format_type, assets_dir)
|
|
65
|
+
|
|
66
|
+
if format_type.lower() == "mp3":
|
|
67
|
+
ID3v2MetadataSetter.set_metadata(target_file, metadata)
|
|
68
|
+
elif format_type.lower() == "id3v1":
|
|
69
|
+
ID3v1MetadataSetter.set_metadata(target_file, metadata)
|
|
70
|
+
elif format_type.lower() in ["id3v2.3", "id3v2.4"]:
|
|
71
|
+
# Use version-specific ID3v2 metadata setting
|
|
72
|
+
version = format_type.lower().replace("id3v2.", "2.")
|
|
73
|
+
ID3v2MetadataSetter.set_metadata(target_file, metadata, version)
|
|
74
|
+
elif format_type.lower() == "flac":
|
|
75
|
+
VorbisMetadataSetter.set_metadata(target_file, metadata)
|
|
76
|
+
elif format_type.lower() == "wav":
|
|
77
|
+
RIFFMetadataSetter.set_metadata(target_file, metadata)
|
|
78
|
+
else:
|
|
79
|
+
msg = f"Unsupported format type: {format_type}"
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
|
|
82
|
+
return target_file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Vorbis metadata format helpers."""
|
|
2
|
+
|
|
3
|
+
from .vorbis_header_verifier import VorbisHeaderVerifier
|
|
4
|
+
from .vorbis_metadata_deleter import VorbisMetadataDeleter
|
|
5
|
+
from .vorbis_metadata_getter import VorbisMetadataGetter
|
|
6
|
+
from .vorbis_metadata_setter import VorbisMetadataSetter
|
|
7
|
+
|
|
8
|
+
__all__ = ["VorbisMetadataGetter", "VorbisHeaderVerifier", "VorbisMetadataDeleter", "VorbisMetadataSetter"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Vorbis metadata header verification and information utilities."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from audiometa.utils.tool_path_resolver import get_tool_path
|
|
7
|
+
|
|
8
|
+
from ..common.external_tool_runner import run_external_tool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VorbisHeaderVerifier:
|
|
12
|
+
"""Utilities for verifying Vorbis metadata headers and retrieving metadata information from audio files."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def has_vorbis_comments(file_path: Path) -> bool:
|
|
16
|
+
"""Check if file has Vorbis comments using metaflac."""
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
[get_tool_path("metaflac"), "--list", str(file_path)], capture_output=True, text=True, check=True
|
|
20
|
+
)
|
|
21
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
22
|
+
return False
|
|
23
|
+
else:
|
|
24
|
+
return "VORBIS_COMMENT" in result.stdout
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def get_metadata_info(file_path: Path) -> str:
|
|
28
|
+
"""Get metadata info using metaflac --list command."""
|
|
29
|
+
command = [get_tool_path("metaflac"), "--list", str(file_path)]
|
|
30
|
+
result = run_external_tool(command, "metaflac")
|
|
31
|
+
return result.stdout
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Vorbis metadata deletion operations."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..common.external_tool_runner import run_external_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VorbisMetadataDeleter:
|
|
10
|
+
"""Static utility class for Vorbis metadata deletion using external metaflac tool."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def delete_tag(file_path: Path, tag_name: str) -> None:
|
|
14
|
+
"""Delete a specific Vorbis comment tag using metaflac tool."""
|
|
15
|
+
command = ["metaflac", "--remove-tag", tag_name, str(file_path)]
|
|
16
|
+
with contextlib.suppress(Exception):
|
|
17
|
+
run_external_tool(command, "metaflac")
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def delete_comment(file_path: Path) -> None:
|
|
21
|
+
VorbisMetadataDeleter.delete_tag(file_path, "COMMENT")
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def delete_title(file_path: Path) -> None:
|
|
25
|
+
VorbisMetadataDeleter.delete_tag(file_path, "TITLE")
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def delete_artist(file_path: Path) -> None:
|
|
29
|
+
VorbisMetadataDeleter.delete_tag(file_path, "ARTIST")
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def delete_album(file_path: Path) -> None:
|
|
33
|
+
VorbisMetadataDeleter.delete_tag(file_path, "ALBUM")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def delete_genre(file_path: Path) -> None:
|
|
37
|
+
VorbisMetadataDeleter.delete_tag(file_path, "GENRE")
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def delete_lyrics(file_path: Path) -> None:
|
|
41
|
+
VorbisMetadataDeleter.delete_tag(file_path, "UNSYNCHRONIZED_LYRICS")
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def delete_language(file_path: Path) -> None:
|
|
45
|
+
VorbisMetadataDeleter.delete_tag(file_path, "LANGUAGE")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def delete_bpm(file_path: Path) -> None:
|
|
49
|
+
VorbisMetadataDeleter.delete_tag(file_path, "BPM")
|