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,506 @@
|
|
|
1
|
+
"""ID3v2 and ID3v1 metadata setting operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..common.external_tool_runner import run_external_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ID3v2MetadataSetter:
|
|
10
|
+
"""Static utility class for ID3v2 metadata setting using external tools."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def set_metadata(file_path: Path, metadata: dict[str, Any], version: str = "2.4") -> None:
|
|
14
|
+
"""Set MP3 metadata using appropriate ID3v2 tool based on version.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
file_path: Path to the MP3 file
|
|
18
|
+
metadata: Dictionary of metadata to set
|
|
19
|
+
version: Optional ID3v2 version ("2.3" or "2.4"). Defaults to "2.4" if not specified.
|
|
20
|
+
"""
|
|
21
|
+
if version == "2.3":
|
|
22
|
+
tool = "id3v2"
|
|
23
|
+
cmd = ["id3v2", "--id3v2-only"]
|
|
24
|
+
# Map common metadata keys to id3v2 tool arguments
|
|
25
|
+
key_mapping = {
|
|
26
|
+
"title": "--song",
|
|
27
|
+
"artist": "--artist",
|
|
28
|
+
"album": "--album",
|
|
29
|
+
"year": "--year",
|
|
30
|
+
"comment": "--comment",
|
|
31
|
+
"track": "--track",
|
|
32
|
+
"track_number": "--TRCK",
|
|
33
|
+
"bpm": "--TBPM",
|
|
34
|
+
"composer": "--TCOM",
|
|
35
|
+
"copyright": "--TCOP",
|
|
36
|
+
"lyrics": "--USLT",
|
|
37
|
+
"language": "--TLAN",
|
|
38
|
+
"rating": "--POPM",
|
|
39
|
+
"album_artist": "--TPE2",
|
|
40
|
+
"encoder": "--TENC",
|
|
41
|
+
"url": "--WOAR",
|
|
42
|
+
"isrc": "--TSRC",
|
|
43
|
+
"mood": "--TMOO",
|
|
44
|
+
"key": "--TKEY",
|
|
45
|
+
"publisher": "--TPUB",
|
|
46
|
+
"disc_number": "--TPOS",
|
|
47
|
+
}
|
|
48
|
+
else:
|
|
49
|
+
tool = "mid3v2"
|
|
50
|
+
cmd = ["mid3v2"]
|
|
51
|
+
# Map common metadata keys to mid3v2 tool arguments
|
|
52
|
+
key_mapping = {
|
|
53
|
+
"title": "--song",
|
|
54
|
+
"artist": "--artist",
|
|
55
|
+
"album": "--album",
|
|
56
|
+
"year": "--year",
|
|
57
|
+
"genre": "--genre",
|
|
58
|
+
"comment": "--comment",
|
|
59
|
+
"track": "--track",
|
|
60
|
+
"track_number": "--track",
|
|
61
|
+
"composer": "--TCOM",
|
|
62
|
+
"copyright": "--TCOP",
|
|
63
|
+
"lyrics": "--USLT",
|
|
64
|
+
"language": "--TLAN",
|
|
65
|
+
"rating": "--POPM",
|
|
66
|
+
"album_artist": "--TPE2",
|
|
67
|
+
"encoder": "--TENC",
|
|
68
|
+
"url": "--WOAR",
|
|
69
|
+
"isrc": "--TSRC",
|
|
70
|
+
"mood": "--TMOO",
|
|
71
|
+
"key": "--TKEY",
|
|
72
|
+
"publisher": "--TPUB",
|
|
73
|
+
"bpm": "--TBPM",
|
|
74
|
+
"disc_number": "--TPOS",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Initialize metadata_added flag
|
|
78
|
+
metadata_added = False
|
|
79
|
+
|
|
80
|
+
# Handle special fields that need combined values
|
|
81
|
+
disc_number = None
|
|
82
|
+
disc_total = None
|
|
83
|
+
for key, value in metadata.items():
|
|
84
|
+
if key.lower() == "disc_number" and not isinstance(value, list):
|
|
85
|
+
disc_number = value
|
|
86
|
+
elif key.lower() == "disc_total" and not isinstance(value, list):
|
|
87
|
+
disc_total = value
|
|
88
|
+
|
|
89
|
+
# Handle disc_number/disc_total combination (TPOS frame)
|
|
90
|
+
if disc_number is not None or disc_total is not None:
|
|
91
|
+
if disc_number is not None and disc_total is not None:
|
|
92
|
+
tpos_value = f"{disc_number}/{disc_total}"
|
|
93
|
+
elif disc_number is not None:
|
|
94
|
+
tpos_value = str(disc_number)
|
|
95
|
+
else:
|
|
96
|
+
tpos_value = f"0/{disc_total}"
|
|
97
|
+
if version == "2.3":
|
|
98
|
+
cmd.extend(["--TPOS", tpos_value])
|
|
99
|
+
else:
|
|
100
|
+
cmd.extend(["--TPOS", tpos_value])
|
|
101
|
+
metadata_added = True
|
|
102
|
+
|
|
103
|
+
# Handle release_date (year) - include in main cmd instead of separate call
|
|
104
|
+
release_date = None
|
|
105
|
+
for key, value in metadata.items():
|
|
106
|
+
if key.lower() in ["release_date", "year"] and not isinstance(value, list):
|
|
107
|
+
release_date = str(value)
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if release_date:
|
|
111
|
+
if version == "2.3":
|
|
112
|
+
# For ID3v2.3, use --year (TYER frame)
|
|
113
|
+
cmd.extend(["--year", release_date])
|
|
114
|
+
else:
|
|
115
|
+
# For ID3v2.4, use --TDRC
|
|
116
|
+
cmd.extend(["--TDRC", release_date])
|
|
117
|
+
metadata_added = True
|
|
118
|
+
|
|
119
|
+
# Handle non-list values (excluding already handled fields and list fields)
|
|
120
|
+
# Only exclude list fields if they're actually lists (single strings should be processed)
|
|
121
|
+
list_field_keys = set()
|
|
122
|
+
for k, v in metadata.items():
|
|
123
|
+
if isinstance(v, list):
|
|
124
|
+
list_field_keys.add(k.lower())
|
|
125
|
+
|
|
126
|
+
for key, value in metadata.items():
|
|
127
|
+
if (
|
|
128
|
+
key.lower() in key_mapping
|
|
129
|
+
and not isinstance(value, list)
|
|
130
|
+
and key.lower() not in ["disc_number", "disc_total", "release_date", "year"]
|
|
131
|
+
and key.lower() not in list_field_keys # Only exclude if it's actually a list
|
|
132
|
+
):
|
|
133
|
+
# Special handling for rating (POPM frame requires app name prefix)
|
|
134
|
+
if key.lower() == "rating":
|
|
135
|
+
if version == "2.3":
|
|
136
|
+
cmd.extend(["--POPM", f"Windows Media Player 9 Series:{value}"])
|
|
137
|
+
else:
|
|
138
|
+
cmd.extend(["--POPM", f"Windows Media Player 9 Series:{value}"])
|
|
139
|
+
else:
|
|
140
|
+
cmd.extend([key_mapping[key.lower()], str(value)])
|
|
141
|
+
metadata_added = True
|
|
142
|
+
|
|
143
|
+
# Only run the tool if metadata was actually added
|
|
144
|
+
if metadata_added:
|
|
145
|
+
cmd.append(str(file_path))
|
|
146
|
+
run_external_tool(cmd, tool)
|
|
147
|
+
|
|
148
|
+
# Handle list values AFTER other metadata (to avoid being overwritten)
|
|
149
|
+
# Process in reverse order so the last one (composer) doesn't overwrite others
|
|
150
|
+
list_items = [(k, v) for k, v in metadata.items() if isinstance(v, list) and v]
|
|
151
|
+
# Reverse the list so we process composer last (it seems to work)
|
|
152
|
+
for key, value in reversed(list_items):
|
|
153
|
+
if key.lower() == "artist":
|
|
154
|
+
ID3v2MetadataSetter.set_artists(file_path, value, version=version)
|
|
155
|
+
elif key.lower() == "genre":
|
|
156
|
+
ID3v2MetadataSetter.set_genres(file_path, value, version=version)
|
|
157
|
+
elif key.lower() == "composer":
|
|
158
|
+
ID3v2MetadataSetter.set_composers(file_path, value, version=version)
|
|
159
|
+
elif key.lower() == "album_artist":
|
|
160
|
+
ID3v2MetadataSetter.set_album_artists(file_path, value)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def set_max_metadata(file_path: Path) -> None:
|
|
164
|
+
from pathlib import Path
|
|
165
|
+
|
|
166
|
+
from ..common.external_tool_runner import run_script
|
|
167
|
+
|
|
168
|
+
scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
|
|
169
|
+
run_script("set-id3v2-max-metadata.sh", file_path, scripts_dir)
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def set_title(file_path: Path, title: str, version: str = "2.4") -> None:
|
|
173
|
+
if version == "2.3":
|
|
174
|
+
command = ["id3v2", "--id3v2-only", "--song", title, str(file_path)]
|
|
175
|
+
run_external_tool(command, "id3v2")
|
|
176
|
+
else:
|
|
177
|
+
command = ["mid3v2", "--song", title, str(file_path)]
|
|
178
|
+
run_external_tool(command, "mid3v2")
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def set_titles(file_path: Path, titles: list[str], in_separate_frames: bool = False, version: str = "2.4"):
|
|
182
|
+
"""Set ID3v2 multiple titles using external mid3v2 tool or manual frame creation.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
file_path: Path to the audio file
|
|
186
|
+
titles: List of title strings to set
|
|
187
|
+
in_separate_frames: If True, creates multiple separate TIT2 frames (one per title)
|
|
188
|
+
using manual binary construction. If False (default), creates a single TIT2 frame
|
|
189
|
+
with multiple values using mid3v2.
|
|
190
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
191
|
+
"""
|
|
192
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TIT2", titles, in_separate_frames, version)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def set_artists(file_path: Path, artists, in_separate_frames: bool = False, version: str = "2.4") -> None:
|
|
196
|
+
if isinstance(artists, str):
|
|
197
|
+
# For string input, use external tool directly to avoid replacing entire tag
|
|
198
|
+
if version == "2.3":
|
|
199
|
+
command = ["id3v2", "--id3v2-only", "--artist", artists, str(file_path)]
|
|
200
|
+
run_external_tool(command, "id3v2")
|
|
201
|
+
else:
|
|
202
|
+
# Use mid3v2 for ID3v2.4
|
|
203
|
+
command = ["mid3v2", "--artist", artists, str(file_path)]
|
|
204
|
+
run_external_tool(command, "mid3v2")
|
|
205
|
+
else:
|
|
206
|
+
# For list input, use multiple values handling
|
|
207
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(
|
|
208
|
+
file_path, "TPE1", artists, in_separate_frames=in_separate_frames, version=version
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def set_album(file_path: Path, album: str, version: str = "2.4") -> None:
|
|
213
|
+
if version == "2.3":
|
|
214
|
+
command = ["id3v2", "--id3v2-only", "--album", album, str(file_path)]
|
|
215
|
+
run_external_tool(command, "id3v2")
|
|
216
|
+
else:
|
|
217
|
+
command = ["mid3v2", "--album", album, str(file_path)]
|
|
218
|
+
run_external_tool(command, "mid3v2")
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def set_genre(file_path: Path, genre: str, version: str = "2.4") -> None:
|
|
222
|
+
if version == "2.3":
|
|
223
|
+
command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
|
|
224
|
+
run_external_tool(command, "id3v2")
|
|
225
|
+
else:
|
|
226
|
+
command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
|
|
227
|
+
run_external_tool(command, "id3v2")
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def set_lyrics(file_path: Path, lyrics: str, version: str = "2.4") -> None:
|
|
231
|
+
if version == "2.3":
|
|
232
|
+
command = ["id3v2", "--id3v2-only", "--USLT", lyrics, str(file_path)]
|
|
233
|
+
run_external_tool(command, "id3v2")
|
|
234
|
+
else:
|
|
235
|
+
command = ["mid3v2", "--USLT", lyrics, str(file_path)]
|
|
236
|
+
run_external_tool(command, "mid3v2")
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def set_language(file_path: Path, language: str, version: str = "2.4") -> None:
|
|
240
|
+
if version == "2.3":
|
|
241
|
+
command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
|
|
242
|
+
run_external_tool(command, "id3v2")
|
|
243
|
+
else:
|
|
244
|
+
command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
|
|
245
|
+
run_external_tool(command, "id3v2")
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def set_bpm(file_path: Path, bpm: int, version: str = "2.4") -> None:
|
|
249
|
+
if version == "2.3":
|
|
250
|
+
command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
|
|
251
|
+
run_external_tool(command, "id3v2")
|
|
252
|
+
else:
|
|
253
|
+
command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
|
|
254
|
+
run_external_tool(command, "id3v2")
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def set_release_date(file_path: Path, date_str: str, version: str = "2.4") -> None:
|
|
258
|
+
"""Set ID3v2 release date using version-specific frames.
|
|
259
|
+
|
|
260
|
+
For ID3v2.3: Uses TYER (year) and TDAT (MMDD) frames
|
|
261
|
+
For ID3v2.4: Uses TDRC frame with full date
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
file_path: Path to the audio file
|
|
265
|
+
date_str: Date string in YYYY-MM-DD format
|
|
266
|
+
version: ID3v2 version to use ("2.3" or "2.4")
|
|
267
|
+
"""
|
|
268
|
+
if version == "2.3":
|
|
269
|
+
# Parse YYYY-MM-DD format
|
|
270
|
+
if len(date_str) >= 10:
|
|
271
|
+
year = date_str[:4]
|
|
272
|
+
month = date_str[5:7]
|
|
273
|
+
day = date_str[8:10]
|
|
274
|
+
# TDAT format is DDMM
|
|
275
|
+
date_ddmm = f"{day}{month}"
|
|
276
|
+
|
|
277
|
+
# Set TYER frame
|
|
278
|
+
command_year = ["id3v2", "--id3v2-only", "--TYER", year, str(file_path)]
|
|
279
|
+
run_external_tool(command_year, "id3v2")
|
|
280
|
+
|
|
281
|
+
# Set TDAT frame
|
|
282
|
+
command_date = ["id3v2", "--id3v2-only", "--TDAT", date_ddmm, str(file_path)]
|
|
283
|
+
run_external_tool(command_date, "id3v2")
|
|
284
|
+
else:
|
|
285
|
+
# Fallback for year-only dates
|
|
286
|
+
command = ["id3v2", "--id3v2-only", "--TYER", date_str, str(file_path)]
|
|
287
|
+
run_external_tool(command, "id3v2")
|
|
288
|
+
else:
|
|
289
|
+
# ID3v2.4: Use TDRC with full date
|
|
290
|
+
command = ["mid3v2", "--TDRC", date_str, str(file_path)]
|
|
291
|
+
run_external_tool(command, "mid3v2")
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _set_multiple_values_single_frame(
|
|
295
|
+
file_path: Path, frame_id: str, values: list[str], version: str = "2.4", separator: str | None = None
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Set multiple values in a single ID3v2 frame with version-specific handling.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
file_path: Path to the audio file
|
|
301
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
|
|
302
|
+
values: List of values to set in the frame
|
|
303
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
304
|
+
separator: Separator to use between values. If None, uses default behavior
|
|
305
|
+
(semicolon for ID3v2.3, null byte for ID3v2.4)
|
|
306
|
+
"""
|
|
307
|
+
# Determine separator if not provided
|
|
308
|
+
if separator is None:
|
|
309
|
+
separator = ";" if version == "2.3" else "\x00"
|
|
310
|
+
|
|
311
|
+
# Use appropriate method for all cases
|
|
312
|
+
ID3v2MetadataSetter._set_single_frame_with_id3v2(file_path, frame_id, values, version, separator)
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def _set_multiple_metadata_values(
|
|
316
|
+
file_path: Path,
|
|
317
|
+
frame_id: str,
|
|
318
|
+
values: list[str],
|
|
319
|
+
in_separate_frames: bool = False,
|
|
320
|
+
version: str = "2.4",
|
|
321
|
+
separator: str | None = None,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Set multiple metadata values, either in separate frames or a single frame.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
file_path: Path to the audio file
|
|
327
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
|
|
328
|
+
values: List of values to set
|
|
329
|
+
in_separate_frames: If True, creates multiple separate frames. If False, creates a
|
|
330
|
+
single frame with multiple values.
|
|
331
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
332
|
+
separator: Separator to use between values when in_separate_frames=False. If None, uses default behavior.
|
|
333
|
+
"""
|
|
334
|
+
from .id3v2_metadata_deleter import ID3v2MetadataDeleter
|
|
335
|
+
|
|
336
|
+
# Delete existing frames
|
|
337
|
+
ID3v2MetadataDeleter.delete_frame(file_path, frame_id)
|
|
338
|
+
|
|
339
|
+
if in_separate_frames:
|
|
340
|
+
# Use manual binary construction to create truly separate frames
|
|
341
|
+
ID3v2MetadataSetter._create_multiple_id3v2_frames(file_path, frame_id, values, version)
|
|
342
|
+
else:
|
|
343
|
+
# Create a single frame with multiple values (version-specific handling)
|
|
344
|
+
ID3v2MetadataSetter._set_multiple_values_single_frame(file_path, frame_id, values, version, separator)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def set_genres(file_path: Path, genres: list[str], in_separate_frames: bool = False, version: str = "2.4"):
|
|
348
|
+
"""Set ID3v2 multiple genres using external mid3v2 tool or manual frame creation.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
file_path: Path to the audio file
|
|
352
|
+
genres: List of genre strings to set
|
|
353
|
+
in_separate_frames: If True, creates multiple separate TCON frames (one per genre)
|
|
354
|
+
using manual binary construction. If False (default), creates a single TCON frame
|
|
355
|
+
with multiple values using mid3v2.
|
|
356
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
357
|
+
"""
|
|
358
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCON", genres, in_separate_frames, version)
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def set_album_artists(file_path: Path, album_artists: list[str], in_separate_frames: bool = False):
|
|
362
|
+
"""Set ID3v2 multiple album artists using external mid3v2 tool or manual frame creation.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
file_path: Path to the audio file
|
|
366
|
+
album_artists: List of album artist strings to set
|
|
367
|
+
in_separate_frames: If True, creates multiple separate TPE2 frames (one per album artist)
|
|
368
|
+
using manual binary construction. If False (default), creates a single TPE2 frame
|
|
369
|
+
with multiple values using mid3v2.
|
|
370
|
+
"""
|
|
371
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TPE2", album_artists, in_separate_frames)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def set_composers(file_path: Path, composers: list[str], in_separate_frames: bool = False, version: str = "2.4"):
|
|
375
|
+
"""Set ID3v2 multiple composers using external mid3v2 tool or manual frame creation.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
file_path: Path to the audio file
|
|
379
|
+
composers: List of composer strings to set
|
|
380
|
+
in_separate_frames: If True, creates multiple separate TCOM frames (one per composer)
|
|
381
|
+
using manual binary construction. If False (default), creates a single TCOM frame
|
|
382
|
+
with multiple values using mid3v2.
|
|
383
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
384
|
+
"""
|
|
385
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCOM", composers, in_separate_frames, version)
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def set_comments(file_path: Path, comments: list[str], in_separate_frames: bool = False):
|
|
389
|
+
"""Set ID3v2 multiple comments using external mid3v2 tool.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
file_path: Path to the audio file
|
|
393
|
+
comments: List of comment strings to set
|
|
394
|
+
in_separate_frames: If True, creates multiple separate COMM frames (one per comment).
|
|
395
|
+
If False (default), creates a single COMM frame with the first comment value.
|
|
396
|
+
"""
|
|
397
|
+
from .id3v2_metadata_deleter import ID3v2MetadataDeleter
|
|
398
|
+
|
|
399
|
+
# Delete existing COMM tags
|
|
400
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "COMM")
|
|
401
|
+
|
|
402
|
+
if in_separate_frames:
|
|
403
|
+
# Set each comment in a separate id3v2 call to force multiple frames
|
|
404
|
+
for comment in comments:
|
|
405
|
+
command = ["id3v2", "--id3v2-only", "--comment", comment, str(file_path)]
|
|
406
|
+
run_external_tool(command, "id3v2")
|
|
407
|
+
# Set only the first comment (ID3v2 comment fields are typically single-valued)
|
|
408
|
+
elif comments:
|
|
409
|
+
command = ["id3v2", "--id3v2-only", "--comment", comments[0], str(file_path)]
|
|
410
|
+
run_external_tool(command, "id3v2")
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _create_multiple_id3v2_frames(file_path: Path, frame_id: str, texts: list[str], version: str = "2.4") -> None:
|
|
414
|
+
"""Create multiple separate ID3v2 frames using manual binary construction.
|
|
415
|
+
|
|
416
|
+
This uses the ManualID3v2FrameCreator to bypass standard tools that
|
|
417
|
+
consolidate multiple frames of the same type, allowing creation of
|
|
418
|
+
truly separate frames for testing purposes.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
file_path: Path to the audio file
|
|
422
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TPE2', 'TCON', 'TCOM')
|
|
423
|
+
texts: List of text values, one per frame
|
|
424
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
425
|
+
"""
|
|
426
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
427
|
+
|
|
428
|
+
# Create frames based on the frame type
|
|
429
|
+
if frame_id == "TPE1":
|
|
430
|
+
ManualID3v2FrameCreator.create_multiple_tpe1_frames(file_path, texts, version)
|
|
431
|
+
elif frame_id == "TPE2":
|
|
432
|
+
ManualID3v2FrameCreator.create_multiple_tpe2_frames(file_path, texts, version)
|
|
433
|
+
elif frame_id == "TCON":
|
|
434
|
+
ManualID3v2FrameCreator.create_multiple_tcon_frames(file_path, texts, version)
|
|
435
|
+
elif frame_id == "TCOM":
|
|
436
|
+
ManualID3v2FrameCreator.create_multiple_tcom_frames(file_path, texts, version)
|
|
437
|
+
else:
|
|
438
|
+
# Generic frame creation for other frame types (including TIT2)
|
|
439
|
+
creator = ManualID3v2FrameCreator()
|
|
440
|
+
frames = []
|
|
441
|
+
for text in texts:
|
|
442
|
+
frame_data = creator._create_text_frame(frame_id, text, version)
|
|
443
|
+
frames.append(frame_data)
|
|
444
|
+
creator._write_id3v2_tag(file_path, frames, version)
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _set_single_frame_with_id3v2(
|
|
448
|
+
file_path: Path, frame_id: str, alist: list[str], version: str, separator: str | None = None
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Internal helper: Create a single ID3v2 frame using appropriate tool based on version.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
file_path: Path to the audio file
|
|
454
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
|
|
455
|
+
alist: List of text values for the frame
|
|
456
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
457
|
+
separator: Separator to use between values. If None, uses default.
|
|
458
|
+
"""
|
|
459
|
+
# Determine separator if not provided
|
|
460
|
+
if separator is None:
|
|
461
|
+
separator = ";" if version == "2.3" else "\x00"
|
|
462
|
+
|
|
463
|
+
# Combine values with the appropriate separator
|
|
464
|
+
combined_text = separator.join(alist) if len(alist) > 1 else alist[0] if alist else ""
|
|
465
|
+
|
|
466
|
+
# Check if we have null bytes - if so, use manual frame creator for ID3v2.4
|
|
467
|
+
if version == "2.4" and "\x00" in combined_text:
|
|
468
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
469
|
+
|
|
470
|
+
creator = ManualID3v2FrameCreator()
|
|
471
|
+
frame_data = creator._create_text_frame(frame_id, combined_text, version)
|
|
472
|
+
creator._write_id3v2_tag(file_path, [frame_data], version)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
# Map frame IDs to tool flags
|
|
476
|
+
flag_mapping = {
|
|
477
|
+
"TCON": "--genre",
|
|
478
|
+
"TIT2": "--song",
|
|
479
|
+
"TPE1": "--artist",
|
|
480
|
+
"TPE2": "--TPE2",
|
|
481
|
+
"TALB": "--album",
|
|
482
|
+
"TDRC": "--year",
|
|
483
|
+
"TRCK": "--track",
|
|
484
|
+
"COMM": "--comment",
|
|
485
|
+
"TCOM": "--TCOM",
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
flag = flag_mapping.get(frame_id, f"--{frame_id}")
|
|
489
|
+
|
|
490
|
+
if version == "2.3":
|
|
491
|
+
# Use id3v2 for ID3v2.3
|
|
492
|
+
command = ["id3v2", "--id3v2-only", flag, combined_text, str(file_path)]
|
|
493
|
+
run_external_tool(command, "id3v2")
|
|
494
|
+
else:
|
|
495
|
+
# Use mid3v2 for ID3v2.4
|
|
496
|
+
command = ["mid3v2", flag, combined_text, str(file_path)]
|
|
497
|
+
run_external_tool(command, "mid3v2")
|
|
498
|
+
|
|
499
|
+
@staticmethod
|
|
500
|
+
def write_tpe1_with_encoding(file_path: Path, text: str, encoding: int) -> None:
|
|
501
|
+
"""Write a TPE1 frame with specific encoding for testing purposes."""
|
|
502
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
503
|
+
|
|
504
|
+
creator = ManualID3v2FrameCreator()
|
|
505
|
+
frame_data = creator._create_text_frame("TPE1", text, "2.4", encoding=encoding)
|
|
506
|
+
creator._write_id3v2_tag(file_path, [frame_data], "2.4")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""RIFF metadata format helpers."""
|
|
2
|
+
|
|
3
|
+
from .riff_header_verifier import RIFFHeaderVerifier
|
|
4
|
+
from .riff_metadata_deleter import RIFFMetadataDeleter
|
|
5
|
+
from .riff_metadata_getter import RIFFMetadataGetter
|
|
6
|
+
from .riff_metadata_setter import RIFFMetadataSetter
|
|
7
|
+
|
|
8
|
+
__all__ = ["RIFFMetadataGetter", "RIFFHeaderVerifier", "RIFFMetadataDeleter", "RIFFMetadataSetter"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""RIFF metadata header verification utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..common.external_tool_runner import run_external_tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RIFFHeaderVerifier:
|
|
9
|
+
"""Utilities for verifying RIFF metadata headers in audio files."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def has_riff_info_chunk(file_path: Path) -> bool:
|
|
13
|
+
"""Check if file has RIFF INFO chunk by reading file structure."""
|
|
14
|
+
try:
|
|
15
|
+
with file_path.open("rb") as f:
|
|
16
|
+
# Read first few bytes to check for ID3v2 tags
|
|
17
|
+
first_bytes = f.read(10)
|
|
18
|
+
f.seek(0) # Reset to beginning
|
|
19
|
+
|
|
20
|
+
if first_bytes.startswith(b"ID3"):
|
|
21
|
+
# File has ID3v2 tags, find RIFF header after them
|
|
22
|
+
data = f.read()
|
|
23
|
+
pos = 0
|
|
24
|
+
while pos < len(data) - 8:
|
|
25
|
+
if data[pos : pos + 4] == b"RIFF":
|
|
26
|
+
# Found RIFF header, check for LIST chunk containing INFO
|
|
27
|
+
riff_size = int.from_bytes(data[pos + 4 : pos + 8], "little")
|
|
28
|
+
riff_data = data[pos + 8 : pos + 8 + riff_size]
|
|
29
|
+
|
|
30
|
+
# Search for LIST chunk containing INFO in RIFF data
|
|
31
|
+
# Skip the WAVE chunk header (4 bytes)
|
|
32
|
+
info_pos = 4
|
|
33
|
+
while info_pos < len(riff_data) - 8:
|
|
34
|
+
chunk_id = riff_data[info_pos : info_pos + 4]
|
|
35
|
+
chunk_size = int.from_bytes(riff_data[info_pos + 4 : info_pos + 8], "little")
|
|
36
|
+
|
|
37
|
+
if chunk_id == b"LIST":
|
|
38
|
+
# Check if this LIST chunk contains INFO
|
|
39
|
+
list_data = riff_data[info_pos + 8 : info_pos + 8 + chunk_size]
|
|
40
|
+
if len(list_data) >= 4 and list_data[:4] == b"INFO":
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# Move to next chunk (chunk size + padding)
|
|
44
|
+
info_pos += 8 + chunk_size
|
|
45
|
+
if chunk_size % 2 == 1: # Odd size needs padding
|
|
46
|
+
info_pos += 1
|
|
47
|
+
return False
|
|
48
|
+
pos += 1
|
|
49
|
+
return False
|
|
50
|
+
# File starts with RIFF header
|
|
51
|
+
riff_header = f.read(12)
|
|
52
|
+
if riff_header[:4] != b"RIFF":
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
# Look for LIST chunk containing INFO
|
|
56
|
+
chunk_size = int.from_bytes(riff_header[4:8], "little")
|
|
57
|
+
data = f.read(chunk_size)
|
|
58
|
+
|
|
59
|
+
# Search for LIST chunk containing INFO
|
|
60
|
+
pos = 0
|
|
61
|
+
while pos < len(data) - 8:
|
|
62
|
+
chunk_id = data[pos : pos + 4]
|
|
63
|
+
chunk_size = int.from_bytes(data[pos + 4 : pos + 8], "little")
|
|
64
|
+
|
|
65
|
+
if chunk_id == b"LIST":
|
|
66
|
+
# Check if this LIST chunk contains INFO
|
|
67
|
+
list_data = data[pos + 8 : pos + 8 + chunk_size]
|
|
68
|
+
if len(list_data) >= 4 and list_data[:4] == b"INFO":
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
# Move to next chunk (chunk size + padding)
|
|
72
|
+
pos += 8 + chunk_size
|
|
73
|
+
if chunk_size % 2 == 1: # Odd size needs padding
|
|
74
|
+
pos += 1
|
|
75
|
+
|
|
76
|
+
return False
|
|
77
|
+
except (OSError, ValueError):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_riff_metadata_info(file_path: Path) -> str:
|
|
82
|
+
"""Get RIFF metadata info using exiftool."""
|
|
83
|
+
command = ["exiftool", "-a", "-G", str(file_path)]
|
|
84
|
+
result = run_external_tool(command, "exiftool")
|
|
85
|
+
return result.stdout
|