audiometa-python 0.2.2__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 +1240 -0
- audiometa/__main__.py +6 -0
- audiometa/_audio_file.py +602 -0
- audiometa/cli.py +347 -0
- audiometa/exceptions.py +167 -0
- audiometa/manager/_MetadataManager.py +665 -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 +945 -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 +778 -0
- audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
- audiometa/manager/_rating_supporting/riff/_riff_constants.py +10 -0
- audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +525 -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 +305 -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 +188 -0
- audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +428 -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 +216 -0
- audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
- audiometa/test/helpers/riff/riff_metadata_getter.py +118 -0
- audiometa/test/helpers/riff/riff_metadata_setter.py +196 -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 +200 -0
- audiometa/test/tests/__init__.py +0 -0
- audiometa/test/tests/conftest.py +261 -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/test_cli_delete.py +20 -0
- audiometa/test/tests/e2e/cli/test_cli_formatting.py +31 -0
- audiometa/test/tests/e2e/cli/test_cli_help.py +40 -0
- audiometa/test/tests/e2e/cli/test_cli_read.py +79 -0
- audiometa/test/tests/e2e/cli/test_cli_unified.py +33 -0
- audiometa/test/tests/e2e/cli/test_cli_write.py +116 -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_binary_data_filtering.py +250 -0
- audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +297 -0
- audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata_edge_cases.py +231 -0
- audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
- audiometa/test/tests/integration/get_full_metadata/test_parsed_fields_keys.py +85 -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/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/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 +134 -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 +38 -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 +23 -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 +130 -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_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 +57 -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_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_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/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/os_dependencies_checker/__init__.py +24 -0
- audiometa/utils/os_dependencies_checker/base.py +62 -0
- audiometa/utils/os_dependencies_checker/config.py +51 -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 +81 -0
- audiometa_python-0.2.2.dist-info/METADATA +1464 -0
- audiometa_python-0.2.2.dist-info/RECORD +318 -0
- audiometa_python-0.2.2.dist-info/WHEEL +5 -0
- audiometa_python-0.2.2.dist-info/entry_points.txt +2 -0
- audiometa_python-0.2.2.dist-info/licenses/LICENSE +202 -0
- audiometa_python-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""id3v2_metadata_getter.py Helper for extracting ID3v2 metadata from audio files."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ID3v2MetadataGetter:
|
|
5
|
+
"""Helper class to get ID3v2 metadata from audio files using manual parsing."""
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def _syncsafe_decode(data: bytes) -> int:
|
|
9
|
+
"""Decode a 4-byte syncsafe integer."""
|
|
10
|
+
return ((data[0] & 0x7F) << 21) | ((data[1] & 0x7F) << 14) | ((data[2] & 0x7F) << 7) | (data[3] & 0x7F)
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def _decode_text(encoding: int, data: bytes) -> str:
|
|
14
|
+
"""Decode text based on encoding."""
|
|
15
|
+
if encoding == 0: # ISO-8859-1
|
|
16
|
+
return data.decode("latin1", errors="ignore")
|
|
17
|
+
if encoding == 1: # UTF-16 with BOM
|
|
18
|
+
return data.decode("utf-16", errors="ignore")
|
|
19
|
+
if encoding == 2: # UTF-16BE without BOM
|
|
20
|
+
return data.decode("utf-16be", errors="ignore")
|
|
21
|
+
if encoding == 3: # UTF-8
|
|
22
|
+
return data.decode("utf-8", errors="ignore")
|
|
23
|
+
return data.decode("latin1", errors="ignore") # fallback
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_raw_metadata(file_path, version=None):
|
|
27
|
+
"""Get the raw metadata from the audio file using manual ID3v2 parsing, returning a string with frame IDs and
|
|
28
|
+
values.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
file_path: Path to the audio file.
|
|
32
|
+
version: The ID3v2 version to parse ("2.3" for ID3v2.3, "2.4" for ID3v2.4). Defaults to "2.4".
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
String with metadata in 'frame_id=value' format, one per line, or error string if parsing fails.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
with file_path.open("rb") as f:
|
|
39
|
+
# Read ID3v2 header (10 bytes)
|
|
40
|
+
header = f.read(10)
|
|
41
|
+
if len(header) < 10 or header[:3] != b"ID3":
|
|
42
|
+
return "No ID3v2 tag found"
|
|
43
|
+
|
|
44
|
+
# Parse header
|
|
45
|
+
file_version = (header[3], header[4])
|
|
46
|
+
if version is None:
|
|
47
|
+
version = "2.4" # Default to 2.4
|
|
48
|
+
if version == "2.3":
|
|
49
|
+
expected_major = 3
|
|
50
|
+
expected_minor = 0
|
|
51
|
+
elif version == "2.4":
|
|
52
|
+
expected_major = 4
|
|
53
|
+
expected_minor = 0
|
|
54
|
+
else:
|
|
55
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
56
|
+
raise ValueError(msg)
|
|
57
|
+
if file_version[0] != expected_major or file_version[1] != expected_minor:
|
|
58
|
+
return None
|
|
59
|
+
header[5]
|
|
60
|
+
tag_size = ID3v2MetadataGetter._syncsafe_decode(header[6:10])
|
|
61
|
+
|
|
62
|
+
# Read tag data
|
|
63
|
+
tag_data = f.read(tag_size)
|
|
64
|
+
if len(tag_data) != tag_size:
|
|
65
|
+
return "Incomplete ID3v2 tag"
|
|
66
|
+
|
|
67
|
+
metadata: dict[str, list[str]] = {}
|
|
68
|
+
pos = 0
|
|
69
|
+
while pos < len(tag_data) - 10:
|
|
70
|
+
# Parse frame header (10 bytes)
|
|
71
|
+
frame_id = tag_data[pos : pos + 4]
|
|
72
|
+
if frame_id == b"\x00\x00\x00\x00":
|
|
73
|
+
break # Padding or end
|
|
74
|
+
try:
|
|
75
|
+
frame_id_str = frame_id.decode("ascii")
|
|
76
|
+
except UnicodeDecodeError:
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if expected_minor == 4:
|
|
80
|
+
frame_size = ID3v2MetadataGetter._syncsafe_decode(tag_data[pos + 4 : pos + 8])
|
|
81
|
+
else:
|
|
82
|
+
frame_size = int.from_bytes(tag_data[pos + 4 : pos + 8], "big")
|
|
83
|
+
|
|
84
|
+
if pos + 10 + frame_size > len(tag_data):
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
frame_data = tag_data[pos + 10 : pos + 10 + frame_size]
|
|
88
|
+
|
|
89
|
+
# Parse text frames (those starting with encoding byte)
|
|
90
|
+
if frame_data and len(frame_data) > 1:
|
|
91
|
+
encoding = frame_data[0]
|
|
92
|
+
text_data = frame_data[1:]
|
|
93
|
+
# Decode the entire text_data first
|
|
94
|
+
decoded_text = ID3v2MetadataGetter._decode_text(encoding, text_data).rstrip("\x00")
|
|
95
|
+
if frame_id_str == "USLT":
|
|
96
|
+
# Parse USLT: language (3 bytes) + descriptor (null-terminated) + lyrics (null-terminated)
|
|
97
|
+
text_data_bytes = text_data
|
|
98
|
+
if len(text_data_bytes) > 3:
|
|
99
|
+
language = text_data_bytes[:3].decode("ascii", errors="ignore")
|
|
100
|
+
uslt_pos = 3 # after language
|
|
101
|
+
while uslt_pos < len(text_data_bytes) and text_data_bytes[uslt_pos] != 0:
|
|
102
|
+
uslt_pos += 1
|
|
103
|
+
uslt_pos += 1 # skip null
|
|
104
|
+
lyrics_bytes = text_data_bytes[uslt_pos:].rstrip(b"\x00")
|
|
105
|
+
lyrics = ID3v2MetadataGetter._decode_text(encoding, lyrics_bytes)
|
|
106
|
+
text = f"{language}\x00{lyrics}"
|
|
107
|
+
else:
|
|
108
|
+
text = decoded_text
|
|
109
|
+
else:
|
|
110
|
+
text = decoded_text
|
|
111
|
+
if frame_id_str not in metadata:
|
|
112
|
+
metadata[frame_id_str] = []
|
|
113
|
+
metadata[frame_id_str].append(text)
|
|
114
|
+
else:
|
|
115
|
+
# Non-text frame, just show size
|
|
116
|
+
if frame_id_str not in metadata:
|
|
117
|
+
metadata[frame_id_str] = []
|
|
118
|
+
metadata[frame_id_str].append(f"<{frame_size} bytes>")
|
|
119
|
+
|
|
120
|
+
pos += 10 + frame_size
|
|
121
|
+
|
|
122
|
+
return metadata
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return f"Error parsing ID3v2: {e!s}"
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def get_artists(file_path, version=None):
|
|
128
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
|
|
129
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
130
|
+
return None
|
|
131
|
+
tpe1_values: list[str] = metadata.get("TPE1", [])
|
|
132
|
+
return tpe1_values[0] if tpe1_values else None
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def get_title(file_path, version=None):
|
|
136
|
+
try:
|
|
137
|
+
versions_to_try = [version] if version else ["2.3", "2.4"]
|
|
138
|
+
for v in versions_to_try:
|
|
139
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, v)
|
|
140
|
+
if isinstance(metadata, dict) and metadata:
|
|
141
|
+
tit2_values = metadata.get("TIT2", [])
|
|
142
|
+
if tit2_values:
|
|
143
|
+
return tit2_values[0]
|
|
144
|
+
except Exception:
|
|
145
|
+
return None
|
|
146
|
+
else:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def get_album(file_path, version=None):
|
|
151
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
|
|
152
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
153
|
+
return None
|
|
154
|
+
talb_values = metadata.get("TALB", [])
|
|
155
|
+
return talb_values[0] if talb_values else None
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def get_year(file_path, version=None):
|
|
159
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
|
|
160
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
161
|
+
return None
|
|
162
|
+
tyer_values = metadata.get("TYER", [])
|
|
163
|
+
tdrc_values = metadata.get("TDRC", [])
|
|
164
|
+
return (tyer_values + tdrc_values)[0] if tyer_values or tdrc_values else None
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def get_genres(file_path, version=None):
|
|
168
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
|
|
169
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
170
|
+
return None
|
|
171
|
+
tcon_values = metadata.get("TCON", [])
|
|
172
|
+
return tcon_values[0] if tcon_values else None
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def get_comment(file_path, version=None):
|
|
176
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
|
|
177
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
178
|
+
return None
|
|
179
|
+
comm_values = metadata.get("COMM", [])
|
|
180
|
+
return comm_values[0] if comm_values else None
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def get_track(file_path, version=None):
|
|
184
|
+
metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
|
|
185
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
186
|
+
return None
|
|
187
|
+
trck_values = metadata.get("TRCK", [])
|
|
188
|
+
return trck_values[0] if trck_values else None
|
|
@@ -0,0 +1,428 @@
|
|
|
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
|
+
}
|
|
47
|
+
else:
|
|
48
|
+
tool = "mid3v2"
|
|
49
|
+
cmd = ["mid3v2"]
|
|
50
|
+
# Map common metadata keys to mid3v2 tool arguments
|
|
51
|
+
key_mapping = {
|
|
52
|
+
"title": "--song",
|
|
53
|
+
"artist": "--artist",
|
|
54
|
+
"album": "--album",
|
|
55
|
+
"year": "--year",
|
|
56
|
+
"genre": "--genre",
|
|
57
|
+
"comment": "--comment",
|
|
58
|
+
"track": "--track",
|
|
59
|
+
"track_number": "--track",
|
|
60
|
+
"composer": "--TCOM",
|
|
61
|
+
"copyright": "--TCOP",
|
|
62
|
+
"lyrics": "--USLT",
|
|
63
|
+
"language": "--TLAN",
|
|
64
|
+
"rating": "--POPM",
|
|
65
|
+
"album_artist": "--TPE2",
|
|
66
|
+
"encoder": "--TENC",
|
|
67
|
+
"url": "--WOAR",
|
|
68
|
+
"isrc": "--TSRC",
|
|
69
|
+
"mood": "--TMOO",
|
|
70
|
+
"key": "--TKEY",
|
|
71
|
+
"publisher": "--TPUB",
|
|
72
|
+
"bpm": "--TBPM",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
metadata_added = False
|
|
76
|
+
for key, value in metadata.items():
|
|
77
|
+
if key.lower() in key_mapping:
|
|
78
|
+
cmd.extend([key_mapping[key.lower()], str(value)])
|
|
79
|
+
metadata_added = True
|
|
80
|
+
|
|
81
|
+
# Only run the tool if metadata was actually added
|
|
82
|
+
if metadata_added:
|
|
83
|
+
cmd.append(str(file_path))
|
|
84
|
+
run_external_tool(cmd, tool)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def set_max_metadata(file_path: Path) -> None:
|
|
88
|
+
from pathlib import Path
|
|
89
|
+
|
|
90
|
+
from ..common.external_tool_runner import run_script
|
|
91
|
+
|
|
92
|
+
scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
|
|
93
|
+
run_script("set-id3v2-max-metadata.sh", file_path, scripts_dir)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def set_title(file_path: Path, title: str, version: str = "2.4") -> None:
|
|
97
|
+
if version == "2.3":
|
|
98
|
+
command = ["id3v2", "--id3v2-only", "--song", title, str(file_path)]
|
|
99
|
+
run_external_tool(command, "id3v2")
|
|
100
|
+
else:
|
|
101
|
+
command = ["mid3v2", "--song", title, str(file_path)]
|
|
102
|
+
run_external_tool(command, "mid3v2")
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def set_titles(file_path: Path, titles: list[str], in_separate_frames: bool = False, version: str = "2.4"):
|
|
106
|
+
"""Set ID3v2 multiple titles using external mid3v2 tool or manual frame creation.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: Path to the audio file
|
|
110
|
+
titles: List of title strings to set
|
|
111
|
+
in_separate_frames: If True, creates multiple separate TIT2 frames (one per title)
|
|
112
|
+
using manual binary construction. If False (default), creates a single TIT2 frame
|
|
113
|
+
with multiple values using mid3v2.
|
|
114
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
115
|
+
"""
|
|
116
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TIT2", titles, in_separate_frames, version)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def set_artists(file_path: Path, artists, in_separate_frames: bool = False, version: str = "2.4") -> None:
|
|
120
|
+
if isinstance(artists, str):
|
|
121
|
+
# For string input, use external tool directly to avoid replacing entire tag
|
|
122
|
+
if version == "2.3":
|
|
123
|
+
command = ["id3v2", "--id3v2-only", "--artist", artists, str(file_path)]
|
|
124
|
+
run_external_tool(command, "id3v2")
|
|
125
|
+
else:
|
|
126
|
+
# Use mid3v2 for ID3v2.4
|
|
127
|
+
command = ["mid3v2", "--artist", artists, str(file_path)]
|
|
128
|
+
run_external_tool(command, "mid3v2")
|
|
129
|
+
else:
|
|
130
|
+
# For list input, use multiple values handling
|
|
131
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(
|
|
132
|
+
file_path, "TPE1", artists, in_separate_frames=in_separate_frames, version=version
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def set_album(file_path: Path, album: str, version: str = "2.4") -> None:
|
|
137
|
+
if version == "2.3":
|
|
138
|
+
command = ["id3v2", "--id3v2-only", "--album", album, str(file_path)]
|
|
139
|
+
run_external_tool(command, "id3v2")
|
|
140
|
+
else:
|
|
141
|
+
command = ["mid3v2", "--album", album, str(file_path)]
|
|
142
|
+
run_external_tool(command, "mid3v2")
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def set_genre(file_path: Path, genre: str, version: str = "2.4") -> None:
|
|
146
|
+
if version == "2.3":
|
|
147
|
+
command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
|
|
148
|
+
run_external_tool(command, "id3v2")
|
|
149
|
+
else:
|
|
150
|
+
command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
|
|
151
|
+
run_external_tool(command, "id3v2")
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def set_lyrics(file_path: Path, lyrics: str, version: str = "2.4") -> None:
|
|
155
|
+
if version == "2.3":
|
|
156
|
+
command = ["id3v2", "--id3v2-only", "--USLT", lyrics, str(file_path)]
|
|
157
|
+
run_external_tool(command, "id3v2")
|
|
158
|
+
else:
|
|
159
|
+
command = ["mid3v2", "--USLT", lyrics, str(file_path)]
|
|
160
|
+
run_external_tool(command, "mid3v2")
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def set_language(file_path: Path, language: str, version: str = "2.4") -> None:
|
|
164
|
+
if version == "2.3":
|
|
165
|
+
command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
|
|
166
|
+
run_external_tool(command, "id3v2")
|
|
167
|
+
else:
|
|
168
|
+
command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
|
|
169
|
+
run_external_tool(command, "id3v2")
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def set_bpm(file_path: Path, bpm: int, version: str = "2.4") -> None:
|
|
173
|
+
if version == "2.3":
|
|
174
|
+
command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
|
|
175
|
+
run_external_tool(command, "id3v2")
|
|
176
|
+
else:
|
|
177
|
+
command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
|
|
178
|
+
run_external_tool(command, "id3v2")
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def set_release_date(file_path: Path, date_str: str, version: str = "2.4") -> None:
|
|
182
|
+
"""Set ID3v2 release date using version-specific frames.
|
|
183
|
+
|
|
184
|
+
For ID3v2.3: Uses TYER (year) and TDAT (MMDD) frames
|
|
185
|
+
For ID3v2.4: Uses TDRC frame with full date
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
file_path: Path to the audio file
|
|
189
|
+
date_str: Date string in YYYY-MM-DD format
|
|
190
|
+
version: ID3v2 version to use ("2.3" or "2.4")
|
|
191
|
+
"""
|
|
192
|
+
if version == "2.3":
|
|
193
|
+
# Parse YYYY-MM-DD format
|
|
194
|
+
if len(date_str) >= 10:
|
|
195
|
+
year = date_str[:4]
|
|
196
|
+
month = date_str[5:7]
|
|
197
|
+
day = date_str[8:10]
|
|
198
|
+
# TDAT format is DDMM
|
|
199
|
+
date_ddmm = f"{day}{month}"
|
|
200
|
+
|
|
201
|
+
# Set TYER frame
|
|
202
|
+
command_year = ["id3v2", "--id3v2-only", "--TYER", year, str(file_path)]
|
|
203
|
+
run_external_tool(command_year, "id3v2")
|
|
204
|
+
|
|
205
|
+
# Set TDAT frame
|
|
206
|
+
command_date = ["id3v2", "--id3v2-only", "--TDAT", date_ddmm, str(file_path)]
|
|
207
|
+
run_external_tool(command_date, "id3v2")
|
|
208
|
+
else:
|
|
209
|
+
# Fallback for year-only dates
|
|
210
|
+
command = ["id3v2", "--id3v2-only", "--TYER", date_str, str(file_path)]
|
|
211
|
+
run_external_tool(command, "id3v2")
|
|
212
|
+
else:
|
|
213
|
+
# ID3v2.4: Use TDRC with full date
|
|
214
|
+
command = ["mid3v2", "--TDRC", date_str, str(file_path)]
|
|
215
|
+
run_external_tool(command, "mid3v2")
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _set_multiple_values_single_frame(
|
|
219
|
+
file_path: Path, frame_id: str, values: list[str], version: str = "2.4", separator: str | None = None
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Set multiple values in a single ID3v2 frame with version-specific handling.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
file_path: Path to the audio file
|
|
225
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
|
|
226
|
+
values: List of values to set in the frame
|
|
227
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
228
|
+
separator: Separator to use between values. If None, uses default behavior
|
|
229
|
+
(semicolon for ID3v2.3, null byte for ID3v2.4)
|
|
230
|
+
"""
|
|
231
|
+
# Determine separator if not provided
|
|
232
|
+
if separator is None:
|
|
233
|
+
separator = ";" if version == "2.3" else "\x00"
|
|
234
|
+
|
|
235
|
+
# Use appropriate method for all cases
|
|
236
|
+
ID3v2MetadataSetter._set_single_frame_with_id3v2(file_path, frame_id, values, version, separator)
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _set_multiple_metadata_values(
|
|
240
|
+
file_path: Path,
|
|
241
|
+
frame_id: str,
|
|
242
|
+
values: list[str],
|
|
243
|
+
in_separate_frames: bool = False,
|
|
244
|
+
version: str = "2.4",
|
|
245
|
+
separator: str | None = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Set multiple metadata values, either in separate frames or a single frame.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
file_path: Path to the audio file
|
|
251
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
|
|
252
|
+
values: List of values to set
|
|
253
|
+
in_separate_frames: If True, creates multiple separate frames. If False, creates a
|
|
254
|
+
single frame with multiple values.
|
|
255
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
256
|
+
separator: Separator to use between values when in_separate_frames=False. If None, uses default behavior.
|
|
257
|
+
"""
|
|
258
|
+
from .id3v2_metadata_deleter import ID3v2MetadataDeleter
|
|
259
|
+
|
|
260
|
+
# Delete existing frames
|
|
261
|
+
ID3v2MetadataDeleter.delete_frame(file_path, frame_id)
|
|
262
|
+
|
|
263
|
+
if in_separate_frames:
|
|
264
|
+
# Use manual binary construction to create truly separate frames
|
|
265
|
+
ID3v2MetadataSetter._create_multiple_id3v2_frames(file_path, frame_id, values, version)
|
|
266
|
+
else:
|
|
267
|
+
# Create a single frame with multiple values (version-specific handling)
|
|
268
|
+
ID3v2MetadataSetter._set_multiple_values_single_frame(file_path, frame_id, values, version, separator)
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def set_genres(file_path: Path, genres: list[str], in_separate_frames: bool = False, version: str = "2.4"):
|
|
272
|
+
"""Set ID3v2 multiple genres using external mid3v2 tool or manual frame creation.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
file_path: Path to the audio file
|
|
276
|
+
genres: List of genre strings to set
|
|
277
|
+
in_separate_frames: If True, creates multiple separate TCON frames (one per genre)
|
|
278
|
+
using manual binary construction. If False (default), creates a single TCON frame
|
|
279
|
+
with multiple values using mid3v2.
|
|
280
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
281
|
+
"""
|
|
282
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCON", genres, in_separate_frames, version)
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def set_album_artists(file_path: Path, album_artists: list[str], in_separate_frames: bool = False):
|
|
286
|
+
"""Set ID3v2 multiple album artists using external mid3v2 tool or manual frame creation.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
file_path: Path to the audio file
|
|
290
|
+
album_artists: List of album artist strings to set
|
|
291
|
+
in_separate_frames: If True, creates multiple separate TPE2 frames (one per album artist)
|
|
292
|
+
using manual binary construction. If False (default), creates a single TPE2 frame
|
|
293
|
+
with multiple values using mid3v2.
|
|
294
|
+
"""
|
|
295
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TPE2", album_artists, in_separate_frames)
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def set_composers(file_path: Path, composers: list[str], in_separate_frames: bool = False, version: str = "2.4"):
|
|
299
|
+
"""Set ID3v2 multiple composers using external mid3v2 tool or manual frame creation.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
file_path: Path to the audio file
|
|
303
|
+
composers: List of composer strings to set
|
|
304
|
+
in_separate_frames: If True, creates multiple separate TCOM frames (one per composer)
|
|
305
|
+
using manual binary construction. If False (default), creates a single TCOM frame
|
|
306
|
+
with multiple values using mid3v2.
|
|
307
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
308
|
+
"""
|
|
309
|
+
ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCOM", composers, in_separate_frames, version)
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def set_comments(file_path: Path, comments: list[str], in_separate_frames: bool = False):
|
|
313
|
+
"""Set ID3v2 multiple comments using external mid3v2 tool.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
file_path: Path to the audio file
|
|
317
|
+
comments: List of comment strings to set
|
|
318
|
+
in_separate_frames: If True, creates multiple separate COMM frames (one per comment).
|
|
319
|
+
If False (default), creates a single COMM frame with the first comment value.
|
|
320
|
+
"""
|
|
321
|
+
from .id3v2_metadata_deleter import ID3v2MetadataDeleter
|
|
322
|
+
|
|
323
|
+
# Delete existing COMM tags
|
|
324
|
+
ID3v2MetadataDeleter.delete_frame(file_path, "COMM")
|
|
325
|
+
|
|
326
|
+
if in_separate_frames:
|
|
327
|
+
# Set each comment in a separate id3v2 call to force multiple frames
|
|
328
|
+
for comment in comments:
|
|
329
|
+
command = ["id3v2", "--id3v2-only", "--comment", comment, str(file_path)]
|
|
330
|
+
run_external_tool(command, "id3v2")
|
|
331
|
+
# Set only the first comment (ID3v2 comment fields are typically single-valued)
|
|
332
|
+
elif comments:
|
|
333
|
+
command = ["id3v2", "--id3v2-only", "--comment", comments[0], str(file_path)]
|
|
334
|
+
run_external_tool(command, "id3v2")
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _create_multiple_id3v2_frames(file_path: Path, frame_id: str, texts: list[str], version: str = "2.4") -> None:
|
|
338
|
+
"""Create multiple separate ID3v2 frames using manual binary construction.
|
|
339
|
+
|
|
340
|
+
This uses the ManualID3v2FrameCreator to bypass standard tools that
|
|
341
|
+
consolidate multiple frames of the same type, allowing creation of
|
|
342
|
+
truly separate frames for testing purposes.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
file_path: Path to the audio file
|
|
346
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TPE2', 'TCON', 'TCOM')
|
|
347
|
+
texts: List of text values, one per frame
|
|
348
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
349
|
+
"""
|
|
350
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
351
|
+
|
|
352
|
+
# Create frames based on the frame type
|
|
353
|
+
if frame_id == "TPE1":
|
|
354
|
+
ManualID3v2FrameCreator.create_multiple_tpe1_frames(file_path, texts, version)
|
|
355
|
+
elif frame_id == "TPE2":
|
|
356
|
+
ManualID3v2FrameCreator.create_multiple_tpe2_frames(file_path, texts, version)
|
|
357
|
+
elif frame_id == "TCON":
|
|
358
|
+
ManualID3v2FrameCreator.create_multiple_tcon_frames(file_path, texts, version)
|
|
359
|
+
elif frame_id == "TCOM":
|
|
360
|
+
ManualID3v2FrameCreator.create_multiple_tcom_frames(file_path, texts, version)
|
|
361
|
+
else:
|
|
362
|
+
# Generic frame creation for other frame types (including TIT2)
|
|
363
|
+
creator = ManualID3v2FrameCreator()
|
|
364
|
+
frames = []
|
|
365
|
+
for text in texts:
|
|
366
|
+
frame_data = creator._create_text_frame(frame_id, text, version)
|
|
367
|
+
frames.append(frame_data)
|
|
368
|
+
creator._write_id3v2_tag(file_path, frames, version)
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _set_single_frame_with_id3v2(
|
|
372
|
+
file_path: Path, frame_id: str, alist: list[str], version: str, separator: str | None = None
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Internal helper: Create a single ID3v2 frame using appropriate tool based on version.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
file_path: Path to the audio file
|
|
378
|
+
frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
|
|
379
|
+
alist: List of text values for the frame
|
|
380
|
+
version: ID3v2 version to use (e.g., "2.3", "2.4")
|
|
381
|
+
separator: Separator to use between values. If None, uses default.
|
|
382
|
+
"""
|
|
383
|
+
# Determine separator if not provided
|
|
384
|
+
if separator is None:
|
|
385
|
+
separator = ";" if version == "2.3" else "\x00"
|
|
386
|
+
|
|
387
|
+
# Combine values with the appropriate separator
|
|
388
|
+
combined_text = separator.join(alist) if len(alist) > 1 else alist[0] if alist else ""
|
|
389
|
+
|
|
390
|
+
# Check if we have null bytes - if so, use manual frame creator for ID3v2.4
|
|
391
|
+
if version == "2.4" and "\x00" in combined_text:
|
|
392
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
393
|
+
|
|
394
|
+
creator = ManualID3v2FrameCreator()
|
|
395
|
+
frame_data = creator._create_text_frame(frame_id, combined_text, version)
|
|
396
|
+
creator._write_id3v2_tag(file_path, [frame_data], version)
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Map frame IDs to tool flags
|
|
400
|
+
flag_mapping = {
|
|
401
|
+
"TCON": "--genre",
|
|
402
|
+
"TIT2": "--song",
|
|
403
|
+
"TPE1": "--artist",
|
|
404
|
+
"TALB": "--album",
|
|
405
|
+
"TDRC": "--year",
|
|
406
|
+
"TRCK": "--track",
|
|
407
|
+
"COMM": "--comment",
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
flag = flag_mapping.get(frame_id, f"--{frame_id.lower()}")
|
|
411
|
+
|
|
412
|
+
if version == "2.3":
|
|
413
|
+
# Use id3v2 for ID3v2.3
|
|
414
|
+
command = ["id3v2", "--id3v2-only", flag, combined_text, str(file_path)]
|
|
415
|
+
run_external_tool(command, "id3v2")
|
|
416
|
+
else:
|
|
417
|
+
# Use mid3v2 for ID3v2.4
|
|
418
|
+
command = ["mid3v2", flag, combined_text, str(file_path)]
|
|
419
|
+
run_external_tool(command, "mid3v2")
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def write_tpe1_with_encoding(file_path: Path, text: str, encoding: int) -> None:
|
|
423
|
+
"""Write a TPE1 frame with specific encoding for testing purposes."""
|
|
424
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
425
|
+
|
|
426
|
+
creator = ManualID3v2FrameCreator()
|
|
427
|
+
frame_data = creator._create_text_frame("TPE1", text, "2.4", encoding=encoding)
|
|
428
|
+
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"]
|