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,512 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
|
|
3
|
+
from mutagen._file import FileType as MutagenMetadata
|
|
4
|
+
|
|
5
|
+
from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
|
|
6
|
+
|
|
7
|
+
from ..._audio_file import _AudioFile
|
|
8
|
+
from ...exceptions import FileCorruptedError, MetadataFieldNotSupportedByMetadataFormatError
|
|
9
|
+
from ...utils.types import RawMetadataDict, RawMetadataKey, UnifiedMetadata, UnifiedMetadataValue
|
|
10
|
+
from .._MetadataManager import _MetadataManager
|
|
11
|
+
from ._constants import ID3V1_MIN_COMMENT_LENGTH_TO_CHECK_TRACK_NUMBER, ID3V1_TAG_SIZE
|
|
12
|
+
from .id3v1_raw_metadata import Id3v1RawMetadata
|
|
13
|
+
from .id3v1_raw_metadata_key import Id3v1RawMetadataKey
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Id3v1Manager(_MetadataManager):
|
|
17
|
+
"""Manages ID3v1 metadata for audio files.
|
|
18
|
+
|
|
19
|
+
ID3v1 is a simple, legacy metadata format with significant limitations:
|
|
20
|
+
- Fixed 128-byte block at end of file
|
|
21
|
+
- No Unicode support (Latin-1 only)
|
|
22
|
+
- Limited field lengths (30 chars)
|
|
23
|
+
- No support for:
|
|
24
|
+
- Album artist
|
|
25
|
+
- BPM
|
|
26
|
+
- Ratings
|
|
27
|
+
- Language
|
|
28
|
+
- Custom genres
|
|
29
|
+
- Multiple genres
|
|
30
|
+
- Multiple artists
|
|
31
|
+
...
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def _get_formatted_metadata_format_name(self) -> str:
|
|
35
|
+
"""Get the formatted metadata format name.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The formatted format name 'ID3v1'
|
|
39
|
+
"""
|
|
40
|
+
return "ID3v1"
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
- Supports both reading and writing metadata using direct file manipulation.
|
|
44
|
+
|
|
45
|
+
Format Structure:
|
|
46
|
+
- Bytes 0-2: "TAG" identifier
|
|
47
|
+
- Bytes 3-32: Title (30 chars)
|
|
48
|
+
- Bytes 33-62: Artist (30 chars)
|
|
49
|
+
- Bytes 63-92: Album (30 chars)
|
|
50
|
+
- Bytes 93-96: Release year (4 chars)
|
|
51
|
+
- Bytes 97-126: Comment (28 chars in ID3v1.1, 30 chars in ID3v1)
|
|
52
|
+
- Byte 125: Always 0 in ID3v1.1 to indicate track number presence
|
|
53
|
+
- Byte 126: Track number in ID3v1.1 (1-255, 0 = not set)
|
|
54
|
+
- Byte 127: Genre code (0-255)
|
|
55
|
+
|
|
56
|
+
Note: ID3v1.1 extends ID3v1 by using the last two bytes of the comment
|
|
57
|
+
field to store the track number. If byte 125 is 0 and byte 126 is not 0,
|
|
58
|
+
then byte 126 contains the track number (1-255).
|
|
59
|
+
|
|
60
|
+
Note 2: The genre code is an index into a predefined list of genres.
|
|
61
|
+
|
|
62
|
+
Supported File Formats:
|
|
63
|
+
- MP3: Native ID3v1 format, optimal support
|
|
64
|
+
- FLAC: Some FLAC files may contain ID3v1 tags (not optimal but supported)
|
|
65
|
+
- WAV: Some WAV files may contain ID3v1 tags (not optimal but supported)
|
|
66
|
+
|
|
67
|
+
While ID3v1 is natively designed for MP3 files, this manager supports reading
|
|
68
|
+
ID3v1 tags from FLAC and WAV files when present, even though it's not the
|
|
69
|
+
optimal metadata format for these file types.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, audio_file: _AudioFile):
|
|
73
|
+
metadata_keys_direct_map_read: dict = {
|
|
74
|
+
UnifiedMetadataKey.TITLE: Id3v1RawMetadataKey.TITLE,
|
|
75
|
+
UnifiedMetadataKey.ARTISTS: Id3v1RawMetadataKey.ARTISTS_NAMES_STR,
|
|
76
|
+
UnifiedMetadataKey.ALBUM: Id3v1RawMetadataKey.ALBUM,
|
|
77
|
+
UnifiedMetadataKey.RELEASE_DATE: Id3v1RawMetadataKey.YEAR,
|
|
78
|
+
UnifiedMetadataKey.TRACK_NUMBER: Id3v1RawMetadataKey.TRACK_NUMBER,
|
|
79
|
+
UnifiedMetadataKey.COMMENT: Id3v1RawMetadataKey.COMMENT,
|
|
80
|
+
UnifiedMetadataKey.GENRES_NAMES: None,
|
|
81
|
+
}
|
|
82
|
+
metadata_keys_direct_map_write: dict = {
|
|
83
|
+
UnifiedMetadataKey.TITLE: Id3v1RawMetadataKey.TITLE,
|
|
84
|
+
UnifiedMetadataKey.ARTISTS: Id3v1RawMetadataKey.ARTISTS_NAMES_STR,
|
|
85
|
+
UnifiedMetadataKey.ALBUM: Id3v1RawMetadataKey.ALBUM,
|
|
86
|
+
UnifiedMetadataKey.RELEASE_DATE: Id3v1RawMetadataKey.YEAR,
|
|
87
|
+
UnifiedMetadataKey.TRACK_NUMBER: Id3v1RawMetadataKey.TRACK_NUMBER,
|
|
88
|
+
UnifiedMetadataKey.COMMENT: Id3v1RawMetadataKey.COMMENT,
|
|
89
|
+
UnifiedMetadataKey.GENRES_NAMES: None, # Handled indirectly
|
|
90
|
+
}
|
|
91
|
+
super().__init__(
|
|
92
|
+
audio_file=audio_file,
|
|
93
|
+
metadata_keys_direct_map_read=metadata_keys_direct_map_read,
|
|
94
|
+
metadata_keys_direct_map_write=metadata_keys_direct_map_write,
|
|
95
|
+
update_using_mutagen_metadata=False, # Use direct file manipulation for ID3v1
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def find_safe_separator(values: list[str]) -> str:
|
|
100
|
+
"""ID3v1-specific separator logic optimized for legacy format constraints.
|
|
101
|
+
|
|
102
|
+
Only single-character separators are used to maximize space efficiency in 30-char fields.
|
|
103
|
+
Tries to use the safest available single-character separator that does not appear in any value.
|
|
104
|
+
|
|
105
|
+
Separator priority for ID3v1 (all single-character, Latin-1 compatible):
|
|
106
|
+
1. ',' (comma) - Standard, readable
|
|
107
|
+
2. ';' (semicolon) - Common alternative
|
|
108
|
+
3. '|' (pipe) - Less common
|
|
109
|
+
4. '·' (middle dot) - Unicode but Latin-1 safe
|
|
110
|
+
5. '/' (slash) - Last resort, may be confusing
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
values: List of string values to check for separator conflicts
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The safest available single-character separator, prioritizing readability and space efficiency
|
|
117
|
+
"""
|
|
118
|
+
# ID3v1 compatible single-character separators in order of preference
|
|
119
|
+
id3v1_separators = [",", ";", "|", "·", "/"]
|
|
120
|
+
|
|
121
|
+
# Find the first separator that doesn't appear in any value
|
|
122
|
+
for sep in id3v1_separators:
|
|
123
|
+
if not any(sep in value for value in values):
|
|
124
|
+
return sep
|
|
125
|
+
|
|
126
|
+
# If all separators conflict, use comma as fallback (established convention)
|
|
127
|
+
return ","
|
|
128
|
+
|
|
129
|
+
def _extract_mutagen_metadata(self) -> Id3v1RawMetadata:
|
|
130
|
+
try:
|
|
131
|
+
return Id3v1RawMetadata(fileobj=self.audio_file.file_path)
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
msg = f"Failed to extract ID3v1 metadata: {exc}"
|
|
134
|
+
raise FileCorruptedError(msg) from exc
|
|
135
|
+
|
|
136
|
+
def _convert_raw_mutagen_metadata_to_dict_with_potential_duplicate_keys(
|
|
137
|
+
self, raw_mutagen_metadata: MutagenMetadata
|
|
138
|
+
) -> RawMetadataDict:
|
|
139
|
+
raw_metadata_id3v1: Id3v1RawMetadata = cast(Id3v1RawMetadata, raw_mutagen_metadata)
|
|
140
|
+
if not raw_metadata_id3v1.tags:
|
|
141
|
+
return {}
|
|
142
|
+
|
|
143
|
+
# Create a mapping of string values to enum members with proper value types
|
|
144
|
+
result: RawMetadataDict = {} # type: ignore[unreachable]
|
|
145
|
+
for key, value in raw_metadata_id3v1.tags.items():
|
|
146
|
+
# Skip empty values
|
|
147
|
+
if not value:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Find the matching enum member by its value
|
|
151
|
+
for enum_member in Id3v1RawMetadataKey:
|
|
152
|
+
if enum_member == key:
|
|
153
|
+
result[enum_member] = value
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def _get_undirectly_mapped_metadata_value_from_raw_clean_metadata(
|
|
157
|
+
self, raw_clean_metadata_uppercase_keys: RawMetadataDict, unified_metadata_key: UnifiedMetadataKey
|
|
158
|
+
) -> UnifiedMetadataValue:
|
|
159
|
+
if unified_metadata_key == UnifiedMetadataKey.GENRES_NAMES:
|
|
160
|
+
return self._get_genre_name_from_raw_clean_metadata_id3v1(
|
|
161
|
+
raw_clean_metadata=raw_clean_metadata_uppercase_keys,
|
|
162
|
+
raw_metadata_ket=Id3v1RawMetadataKey.GENRE_CODE_OR_NAME,
|
|
163
|
+
)
|
|
164
|
+
msg = f"{unified_metadata_key} metadata is not undirectly handled"
|
|
165
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
166
|
+
|
|
167
|
+
def _update_undirectly_mapped_metadata(
|
|
168
|
+
self,
|
|
169
|
+
raw_mutagen_metadata: MutagenMetadata,
|
|
170
|
+
app_metadata_value: UnifiedMetadataValue,
|
|
171
|
+
unified_metadata_key: UnifiedMetadataKey,
|
|
172
|
+
) -> None:
|
|
173
|
+
# Ensure tags exist
|
|
174
|
+
if not hasattr(raw_mutagen_metadata, "tags") or raw_mutagen_metadata.tags is None:
|
|
175
|
+
raw_mutagen_metadata.tags = {}
|
|
176
|
+
# Type narrowing: mypy now knows tags is not None after the assignment above
|
|
177
|
+
tags: dict[Any, Any] = cast(dict[Any, Any], raw_mutagen_metadata.tags)
|
|
178
|
+
|
|
179
|
+
if unified_metadata_key == UnifiedMetadataKey.GENRES_NAMES:
|
|
180
|
+
# Handle both single string and list values gracefully
|
|
181
|
+
if isinstance(app_metadata_value, list):
|
|
182
|
+
app_metadata_value = app_metadata_value[0] if app_metadata_value else None
|
|
183
|
+
|
|
184
|
+
if app_metadata_value:
|
|
185
|
+
# Convert genre name to genre code
|
|
186
|
+
genre_code = self._convert_genre_name_to_code(str(app_metadata_value))
|
|
187
|
+
if genre_code is not None:
|
|
188
|
+
tags[Id3v1RawMetadataKey.GENRE_CODE_OR_NAME] = [str(genre_code)]
|
|
189
|
+
else:
|
|
190
|
+
msg = f"{unified_metadata_key} metadata is not undirectly handled"
|
|
191
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
192
|
+
|
|
193
|
+
def _update_formatted_value_in_raw_mutagen_metadata(
|
|
194
|
+
self,
|
|
195
|
+
raw_mutagen_metadata: MutagenMetadata,
|
|
196
|
+
raw_metadata_key: RawMetadataKey,
|
|
197
|
+
app_metadata_value: UnifiedMetadataValue,
|
|
198
|
+
) -> None:
|
|
199
|
+
# Ensure tags exist
|
|
200
|
+
if not hasattr(raw_mutagen_metadata, "tags") or raw_mutagen_metadata.tags is None:
|
|
201
|
+
raw_mutagen_metadata.tags = {}
|
|
202
|
+
# Type narrowing: mypy now knows tags is not None after the assignment above
|
|
203
|
+
tags: dict[Any, Any] = cast(dict[Any, Any], raw_mutagen_metadata.tags)
|
|
204
|
+
|
|
205
|
+
# If value is None, remove the field (delete from tags)
|
|
206
|
+
if app_metadata_value is None:
|
|
207
|
+
if raw_metadata_key in tags:
|
|
208
|
+
del tags[raw_metadata_key]
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Convert and truncate the value according to ID3v1 constraints
|
|
212
|
+
if raw_metadata_key == Id3v1RawMetadataKey.TITLE:
|
|
213
|
+
value = self._truncate_string(str(app_metadata_value), 30)
|
|
214
|
+
elif raw_metadata_key == Id3v1RawMetadataKey.ARTISTS_NAMES_STR:
|
|
215
|
+
# Convert list to string using smart separator logic and truncate
|
|
216
|
+
if isinstance(app_metadata_value, list):
|
|
217
|
+
if app_metadata_value:
|
|
218
|
+
separator = self.find_safe_separator(app_metadata_value)
|
|
219
|
+
artists_str = separator.join(app_metadata_value)
|
|
220
|
+
else:
|
|
221
|
+
# If no valid values, set to empty string
|
|
222
|
+
artists_str = ""
|
|
223
|
+
else:
|
|
224
|
+
artists_str = str(app_metadata_value)
|
|
225
|
+
value = self._truncate_string(artists_str, 30)
|
|
226
|
+
elif raw_metadata_key == Id3v1RawMetadataKey.ALBUM:
|
|
227
|
+
value = self._truncate_string(str(app_metadata_value), 30)
|
|
228
|
+
elif raw_metadata_key == Id3v1RawMetadataKey.YEAR:
|
|
229
|
+
value = self._truncate_string(str(app_metadata_value), 4)
|
|
230
|
+
elif raw_metadata_key == Id3v1RawMetadataKey.TRACK_NUMBER:
|
|
231
|
+
# Convert to int and validate range, parsing strings like "5/12"
|
|
232
|
+
if isinstance(app_metadata_value, str):
|
|
233
|
+
import re
|
|
234
|
+
|
|
235
|
+
if re.match(r"^\d+([-/]\d*)?$", app_metadata_value):
|
|
236
|
+
track_match = re.match(r"(\d+)", app_metadata_value)
|
|
237
|
+
track_num = 0 if track_match is None else int(track_match.group(1))
|
|
238
|
+
else:
|
|
239
|
+
track_num = 0
|
|
240
|
+
elif isinstance(app_metadata_value, int | float):
|
|
241
|
+
track_num = int(float(app_metadata_value))
|
|
242
|
+
else:
|
|
243
|
+
track_num = 0
|
|
244
|
+
value = str(max(0, min(255, track_num)))
|
|
245
|
+
elif raw_metadata_key == Id3v1RawMetadataKey.COMMENT:
|
|
246
|
+
value = self._truncate_string(str(app_metadata_value), 28) # 28 for ID3v1.1 with track number
|
|
247
|
+
else:
|
|
248
|
+
value = str(app_metadata_value)
|
|
249
|
+
|
|
250
|
+
tags[raw_metadata_key] = [value]
|
|
251
|
+
|
|
252
|
+
def _update_not_using_mutagen_metadata(self, unified_metadata: UnifiedMetadata) -> None:
|
|
253
|
+
"""Update ID3v1 metadata using direct file manipulation."""
|
|
254
|
+
# Validate that all fields are supported by ID3v1
|
|
255
|
+
if self.metadata_keys_direct_map_write is None:
|
|
256
|
+
msg = "metadata_keys_direct_map_write is None"
|
|
257
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
258
|
+
for unified_metadata_key in unified_metadata:
|
|
259
|
+
if unified_metadata_key not in self.metadata_keys_direct_map_write:
|
|
260
|
+
metadata_format_name = self._get_formatted_metadata_format_name()
|
|
261
|
+
msg = f"{unified_metadata_key} metadata not supported by {metadata_format_name} format"
|
|
262
|
+
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
263
|
+
|
|
264
|
+
# Read the entire file
|
|
265
|
+
self.audio_file.seek(0)
|
|
266
|
+
file_data = bytearray(self.audio_file.read())
|
|
267
|
+
|
|
268
|
+
# Create ID3v1 tag data
|
|
269
|
+
tag_data = self._create_id3v1_tag_data(unified_metadata)
|
|
270
|
+
|
|
271
|
+
# Find and remove existing ID3v1 tag if present
|
|
272
|
+
self._remove_existing_id3v1_tag(file_data)
|
|
273
|
+
|
|
274
|
+
# Append new ID3v1 tag
|
|
275
|
+
file_data.extend(tag_data)
|
|
276
|
+
|
|
277
|
+
# Write back to file
|
|
278
|
+
self.audio_file.write(file_data)
|
|
279
|
+
|
|
280
|
+
def _create_id3v1_tag_data(self, unified_metadata: UnifiedMetadata) -> bytes:
|
|
281
|
+
"""Create 128-byte ID3v1 tag data from app metadata."""
|
|
282
|
+
# Initialize with null bytes
|
|
283
|
+
tag_data = bytearray(128)
|
|
284
|
+
|
|
285
|
+
# TAG identifier (bytes 0-2)
|
|
286
|
+
tag_data[0:3] = b"TAG"
|
|
287
|
+
|
|
288
|
+
# Title (bytes 3-32, 30 chars max)
|
|
289
|
+
title = unified_metadata.get(UnifiedMetadataKey.TITLE)
|
|
290
|
+
if title is not None:
|
|
291
|
+
title_bytes = self._truncate_string(str(title), 30).encode("latin-1", errors="ignore")
|
|
292
|
+
tag_data[3 : 3 + len(title_bytes)] = title_bytes
|
|
293
|
+
|
|
294
|
+
# Artist (bytes 33-62, 30 chars max)
|
|
295
|
+
artists = unified_metadata.get(UnifiedMetadataKey.ARTISTS)
|
|
296
|
+
if artists is not None:
|
|
297
|
+
if isinstance(artists, list):
|
|
298
|
+
separator = self.find_safe_separator(artists)
|
|
299
|
+
artist_str = separator.join(artists)
|
|
300
|
+
else:
|
|
301
|
+
artist_str = str(artists)
|
|
302
|
+
artist_bytes = self._truncate_string(artist_str, 30).encode("latin-1", errors="ignore")
|
|
303
|
+
tag_data[33 : 33 + len(artist_bytes)] = artist_bytes
|
|
304
|
+
|
|
305
|
+
# Album (bytes 63-92, 30 chars max)
|
|
306
|
+
album = unified_metadata.get(UnifiedMetadataKey.ALBUM)
|
|
307
|
+
if album is not None:
|
|
308
|
+
album_bytes = self._truncate_string(str(album), 30).encode("latin-1", errors="ignore")
|
|
309
|
+
tag_data[63 : 63 + len(album_bytes)] = album_bytes
|
|
310
|
+
|
|
311
|
+
# Year (bytes 93-96, 4 chars max)
|
|
312
|
+
year = unified_metadata.get(UnifiedMetadataKey.RELEASE_DATE)
|
|
313
|
+
if year is not None:
|
|
314
|
+
year_bytes = self._truncate_string(str(year), 4).encode("latin-1", errors="ignore")
|
|
315
|
+
tag_data[93 : 93 + len(year_bytes)] = year_bytes
|
|
316
|
+
|
|
317
|
+
# Comment and track number (bytes 97-126, 28 chars for comment + 2 for track)
|
|
318
|
+
comment = unified_metadata.get(UnifiedMetadataKey.COMMENT)
|
|
319
|
+
if comment is not None:
|
|
320
|
+
comment_bytes = self._truncate_string(str(comment), 28).encode("latin-1", errors="ignore")
|
|
321
|
+
tag_data[97 : 97 + len(comment_bytes)] = comment_bytes
|
|
322
|
+
|
|
323
|
+
# Track number (bytes 125-126 for ID3v1.1)
|
|
324
|
+
track_number = unified_metadata.get(UnifiedMetadataKey.TRACK_NUMBER)
|
|
325
|
+
if track_number is not None:
|
|
326
|
+
if isinstance(track_number, str):
|
|
327
|
+
import re
|
|
328
|
+
|
|
329
|
+
if re.match(r"^\d+([-/]\d*)?$", track_number):
|
|
330
|
+
track_match = re.match(r"(\d+)", track_number)
|
|
331
|
+
track_num = 0 if track_match is None else int(track_match.group(1))
|
|
332
|
+
else:
|
|
333
|
+
track_num = 0
|
|
334
|
+
elif isinstance(track_number, int | float):
|
|
335
|
+
track_num = int(float(track_number))
|
|
336
|
+
else:
|
|
337
|
+
track_num = 0
|
|
338
|
+
track_num = max(0, min(255, track_num))
|
|
339
|
+
if track_num > 0:
|
|
340
|
+
tag_data[125] = 0 # Null byte to indicate track number presence
|
|
341
|
+
tag_data[126] = track_num
|
|
342
|
+
|
|
343
|
+
# Genre (byte 127)
|
|
344
|
+
genre_name = unified_metadata.get(UnifiedMetadataKey.GENRES_NAMES)
|
|
345
|
+
if genre_name is not None:
|
|
346
|
+
# Handle both single string and list values gracefully
|
|
347
|
+
if isinstance(genre_name, list):
|
|
348
|
+
genre_name = genre_name[0] if genre_name else None
|
|
349
|
+
|
|
350
|
+
if genre_name:
|
|
351
|
+
genre_code = self._convert_genre_name_to_code(str(genre_name))
|
|
352
|
+
if genre_code is not None:
|
|
353
|
+
tag_data[127] = genre_code
|
|
354
|
+
else:
|
|
355
|
+
tag_data[127] = 255 # Unknown genre
|
|
356
|
+
else:
|
|
357
|
+
# Genre is explicitly set to None/empty - mark as undefined for deletion
|
|
358
|
+
tag_data[127] = 255 # Undefined genre
|
|
359
|
+
else:
|
|
360
|
+
# Genre is not specified - mark as undefined for deletion
|
|
361
|
+
tag_data[127] = 255 # Undefined genre
|
|
362
|
+
|
|
363
|
+
return bytes(tag_data)
|
|
364
|
+
|
|
365
|
+
def _remove_existing_id3v1_tag(self, file_data: bytearray) -> bool:
|
|
366
|
+
"""Remove existing ID3v1 tag from file data if present.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
bool: True if a tag was removed, False otherwise
|
|
370
|
+
"""
|
|
371
|
+
if len(file_data) >= ID3V1_TAG_SIZE:
|
|
372
|
+
# Check if last 128 bytes contain ID3v1 tag
|
|
373
|
+
last_128 = file_data[-ID3V1_TAG_SIZE:]
|
|
374
|
+
if last_128[:3] == b"TAG":
|
|
375
|
+
# Remove the last 128 bytes
|
|
376
|
+
del file_data[-ID3V1_TAG_SIZE:]
|
|
377
|
+
return True
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def _truncate_string(self, text: str, max_length: int) -> str:
|
|
381
|
+
"""Truncate string to maximum length, handling encoding properly."""
|
|
382
|
+
if len(text) <= max_length:
|
|
383
|
+
return text
|
|
384
|
+
return text[:max_length]
|
|
385
|
+
|
|
386
|
+
def _convert_genre_name_to_code(self, genre_name: str) -> int | None:
|
|
387
|
+
"""Convert genre name to ID3v1 genre code."""
|
|
388
|
+
from .._MetadataManager import ID3V1_GENRE_CODE_MAP
|
|
389
|
+
|
|
390
|
+
# First try exact match
|
|
391
|
+
for code, name in ID3V1_GENRE_CODE_MAP.items():
|
|
392
|
+
if name and name.lower() == genre_name.lower():
|
|
393
|
+
return code
|
|
394
|
+
|
|
395
|
+
# Try partial match
|
|
396
|
+
for code, name in ID3V1_GENRE_CODE_MAP.items():
|
|
397
|
+
if name and genre_name.lower() in name.lower():
|
|
398
|
+
return code
|
|
399
|
+
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
def delete_metadata(self) -> bool:
|
|
403
|
+
"""Delete ID3v1 metadata from the audio file.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
bool: True if metadata was successfully deleted, False otherwise
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
# Read the entire file
|
|
410
|
+
self.audio_file.seek(0)
|
|
411
|
+
file_data = bytearray(self.audio_file.read())
|
|
412
|
+
|
|
413
|
+
# Remove existing ID3v1 tag if present
|
|
414
|
+
if self._remove_existing_id3v1_tag(file_data):
|
|
415
|
+
# Write back to file
|
|
416
|
+
self.audio_file.write(file_data)
|
|
417
|
+
return True
|
|
418
|
+
except Exception:
|
|
419
|
+
return False
|
|
420
|
+
else:
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
def get_header_info(self) -> dict:
|
|
424
|
+
try:
|
|
425
|
+
if self.raw_mutagen_metadata is None:
|
|
426
|
+
self.raw_mutagen_metadata = self._extract_mutagen_metadata()
|
|
427
|
+
|
|
428
|
+
if not self.raw_mutagen_metadata:
|
|
429
|
+
return {
|
|
430
|
+
"present": False,
|
|
431
|
+
"position": "end_of_file",
|
|
432
|
+
"size_bytes": 0,
|
|
433
|
+
"version": None,
|
|
434
|
+
"has_track_number": False,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Check if ID3v1 tag is present
|
|
438
|
+
present = hasattr(self.raw_mutagen_metadata, "tags") and self.raw_mutagen_metadata.tags is not None
|
|
439
|
+
|
|
440
|
+
if not present:
|
|
441
|
+
return {
|
|
442
|
+
"present": False,
|
|
443
|
+
"position": "end_of_file",
|
|
444
|
+
"size_bytes": 0,
|
|
445
|
+
"version": None,
|
|
446
|
+
"has_track_number": False,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# Determine version (ID3v1 or ID3v1.1)
|
|
450
|
+
version = "1.0"
|
|
451
|
+
has_track_number = False
|
|
452
|
+
|
|
453
|
+
# Check if track number is present (ID3v1.1 feature)
|
|
454
|
+
# We already know tags is not None from the present check above
|
|
455
|
+
tags = cast(dict[Any, Any], self.raw_mutagen_metadata.tags)
|
|
456
|
+
comment = tags.get("COMMENT", [""])[0]
|
|
457
|
+
if (
|
|
458
|
+
len(comment) >= ID3V1_MIN_COMMENT_LENGTH_TO_CHECK_TRACK_NUMBER
|
|
459
|
+
and comment[-2] == "\x00"
|
|
460
|
+
and comment[-1] != "\x00"
|
|
461
|
+
):
|
|
462
|
+
version = "1.1"
|
|
463
|
+
has_track_number = True
|
|
464
|
+
except Exception:
|
|
465
|
+
return {
|
|
466
|
+
"present": False,
|
|
467
|
+
"position": "end_of_file",
|
|
468
|
+
"size_bytes": 0,
|
|
469
|
+
"version": None,
|
|
470
|
+
"has_track_number": False,
|
|
471
|
+
}
|
|
472
|
+
else:
|
|
473
|
+
return {
|
|
474
|
+
"present": True,
|
|
475
|
+
"position": "end_of_file",
|
|
476
|
+
"size_bytes": 128,
|
|
477
|
+
"version": version,
|
|
478
|
+
"has_track_number": has_track_number,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
def get_raw_metadata_info(self) -> dict:
|
|
482
|
+
"""Get raw ID3v1 metadata information."""
|
|
483
|
+
try:
|
|
484
|
+
if self.raw_mutagen_metadata is None:
|
|
485
|
+
self.raw_mutagen_metadata = self._extract_mutagen_metadata()
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
not self.raw_mutagen_metadata
|
|
489
|
+
or not hasattr(self.raw_mutagen_metadata, "tags")
|
|
490
|
+
or self.raw_mutagen_metadata.tags is None
|
|
491
|
+
):
|
|
492
|
+
return {"raw_data": None, "parsed_fields": {}, "frames": {}, "comments": {}, "chunk_structure": {}}
|
|
493
|
+
|
|
494
|
+
# Get parsed fields using unified metadata keys
|
|
495
|
+
parsed_fields: dict[str, Any] = {}
|
|
496
|
+
# We already checked tags exist and is not None above
|
|
497
|
+
tags = cast(dict[Any, Any], self.raw_mutagen_metadata.tags)
|
|
498
|
+
# Map raw mutagen keys to unified metadata keys
|
|
499
|
+
for unified_key, raw_key in self.metadata_keys_direct_map_read.items():
|
|
500
|
+
if raw_key and raw_key in tags:
|
|
501
|
+
value = tags[raw_key]
|
|
502
|
+
parsed_fields[unified_key] = value[0] if value else ""
|
|
503
|
+
except Exception:
|
|
504
|
+
return {"raw_data": None, "parsed_fields": {}, "frames": {}, "comments": {}, "chunk_structure": {}}
|
|
505
|
+
else:
|
|
506
|
+
return {
|
|
507
|
+
"raw_data": None, # ID3v1 is 128 bytes at end of file
|
|
508
|
+
"parsed_fields": parsed_fields,
|
|
509
|
+
"frames": {},
|
|
510
|
+
"comments": {},
|
|
511
|
+
"chunk_structure": {},
|
|
512
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ID3v1 metadata management."""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Constants for ID3v1 format."""
|
|
2
|
+
|
|
3
|
+
ID3V1_TAG_SIZE = 128
|
|
4
|
+
ID3V1_COMMENT_FIELD_SIZE = 30
|
|
5
|
+
ID3V1_TRACK_NUMBER_POSITION = 28
|
|
6
|
+
ID3V1_TRACK_NUMBER_VALUE_POSITION = 29
|
|
7
|
+
ID3V1_MIN_COMMENT_LENGTH_FOR_TRACK_NUMBER = 30
|
|
8
|
+
ID3V1_MIN_COMMENT_LENGTH_TO_CHECK_TRACK_NUMBER = 2
|