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,82 @@
|
|
|
1
|
+
"""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 ID3v1MetadataSetter:
|
|
10
|
+
"""Static utility class for ID3v1 metadata setting using external tools."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def set_genre(file_path: Path, genre_code: str) -> None:
|
|
14
|
+
"""Set ID3v1 genre using external id3v2 tool."""
|
|
15
|
+
command = ["id3v2", "--id3v1-only", "--genre", genre_code, str(file_path)]
|
|
16
|
+
run_external_tool(command, "id3v2")
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def set_comment(file_path: Path, comment: str) -> None:
|
|
20
|
+
"""Set ID3v1 comment using external id3v2 tool."""
|
|
21
|
+
command = ["id3v2", "--id3v1-only", "--comment", comment, str(file_path)]
|
|
22
|
+
run_external_tool(command, "id3v2")
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def set_title(file_path: Path, title: str) -> None:
|
|
26
|
+
"""Set ID3v1 title using external id3v2 tool."""
|
|
27
|
+
command = ["id3v2", "--id3v1-only", "--song", title, str(file_path)]
|
|
28
|
+
run_external_tool(command, "id3v2")
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def set_artist(file_path: Path, artist: str) -> None:
|
|
32
|
+
"""Set ID3v1 artist using external id3v2 tool."""
|
|
33
|
+
command = ["id3v2", "--id3v1-only", "--artist", artist, str(file_path)]
|
|
34
|
+
run_external_tool(command, "id3v2")
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def set_album(file_path: Path, album: str) -> None:
|
|
38
|
+
"""Set ID3v1 album using external id3v2 tool."""
|
|
39
|
+
command = ["id3v2", "--id3v1-only", "--album", album, str(file_path)]
|
|
40
|
+
run_external_tool(command, "id3v2")
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def set_max_metadata(file_path: Path) -> None:
|
|
44
|
+
"""Set maximum ID3v1 metadata using external script."""
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
from ..common.external_tool_runner import run_script
|
|
48
|
+
|
|
49
|
+
scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
|
|
50
|
+
run_script("set-id3v1-max-metadata.sh", file_path, scripts_dir)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def set_metadata(file_path: Path, metadata: dict[str, Any]) -> None:
|
|
54
|
+
"""Set ID3v1 metadata using id3v2 tool (id3v2 can also set ID3v1 tags)."""
|
|
55
|
+
# Ensure ID3v1.1 format when track is set
|
|
56
|
+
metadata = metadata.copy()
|
|
57
|
+
if "track" in metadata and "comment" not in metadata:
|
|
58
|
+
metadata["comment"] = " " * 28 # Set comment to 28 spaces to enable ID3v1.1
|
|
59
|
+
|
|
60
|
+
cmd = ["id3v2", "--id3v1-only"]
|
|
61
|
+
|
|
62
|
+
# Map common metadata keys to id3v2 arguments for ID3v1
|
|
63
|
+
key_mapping = {
|
|
64
|
+
"title": "--song",
|
|
65
|
+
"artist": "--artist",
|
|
66
|
+
"album": "--album",
|
|
67
|
+
"year": "--year",
|
|
68
|
+
"genre": "--genre",
|
|
69
|
+
"comment": "--comment",
|
|
70
|
+
"track": "--track",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
metadata_added = False
|
|
74
|
+
for key, value in metadata.items():
|
|
75
|
+
if key.lower() in key_mapping:
|
|
76
|
+
cmd.extend([key_mapping[key.lower()], str(value)])
|
|
77
|
+
metadata_added = True
|
|
78
|
+
|
|
79
|
+
# Only run id3v2 if metadata was actually added
|
|
80
|
+
if metadata_added:
|
|
81
|
+
cmd.append(str(file_path))
|
|
82
|
+
run_external_tool(cmd, "id3v2")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""ID3v2 metadata format helpers."""
|
|
2
|
+
|
|
3
|
+
# Core operations (following RIFF pattern)
|
|
4
|
+
# External tool wrappers
|
|
5
|
+
from ..common.external_tool_runner import ExternalMetadataToolError
|
|
6
|
+
|
|
7
|
+
# Advanced tools
|
|
8
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
9
|
+
from .id3v2_header_verifier import ID3v2HeaderVerifier
|
|
10
|
+
from .id3v2_metadata_deleter import ID3v2MetadataDeleter
|
|
11
|
+
from .id3v2_metadata_getter import ID3v2MetadataGetter
|
|
12
|
+
from .id3v2_metadata_setter import ID3v2MetadataSetter
|
|
13
|
+
|
|
14
|
+
# Specialized managers (moved to ID3v2MetadataSetter)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Core operations
|
|
19
|
+
"ID3v2HeaderVerifier",
|
|
20
|
+
"ID3v2MetadataDeleter",
|
|
21
|
+
"ID3v2MetadataSetter",
|
|
22
|
+
"ID3v2MetadataGetter",
|
|
23
|
+
# Specialized managers (moved to ID3v2MetadataSetter)
|
|
24
|
+
# Advanced tools
|
|
25
|
+
"ManualID3v2FrameCreator",
|
|
26
|
+
# External tool wrappers
|
|
27
|
+
"ExternalMetadataToolError",
|
|
28
|
+
]
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Manual implementation to create multiple separate ID3v2 frames for testing.
|
|
3
|
+
|
|
4
|
+
This bypasses standard tools and libraries that automatically consolidate frames, allowing creation of test files with
|
|
5
|
+
truly separate TPE1, TPE2, TCON etc. frames.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from audiometa.utils.tool_path_resolver import get_tool_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ManualID3v2FrameCreator:
|
|
17
|
+
"""Creates ID3v2 tags with multiple separate frames by manual binary construction."""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def create_multiple_tpe1_frames(file_path: Path, artists: list[str], version: str = "2.4") -> None:
|
|
21
|
+
if version not in ["2.3", "2.4"]:
|
|
22
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
23
|
+
raise ValueError(msg)
|
|
24
|
+
frames = []
|
|
25
|
+
for artist in artists:
|
|
26
|
+
frame_data = ManualID3v2FrameCreator._create_text_frame("TPE1", artist, version)
|
|
27
|
+
frames.append(frame_data)
|
|
28
|
+
|
|
29
|
+
ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def create_multiple_tpe2_frames(file_path: Path, album_artists: list[str], version: str = "2.4") -> None:
|
|
33
|
+
if version not in ["2.3", "2.4"]:
|
|
34
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
35
|
+
raise ValueError(msg)
|
|
36
|
+
frames = []
|
|
37
|
+
for album_artist in album_artists:
|
|
38
|
+
frame_data = ManualID3v2FrameCreator._create_text_frame("TPE2", album_artist, version)
|
|
39
|
+
frames.append(frame_data)
|
|
40
|
+
|
|
41
|
+
ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def create_multiple_tcon_frames(file_path: Path, genres: list[str], version: str = "2.4") -> None:
|
|
45
|
+
if version not in ["2.3", "2.4"]:
|
|
46
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
47
|
+
raise ValueError(msg)
|
|
48
|
+
frames = []
|
|
49
|
+
for genre in genres:
|
|
50
|
+
frame_data = ManualID3v2FrameCreator._create_text_frame("TCON", genre, version)
|
|
51
|
+
frames.append(frame_data)
|
|
52
|
+
|
|
53
|
+
ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def create_multiple_tcom_frames(file_path: Path, composers: list[str], version: str = "2.4") -> None:
|
|
57
|
+
if version not in ["2.3", "2.4"]:
|
|
58
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
59
|
+
raise ValueError(msg)
|
|
60
|
+
frames = []
|
|
61
|
+
for composer in composers:
|
|
62
|
+
frame_data = ManualID3v2FrameCreator._create_text_frame("TCOM", composer, version)
|
|
63
|
+
frames.append(frame_data)
|
|
64
|
+
|
|
65
|
+
ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def create_mixed_multiple_frames(
|
|
69
|
+
file_path: Path, artists: list[str], genres: list[str], version: str = "2.4"
|
|
70
|
+
) -> None:
|
|
71
|
+
if version not in ["2.3", "2.4"]:
|
|
72
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
73
|
+
raise ValueError(msg)
|
|
74
|
+
frames = []
|
|
75
|
+
|
|
76
|
+
# Add multiple TPE1 frames
|
|
77
|
+
for artist in artists:
|
|
78
|
+
frame_data = ManualID3v2FrameCreator._create_text_frame("TPE1", artist, version)
|
|
79
|
+
frames.append(frame_data)
|
|
80
|
+
|
|
81
|
+
# Add multiple TCON frames
|
|
82
|
+
for genre in genres:
|
|
83
|
+
frame_data = ManualID3v2FrameCreator._create_text_frame("TCON", genre, version)
|
|
84
|
+
frames.append(frame_data)
|
|
85
|
+
|
|
86
|
+
ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _create_text_frame(frame_id: str, text: str, version: str, encoding: int | None = None) -> bytes:
|
|
90
|
+
"""Create a single ID3v2 text frame with the given ID and text."""
|
|
91
|
+
# Choose encoding based on version or provided encoding
|
|
92
|
+
if encoding is not None:
|
|
93
|
+
enc = encoding
|
|
94
|
+
elif version == "2.3":
|
|
95
|
+
# ID3v2.3: Use ISO-8859-1
|
|
96
|
+
enc = 0
|
|
97
|
+
else: # ID3v2.4
|
|
98
|
+
# ID3v2.4: Use UTF-8
|
|
99
|
+
enc = 3
|
|
100
|
+
|
|
101
|
+
# Determine null terminator based on encoding
|
|
102
|
+
null_terminator = b"\x00\x00" if enc in (1, 2) else b"\x00"
|
|
103
|
+
|
|
104
|
+
# Encode text
|
|
105
|
+
if enc == 0: # ISO-8859-1
|
|
106
|
+
text_bytes = text.encode("latin1", errors="ignore")
|
|
107
|
+
elif enc == 1: # UTF-16 with BOM
|
|
108
|
+
text_bytes = text.encode("utf-16")
|
|
109
|
+
elif enc == 2: # UTF-16BE without BOM
|
|
110
|
+
text_bytes = text.encode("utf-16be")
|
|
111
|
+
elif enc == 3: # UTF-8
|
|
112
|
+
text_bytes = text.encode("utf-8")
|
|
113
|
+
else:
|
|
114
|
+
text_bytes = text.encode("latin1", errors="ignore")
|
|
115
|
+
|
|
116
|
+
# Frame data: encoding byte + text + null terminator
|
|
117
|
+
frame_data = struct.pack("B", enc) + text_bytes + null_terminator
|
|
118
|
+
|
|
119
|
+
# Frame header: ID (4 bytes) + size (4 bytes) + flags (2 bytes)
|
|
120
|
+
frame_id_bytes = frame_id.encode("ascii")
|
|
121
|
+
frame_size = len(frame_data)
|
|
122
|
+
frame_flags = 0x0000 # No flags
|
|
123
|
+
|
|
124
|
+
if version == "2.3":
|
|
125
|
+
frame_header = (
|
|
126
|
+
frame_id_bytes
|
|
127
|
+
+ struct.pack(">I", frame_size) # Big-endian 32-bit size
|
|
128
|
+
+ struct.pack(">H", frame_flags) # Big-endian 16-bit flags
|
|
129
|
+
)
|
|
130
|
+
else: # ID3v2.4
|
|
131
|
+
frame_header = (
|
|
132
|
+
frame_id_bytes
|
|
133
|
+
+ ManualID3v2FrameCreator._synchsafe_int(frame_size) # Synchsafe size
|
|
134
|
+
+ struct.pack(">H", frame_flags) # Big-endian 16-bit flags
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return frame_header + frame_data
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _synchsafe_int(value: int) -> bytes:
|
|
141
|
+
"""Convert integer to ID3v2 synchsafe integer (7 bits per byte)."""
|
|
142
|
+
# Split into 7-bit chunks, most significant first
|
|
143
|
+
result: list[int] = []
|
|
144
|
+
for _i in range(4):
|
|
145
|
+
result.insert(0, value & 0x7F)
|
|
146
|
+
value >>= 7
|
|
147
|
+
return struct.pack("4B", *result)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _syncsafe_decode(data: bytes) -> int:
|
|
151
|
+
"""Decode a 4-byte syncsafe integer."""
|
|
152
|
+
return ((data[0] & 0x7F) << 21) | ((data[1] & 0x7F) << 14) | ((data[2] & 0x7F) << 7) | (data[3] & 0x7F)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _write_id3v2_tag(file_path: Path, frames: list[bytes], version: str) -> None:
|
|
156
|
+
"""Write ID3v2 tag with the given frames to the file, preserving existing frames."""
|
|
157
|
+
|
|
158
|
+
# Read existing file content
|
|
159
|
+
with file_path.open("rb") as f:
|
|
160
|
+
original_data = f.read()
|
|
161
|
+
|
|
162
|
+
# Extract existing frames and audio data
|
|
163
|
+
existing_frames: list[bytes] = []
|
|
164
|
+
audio_data = original_data
|
|
165
|
+
frame_ids_to_replace: set[str] = set()
|
|
166
|
+
|
|
167
|
+
# Extract frame IDs from new frames to know which ones to replace
|
|
168
|
+
for frame_bytes in frames:
|
|
169
|
+
if len(frame_bytes) >= 4:
|
|
170
|
+
frame_id = frame_bytes[:4].decode("ascii", errors="ignore")
|
|
171
|
+
frame_ids_to_replace.add(frame_id)
|
|
172
|
+
|
|
173
|
+
if original_data.startswith(b"ID3") and len(original_data) >= 10:
|
|
174
|
+
# Parse existing ID3v2 tag
|
|
175
|
+
existing_version = original_data[3]
|
|
176
|
+
size_bytes = original_data[6:10]
|
|
177
|
+
|
|
178
|
+
if existing_version == 4:
|
|
179
|
+
# ID3v2.4 uses synchsafe integers
|
|
180
|
+
existing_tag_size = 0
|
|
181
|
+
for byte in size_bytes:
|
|
182
|
+
existing_tag_size = (existing_tag_size << 7) | (byte & 0x7F)
|
|
183
|
+
else:
|
|
184
|
+
# ID3v2.3 and earlier use regular integers
|
|
185
|
+
existing_tag_size = struct.unpack(">I", size_bytes)[0]
|
|
186
|
+
|
|
187
|
+
# Read existing tag data
|
|
188
|
+
tag_data = original_data[10 : 10 + existing_tag_size]
|
|
189
|
+
audio_data = original_data[10 + existing_tag_size :]
|
|
190
|
+
|
|
191
|
+
# Parse existing frames, preserving those not being replaced
|
|
192
|
+
pos = 0
|
|
193
|
+
while pos < len(tag_data) - 10:
|
|
194
|
+
frame_id_bytes = tag_data[pos : pos + 4]
|
|
195
|
+
if frame_id_bytes == b"\x00\x00\x00\x00":
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
frame_id_str = frame_id_bytes.decode("ascii")
|
|
200
|
+
except UnicodeDecodeError:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
# Determine frame size based on version
|
|
204
|
+
if existing_version == 4:
|
|
205
|
+
frame_size = ManualID3v2FrameCreator._syncsafe_decode(tag_data[pos + 4 : pos + 8])
|
|
206
|
+
else:
|
|
207
|
+
frame_size = int.from_bytes(tag_data[pos + 4 : pos + 8], "big")
|
|
208
|
+
|
|
209
|
+
if pos + 10 + frame_size > len(tag_data):
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
# Only preserve frames that aren't being replaced
|
|
213
|
+
if frame_id_str not in frame_ids_to_replace:
|
|
214
|
+
frame_data = tag_data[pos : pos + 10 + frame_size]
|
|
215
|
+
existing_frames.append(frame_data)
|
|
216
|
+
|
|
217
|
+
pos += 10 + frame_size
|
|
218
|
+
|
|
219
|
+
# Combine existing frames (that aren't being replaced) with new frames
|
|
220
|
+
all_frames = existing_frames + frames
|
|
221
|
+
frames_data = b"".join(all_frames)
|
|
222
|
+
tag_size = len(frames_data)
|
|
223
|
+
|
|
224
|
+
# Create header based on version
|
|
225
|
+
if version == "2.3":
|
|
226
|
+
# ID3v2.3 header: "ID3" + version + flags + size (regular integer)
|
|
227
|
+
header = (
|
|
228
|
+
b"ID3" # ID3 identifier
|
|
229
|
+
+ struct.pack("BB", 3, 0) # Version 2.3.0
|
|
230
|
+
+ struct.pack("B", 0) # Flags (no unsynchronisation, etc.)
|
|
231
|
+
+ struct.pack(">I", tag_size) # Size as regular 32-bit integer
|
|
232
|
+
)
|
|
233
|
+
else: # ID3v2.4
|
|
234
|
+
# ID3v2.4 header: "ID3" + version + flags + size (synchsafe integer)
|
|
235
|
+
header = (
|
|
236
|
+
b"ID3" # ID3 identifier
|
|
237
|
+
+ struct.pack("BB", 4, 0) # Version 2.4.0
|
|
238
|
+
+ struct.pack("B", 0) # Flags (no unsynchronisation, etc.)
|
|
239
|
+
+ ManualID3v2FrameCreator._synchsafe_int(tag_size) # Size as synchsafe integer
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Write new file with merged ID3v2 tag
|
|
243
|
+
with file_path.open("wb") as f:
|
|
244
|
+
f.write(header)
|
|
245
|
+
f.write(frames_data)
|
|
246
|
+
f.write(audio_data)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def manual_multiple_frames_test():
|
|
250
|
+
"""Test the manual frame creation for both ID3v2.3 and ID3v2.4."""
|
|
251
|
+
|
|
252
|
+
def run_test_for_version(version: str):
|
|
253
|
+
"""Test a specific ID3v2 version."""
|
|
254
|
+
|
|
255
|
+
# Create a temporary MP3 file
|
|
256
|
+
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp:
|
|
257
|
+
tmp_path = Path(tmp.name)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
# Create minimal MP3 file
|
|
261
|
+
subprocess.run(
|
|
262
|
+
["ffmpeg", "-f", "lavfi", "-i", "anullsrc=duration=1", "-acodec", "mp3", "-y", str(tmp_path)],
|
|
263
|
+
check=True,
|
|
264
|
+
capture_output=True,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Test 1: Multiple TPE1 frames
|
|
268
|
+
artists = ["Artist One", "Artist Two", "Artist Three"]
|
|
269
|
+
ManualID3v2FrameCreator.create_multiple_tpe1_frames(tmp_path, artists, version)
|
|
270
|
+
|
|
271
|
+
# Check result with mid3v2
|
|
272
|
+
result = subprocess.run(
|
|
273
|
+
[get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Count TPE1 occurrences
|
|
277
|
+
tpe1_count = result.stdout.count("TPE1=")
|
|
278
|
+
|
|
279
|
+
if tpe1_count > 1:
|
|
280
|
+
pass
|
|
281
|
+
else:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
# Test 2: Multiple TCON frames
|
|
285
|
+
genres = ["Rock", "Pop", "Alternative"]
|
|
286
|
+
ManualID3v2FrameCreator.create_multiple_tcon_frames(tmp_path, genres, version)
|
|
287
|
+
|
|
288
|
+
result = subprocess.run(
|
|
289
|
+
[get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
tcon_count = result.stdout.count("TCON=")
|
|
293
|
+
|
|
294
|
+
# Test 3: Mixed multiple frames
|
|
295
|
+
ManualID3v2FrameCreator.create_mixed_multiple_frames(
|
|
296
|
+
tmp_path, artists=["Artist A", "Artist B"], genres=["Genre X", "Genre Y"], version=version
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
result = subprocess.run(
|
|
300
|
+
[get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Check version in the file and verify multiple frames exist in binary
|
|
304
|
+
result = subprocess.run(
|
|
305
|
+
[get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
|
|
306
|
+
)
|
|
307
|
+
if result.stdout:
|
|
308
|
+
# Verify multiple frames exist by checking raw binary
|
|
309
|
+
with Path(tmp_path).open("rb") as f:
|
|
310
|
+
data = f.read(1000) # Read first 1KB to check for multiple frame IDs
|
|
311
|
+
tpe1_count = data.count(b"TPE1")
|
|
312
|
+
tcon_count = data.count(b"TCON")
|
|
313
|
+
|
|
314
|
+
if tpe1_count > 1 or tcon_count > 1:
|
|
315
|
+
pass
|
|
316
|
+
else:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
finally:
|
|
320
|
+
if tmp_path.exists():
|
|
321
|
+
tmp_path.unlink()
|
|
322
|
+
|
|
323
|
+
# Test both versions
|
|
324
|
+
run_test_for_version("2.3")
|
|
325
|
+
run_test_for_version("2.4")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def create_test_file_with_version(
|
|
329
|
+
output_path: Path, version: str = "2.4", artists: list[str] | None = None, genres: list[str] | None = None
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Create a test MP3 file with multiple frames in the specified ID3v2 version."""
|
|
332
|
+
if artists is None:
|
|
333
|
+
artists = ["Artist One", "Artist Two", "Artist Three"]
|
|
334
|
+
if genres is None:
|
|
335
|
+
genres = ["Rock", "Pop", "Alternative"]
|
|
336
|
+
|
|
337
|
+
# Create minimal MP3 file
|
|
338
|
+
subprocess.run(
|
|
339
|
+
["ffmpeg", "-f", "lavfi", "-i", "anullsrc=duration=1", "-acodec", "mp3", "-y", str(output_path)],
|
|
340
|
+
check=True,
|
|
341
|
+
capture_output=True,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Add multiple frames
|
|
345
|
+
ManualID3v2FrameCreator.create_mixed_multiple_frames(output_path, artists, genres, version)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
manual_multiple_frames_test()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""ID3 metadata header verification utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ID3v2HeaderVerifier:
|
|
7
|
+
"""Utilities for verifying ID3 metadata headers in audio files."""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def has_id3v2_header(file_path: Path) -> bool:
|
|
11
|
+
"""Check if file has ID3v2 header by reading the first 10 bytes."""
|
|
12
|
+
try:
|
|
13
|
+
with file_path.open("rb") as f:
|
|
14
|
+
header = f.read(10)
|
|
15
|
+
return header[:3] == b"ID3"
|
|
16
|
+
except OSError:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def get_id3v2_version(file_path: Path) -> tuple[int, int, int] | None:
|
|
21
|
+
"""Get the ID3v2 version of the file.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
file_path: Path to the audio file
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Version tuple (major, minor, revision) or None if no ID3v2 header found
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
from mutagen.id3 import ID3, ID3NoHeaderError
|
|
31
|
+
|
|
32
|
+
id3_tags = ID3(file_path)
|
|
33
|
+
except ID3NoHeaderError:
|
|
34
|
+
return None
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
else:
|
|
38
|
+
return id3_tags.version
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""ID3v2 metadata deletion operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..common.external_tool_runner import run_external_tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ID3v2MetadataDeleter:
|
|
9
|
+
"""Static utility class for ID3v2 metadata deletion using external tools."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def delete_frame(file_path: Path, frame_id: str) -> None:
|
|
13
|
+
"""Delete a specific ID3v2 frame."""
|
|
14
|
+
try:
|
|
15
|
+
command = ["mid3v2", "--delete-frames", frame_id, str(file_path)]
|
|
16
|
+
run_external_tool(command, "mid3v2")
|
|
17
|
+
except Exception:
|
|
18
|
+
# Ignore if frame doesn't exist
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def delete_comment(file_path: Path) -> None:
|
|
23
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "COMM")
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def delete_title(file_path: Path) -> None:
|
|
27
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "TIT2")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def delete_artist(file_path: Path) -> None:
|
|
31
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "TPE1")
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def delete_album(file_path: Path) -> None:
|
|
35
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "TALB")
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def delete_genre(file_path: Path) -> None:
|
|
39
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "TCON")
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def delete_lyrics(file_path: Path) -> None:
|
|
43
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "USLT")
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def delete_language(file_path: Path) -> None:
|
|
47
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "TLAN")
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def delete_bpm(file_path: Path) -> None:
|
|
51
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "TBPM")
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def delete_tag(file_path: Path, tag_name: str) -> None:
|
|
55
|
+
command = ["id3v2", "--id3v2-only", "--delete", tag_name, str(file_path)]
|
|
56
|
+
run_external_tool(command, "id3v2")
|