audiometa-python 0.9.0__tar.gz → 0.11.0__tar.gz
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_python-0.9.0/audiometa_python.egg-info → audiometa_python-0.11.0}/PKG-INFO +1 -1
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/__init__.py +10 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/cli.py +13 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_MetadataManager.py +42 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/id3v2/_Id3v2Manager.py +81 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/riff/_RiffManager.py +26 -1
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/riff/_riff_bext_chunk.py +28 -8
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +4 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v2/id3v2_frame_manual_creator.py +54 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +73 -1
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/riff/riff_manual_metadata_creator.py +12 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/riff/riff_metadata_setter.py +20 -1
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/vorbis/vorbis_metadata_setter.py +1 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_comprehensive.py +11 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_comprehensive.py +12 -0
- audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/musicbrainz_trackid/test_deleting.py +37 -0
- audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/musicbrainz_trackid/test_reading.py +91 -0
- audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/musicbrainz_trackid/test_writing.py +161 -0
- audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/originator/test_deleting.py +50 -0
- audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/originator/test_reading.py +37 -0
- audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/originator/test_writing.py +60 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/__init__.py +0 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/musicbrainz_trackid/__init__.py +0 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/musicbrainz_trackid/test_musicbrainz_trackid_format_validation.py +111 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/musicbrainz_trackid/test_musicbrainz_trackid_type_validation.py +32 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/__init__.py +0 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/__init__.py +0 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/__init__.py +0 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/release_date/__init__.py +0 -0
- audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/track_number/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/unified_metadata_key.py +4 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0/audiometa_python.egg-info}/PKG-INFO +1 -1
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa_python.egg-info/SOURCES.txt +20 -5
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/pyproject.toml +1 -1
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/LICENSE +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/README.md +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/__main__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/_audio_file.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/exceptions.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/_RatingSupportingMetadataManager.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/id3v2/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/id3v2/_id3v2_constants.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/riff/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/riff/_riff_constants.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/riff/_riff_file_structure.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/riff/_riff_info_chunk.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/vorbis/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/_rating_supporting/vorbis/_vorbis_constants.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/id3v1/_Id3v1Manager.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/id3v1/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/id3v1/_constants.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/id3v1/id3v1_raw_metadata.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/manager/id3v1/id3v1_raw_metadata_key.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/assets/create_test_files.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/common/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/common/audio_file_creator.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/common/external_tool_runner.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v1/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v1/id3v1_header_verifier.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v1/id3v1_metadata_deleter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v1/id3v1_metadata_getter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v1/id3v1_metadata_setter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v2/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v2/id3v2_header_verifier.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v2/id3v2_metadata_deleter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/id3v2/id3v2_metadata_getter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/riff/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/riff/riff_header_verifier.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/riff/riff_metadata_deleter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/riff/riff_metadata_getter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/scripts/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/technical_info_inspector.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/temp_file_with_metadata.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/vorbis/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/vorbis/vorbis_header_verifier.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/vorbis/vorbis_metadata_deleter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/helpers/vorbis/vorbis_metadata_getter.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/conftest.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_command_structure_errors.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_file_access_errors.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_format_output_errors.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_input_validation_errors.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_missing_fields_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_multiple_files_errors.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_rating_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/error_handling/test_year_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_basic.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_formats.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_metadata_content.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_multiple_files.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_options.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/read/test_unified.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/test_delete.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/test_formatting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/test_help.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_basic.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_force_format.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_integer_fields.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_list_fields.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_rating.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_string_fields.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/cli/write/test_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/scenarios/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/scenarios/test_user_scenarios.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/workflows/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/workflows/test_core_workflows.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/workflows/test_deletion_workflows.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/workflows/test_error_handling_workflows.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/workflows/test_format_specific_workflows.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/e2e/workflows/test_rating_workflows.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/flac/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/flac/test_flac_delete_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/flac/test_flac_reading_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/flac/test_flac_reading_field.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/flac/test_flac_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/mp3/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/mp3/test_mp3_delete_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_field.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/mp3/test_mp3_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/wav/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/wav/test_wav_delete_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/wav/test_wav_reading_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/wav/test_wav_reading_field.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/audio_format/wav/test_wav_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/conftest.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/delete_all_metadata/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/delete_all_metadata/test_audio_format_all.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/delete_all_metadata/test_audio_format_header_deletion.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/delete_all_metadata/test_basic_functionality.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/delete_all_metadata/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/encoding/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/encoding/test_encoding.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/encoding/test_special_characters_edge_cases.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_audio_formats.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_consistency.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_edge_cases.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_options.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_performance.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_riff_bext.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/get_full_metadata/test_structure.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album_artists/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album_artists/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album_artists/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/album_artists/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/artists/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/artists/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/artists/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/artists/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/bpm/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/bpm/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/bpm/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/bpm/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/comment/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/comment/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/comment/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/comment/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/composer/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/composer/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/composer/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/composer/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/copyright/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/copyright/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/copyright/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/copyright/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/description/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/description/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/description/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/description/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/disc_number/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/disc_number/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/disc_number/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/disc_number/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/field_not_supported/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/field_not_supported/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/field_not_supported/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/field_not_supported/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v1_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v2_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_riff_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_vorbis_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/reading/test_smart_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/genre/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/isrc/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/isrc/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/isrc/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/isrc/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/language/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/language/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/language/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/language/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/lyrics/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/lyrics/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/lyrics/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/lyrics/test_writing.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/publisher → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/musicbrainz_trackid}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/rating → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/originator}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/rating/reading → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/publisher}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/publisher/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/publisher/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/publisher/test_writing.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/rating/writing → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/rating}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/rating/reading}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/reading/test_base_100_proportional.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_non_proportional.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_proportional.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/test_error_handling.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/release_date → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/rating/writing}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/title → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_id3v2.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_riff.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_vorbis.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/rating/writing/test_comprehensive.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/track_number → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/release_date}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/release_date/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/release_date/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/release_date/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/release_date/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/test_metadata_field_validation.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/metadata_field/track_number/reading → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/title}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/title/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/title/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/title/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/title/test_writing.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/real_audio_files → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/track_number}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/technical_info → audiometa_python-0.11.0/audiometa/test/tests/integration/metadata_field/track_number/reading}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/track_number/reading/test_edge_cases.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/track_number/reading/test_metadata_format.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/track_number/test_deleting.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/metadata_field/track_number/test_writing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/metadata_format/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v1.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_3.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_4.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_riff.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_vorbis.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/test_performance_large_data.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/test_smart_parsing_scenarios.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/reading/test_unicode_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/metadata_format/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v1.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_3.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_4.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_riff.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_vorbis.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/multiple_values/writing/test_large_values.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/reading/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/reading/test_read_multiple_metadata.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/reading/test_reading_error_handling.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/technical_info/flac_md5 → audiometa_python-0.11.0/audiometa/test/tests/integration/real_audio_files}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/real_audio_files/test_reading.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/real_audio_files/test_writing.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking → audiometa_python-0.11.0/audiometa/test/tests/integration/technical_info}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations → audiometa_python-0.11.0/audiometa/test/tests/integration/technical_info/flac_md5}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/conftest.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair → audiometa_python-0.11.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_audio_data_corruption.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_file.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_invalid_md5.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_invalid_md5 → audiometa_python-0.11.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_invalid_with_metadata_combinations.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_state_precedence.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_unset_with_metadata_combinations.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_validation_fails_with_id3v1.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_validation_with_id3v2_only.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_validation_works_without_id3v1.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_metadata_combinations/test_md5_with_metadata_combinations.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_checking/test_unset_md5.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/writing → audiometa_python-0.11.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_delete_original.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/writing/writing_strategies → audiometa_python-0.11.0/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_invalid_md5}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_invalid_md5/test_flipped_md5.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_invalid_md5/test_partial_md5.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_invalid_md5/test_random_md5.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_md5_repair_with_metadata.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_non_flac_error.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/flac_md5/md5_repair/test_unset_md5.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/test_bitrate.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/test_channels.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/test_duration_in_sec.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/technical_info/test_sample_rate.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/test_audio_file.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/test_audio_format_readable_after_update_all_metadata_formats.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields → audiometa_python-0.11.0/audiometa/test/tests/integration/writing}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/test_forced_format.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/test_multiple_format_preservation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/test_partial_update.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit → audiometa_python-0.11.0/audiometa/test/tests/integration/writing/writing_strategies}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/sync_strategy/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/sync_strategy/test_flac_sync.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/sync_strategy/test_mp3_sync.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/sync_strategy/test_wav_sync.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/test_cleanup_strategy.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/test_preserve_strategy.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/audio_file → audiometa_python-0.11.0/audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_fail_behavior.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_no_writing_on_failure.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_strategy_specific.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/audio_file/technical_info → audiometa_python-0.11.0/audiometa/test/tests/unit}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/cli → audiometa_python-0.11.0/audiometa/test/tests/unit/audio_file}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers → audiometa_python-0.11.0/audiometa/test/tests/unit/audio_file/technical_info}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_bitrate.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_channels.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_duration_in_sec.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_error_handling.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_file_size.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_format_name.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/technical_info/test_sample_rate.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/test_context_manager.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/test_file_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/test_is_audio_file.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/test_operations.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/audio_file/test_path_handling.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/header_info → audiometa_python-0.11.0/audiometa/test/tests/unit/cli}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/cli/test_expand_file_patterns.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/conftest.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/header_info}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/header_info/test_id3v1.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/header_info/test_id3v2.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/header_info/test_riff.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/header_info/test_vorbis.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/disc_number}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/disc_number}/test_disc_number_validation.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field/rating → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/isrc}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/isrc}/test_isrc_format_validation.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/isrc}/test_isrc_type_validation.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values}/__init__.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading}/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/test_smart_parsing.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_separator_selection.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_value_filtering.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_normalization.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_profiles_values.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/test_rating_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_configuration_error.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_writing_profiles.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/release_date}/test_date_format_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/test_type_validation_exception.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/metadata_field/test_validation.py +0 -0
- {audiometa_python-0.9.0/audiometa/test/tests/unit/metadata_managers/metadata_field → audiometa_python-0.11.0/audiometa/test/tests/unit/metadata_managers/metadata_field/track_number}/test_track_number_validation.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/test_metadata_format_managers_write_and_read.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/test/tests/unit/metadata_managers/test_riff_configuration_error.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/flac_md5_state.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/id3v1_genre_code_map.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/metadata_format.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/metadata_writing_strategy.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/mutagen_exception_handler.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/os_dependencies_checker/__init__.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/os_dependencies_checker/base.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/os_dependencies_checker/config.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/os_dependencies_checker/macos.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/os_dependencies_checker/ubuntu.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/os_dependencies_checker/windows.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/rating_profiles.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/tool_path_resolver.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa/utils/types.py +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa_python.egg-info/dependency_links.txt +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa_python.egg-info/entry_points.txt +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa_python.egg-info/requires.txt +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/audiometa_python.egg-info/top_level.txt +0 -0
- {audiometa_python-0.9.0 → audiometa_python-0.11.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: audiometa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: A comprehensive Python library for reading and writing audio metadata across multiple formats
|
|
5
5
|
Author: AudioMeta Python Contributors
|
|
6
6
|
Author-email: Andreas Garcia <garcia.andreas.1991@gmail.com>
|
|
@@ -468,6 +468,16 @@ def _validate_metadata_field_formats(unified_metadata: UnifiedMetadata) -> None:
|
|
|
468
468
|
if isrc_value is not None and isinstance(isrc_value, str) and isrc_value:
|
|
469
469
|
_MetadataManager.validate_isrc(isrc_value)
|
|
470
470
|
|
|
471
|
+
# Validate MusicBrainz Track ID format if present and non-empty
|
|
472
|
+
if UnifiedMetadataKey.MUSICBRAINZ_TRACKID in unified_metadata:
|
|
473
|
+
musicbrainz_trackid_value = unified_metadata[UnifiedMetadataKey.MUSICBRAINZ_TRACKID]
|
|
474
|
+
if (
|
|
475
|
+
musicbrainz_trackid_value is not None
|
|
476
|
+
and isinstance(musicbrainz_trackid_value, str)
|
|
477
|
+
and musicbrainz_trackid_value
|
|
478
|
+
):
|
|
479
|
+
_MetadataManager.validate_musicbrainz_trackid(musicbrainz_trackid_value)
|
|
480
|
+
|
|
471
481
|
|
|
472
482
|
def validate_metadata_for_update(
|
|
473
483
|
unified_metadata: dict[UnifiedMetadataKey, Any] | UnifiedMetadata,
|
|
@@ -94,6 +94,9 @@ def format_as_table(data: dict[str, Any]) -> str:
|
|
|
94
94
|
"replaygain",
|
|
95
95
|
"archival_location",
|
|
96
96
|
"isrc",
|
|
97
|
+
"musicbrainz_trackid",
|
|
98
|
+
"description",
|
|
99
|
+
"originator",
|
|
97
100
|
}
|
|
98
101
|
if unified_keys.intersection(set(data.keys())):
|
|
99
102
|
# This is a unified metadata dict, wrap it
|
|
@@ -188,12 +191,16 @@ def _write_metadata(args: argparse.Namespace) -> None:
|
|
|
188
191
|
metadata[UnifiedMetadataKey.COMMENT] = args.comment
|
|
189
192
|
if args.description and args.description.strip():
|
|
190
193
|
metadata[UnifiedMetadataKey.DESCRIPTION] = args.description
|
|
194
|
+
if args.originator and args.originator.strip():
|
|
195
|
+
metadata[UnifiedMetadataKey.ORIGINATOR] = args.originator
|
|
191
196
|
if args.replaygain and args.replaygain.strip():
|
|
192
197
|
metadata[UnifiedMetadataKey.REPLAYGAIN] = args.replaygain
|
|
193
198
|
if args.archival_location and args.archival_location.strip():
|
|
194
199
|
metadata[UnifiedMetadataKey.ARCHIVAL_LOCATION] = args.archival_location
|
|
195
200
|
if args.isrc and args.isrc.strip():
|
|
196
201
|
metadata[UnifiedMetadataKey.ISRC] = args.isrc
|
|
202
|
+
if args.musicbrainz_track_id and args.musicbrainz_track_id.strip():
|
|
203
|
+
metadata[UnifiedMetadataKey.MUSICBRAINZ_TRACKID] = args.musicbrainz_track_id
|
|
197
204
|
|
|
198
205
|
# List fields (can be specified multiple times)
|
|
199
206
|
if args.artist:
|
|
@@ -432,9 +439,15 @@ Examples:
|
|
|
432
439
|
write_parser.add_argument("--lyrics", help="Unsynchronized lyrics text")
|
|
433
440
|
write_parser.add_argument("--comment", help="Comment")
|
|
434
441
|
write_parser.add_argument("--description", help="Description")
|
|
442
|
+
write_parser.add_argument("--originator", help="Originator")
|
|
435
443
|
write_parser.add_argument("--replaygain", help="ReplayGain information")
|
|
436
444
|
write_parser.add_argument("--archival-location", help="Archival location")
|
|
437
445
|
write_parser.add_argument("--isrc", help="International Standard Recording Code (12 characters)")
|
|
446
|
+
write_parser.add_argument(
|
|
447
|
+
"--musicbrainz-track-id",
|
|
448
|
+
dest="musicbrainz_track_id",
|
|
449
|
+
help="MusicBrainz Track ID (Recording ID) - UUID format (e.g., '9d6f6f7c-9d52-4c76-8f9e-01d18d8f8ec6')",
|
|
450
|
+
)
|
|
438
451
|
write_parser.add_argument(
|
|
439
452
|
"--force-format",
|
|
440
453
|
choices=["id3v2", "id3v1", "vorbis", "riff"],
|
|
@@ -255,6 +255,48 @@ class _MetadataManager:
|
|
|
255
255
|
isrc,
|
|
256
256
|
)
|
|
257
257
|
|
|
258
|
+
@staticmethod
|
|
259
|
+
def validate_musicbrainz_trackid(track_id: str) -> None:
|
|
260
|
+
"""Validate MusicBrainz Track ID (UUID) format.
|
|
261
|
+
|
|
262
|
+
MusicBrainz Track ID must be a valid UUID string in one of the following formats:
|
|
263
|
+
- 36-character hyphenated UUID (preferred): "9d6f6f7c-9d52-4c76-8f9e-01d18d8f8ec6"
|
|
264
|
+
- 32-character hex string without hyphens: "9d6f6f7c9d524c768f9e01d18d8f8ec6"
|
|
265
|
+
- Empty string is allowed (represents no Track ID)
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
track_id: The MusicBrainz Track ID string to validate
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
InvalidMetadataFieldFormatError: If the Track ID format is invalid
|
|
272
|
+
|
|
273
|
+
Examples:
|
|
274
|
+
>>> _MetadataManager.validate_musicbrainz_trackid("9d6f6f7c-9d52-4c76-8f9e-01d18d8f8ec6")
|
|
275
|
+
# Valid (36 chars)
|
|
276
|
+
>>> _MetadataManager.validate_musicbrainz_trackid("9d6f6f7c9d524c768f9e01d18d8f8ec6")
|
|
277
|
+
# Valid (32 chars)
|
|
278
|
+
>>> _MetadataManager.validate_musicbrainz_trackid("") # Valid (empty string)
|
|
279
|
+
>>> _MetadataManager.validate_musicbrainz_trackid("not-a-uuid")
|
|
280
|
+
# Raises InvalidMetadataFieldFormatError
|
|
281
|
+
"""
|
|
282
|
+
if not track_id:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# 36-character hyphenated UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
286
|
+
if re.match(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", track_id):
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
# 32-character hex string without hyphens
|
|
290
|
+
if re.match(r"^[0-9a-fA-F]{32}$", track_id):
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
raise InvalidMetadataFieldFormatError(
|
|
294
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID.value,
|
|
295
|
+
"36-character hyphenated UUID (e.g., '9d6f6f7c-9d52-4c76-8f9e-01d18d8f8ec6') or "
|
|
296
|
+
"32-character hex string (e.g., '9d6f6f7c9d524c768f9e01d18d8f8ec6')",
|
|
297
|
+
track_id,
|
|
298
|
+
)
|
|
299
|
+
|
|
258
300
|
@abstractmethod
|
|
259
301
|
def _extract_mutagen_metadata(self) -> MutagenMetadata:
|
|
260
302
|
raise NotImplementedError
|
|
@@ -31,6 +31,7 @@ from mutagen.id3._frames import (
|
|
|
31
31
|
TSRC,
|
|
32
32
|
TXXX,
|
|
33
33
|
TYER,
|
|
34
|
+
UFID,
|
|
34
35
|
USLT,
|
|
35
36
|
WOAR,
|
|
36
37
|
)
|
|
@@ -284,6 +285,7 @@ class _Id3v2Manager(_RatingSupportingMetadataManager):
|
|
|
284
285
|
UnifiedMetadataKey.COMMENT: self.Id3TextFrame.COMMENT,
|
|
285
286
|
UnifiedMetadataKey.REPLAYGAIN: None,
|
|
286
287
|
UnifiedMetadataKey.ISRC: self.Id3TextFrame.ISRC,
|
|
288
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID: None,
|
|
287
289
|
}
|
|
288
290
|
metadata_keys_direct_map_write: dict[UnifiedMetadataKey, RawMetadataKey | None] = {
|
|
289
291
|
UnifiedMetadataKey.TITLE: self.Id3TextFrame.TITLE,
|
|
@@ -305,6 +307,7 @@ class _Id3v2Manager(_RatingSupportingMetadataManager):
|
|
|
305
307
|
UnifiedMetadataKey.COMMENT: self.Id3TextFrame.COMMENT,
|
|
306
308
|
UnifiedMetadataKey.REPLAYGAIN: None,
|
|
307
309
|
UnifiedMetadataKey.ISRC: self.Id3TextFrame.ISRC,
|
|
310
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID: None,
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
super().__init__(
|
|
@@ -396,6 +399,56 @@ class _Id3v2Manager(_RatingSupportingMetadataManager):
|
|
|
396
399
|
result[self.Id3TextFrame.REPLAYGAIN] = txxx_frame.text
|
|
397
400
|
break
|
|
398
401
|
|
|
402
|
+
# Handle UFID frames for MusicBrainz Track ID (preferred)
|
|
403
|
+
musicbrainz_trackid = None
|
|
404
|
+
for raw_mutagen_frame in raw_mutagen_metadata.items():
|
|
405
|
+
if raw_mutagen_frame[0].startswith("UFID"):
|
|
406
|
+
ufid_frame = raw_mutagen_frame[1]
|
|
407
|
+
if (
|
|
408
|
+
hasattr(ufid_frame, "owner")
|
|
409
|
+
and ufid_frame.owner == "http://musicbrainz.org"
|
|
410
|
+
and hasattr(ufid_frame, "data")
|
|
411
|
+
):
|
|
412
|
+
# UFID data is bytes, decode to string
|
|
413
|
+
try:
|
|
414
|
+
musicbrainz_trackid = ufid_frame.data.decode("utf-8", errors="replace").strip("\x00")
|
|
415
|
+
# Normalize to hyphenated UUID format if it's 32 hex chars
|
|
416
|
+
uuid_hex_length = 32
|
|
417
|
+
if len(musicbrainz_trackid) == uuid_hex_length and all(
|
|
418
|
+
c in "0123456789abcdefABCDEF" for c in musicbrainz_trackid
|
|
419
|
+
):
|
|
420
|
+
musicbrainz_trackid = (
|
|
421
|
+
f"{musicbrainz_trackid[:8]}-{musicbrainz_trackid[8:12]}-"
|
|
422
|
+
f"{musicbrainz_trackid[12:16]}-{musicbrainz_trackid[16:20]}-{musicbrainz_trackid[20:32]}"
|
|
423
|
+
)
|
|
424
|
+
break
|
|
425
|
+
except (UnicodeDecodeError, AttributeError):
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
# Handle TXXX frames for MusicBrainz Track ID (fallback)
|
|
429
|
+
if not musicbrainz_trackid:
|
|
430
|
+
for raw_mutagen_frame in raw_mutagen_metadata.items():
|
|
431
|
+
if raw_mutagen_frame[0].startswith("TXXX"):
|
|
432
|
+
txxx_frame = raw_mutagen_frame[1]
|
|
433
|
+
if hasattr(txxx_frame, "desc") and txxx_frame.desc == "MusicBrainz Track Id" and txxx_frame.text:
|
|
434
|
+
musicbrainz_trackid = (
|
|
435
|
+
txxx_frame.text[0] if isinstance(txxx_frame.text, list) else str(txxx_frame.text)
|
|
436
|
+
)
|
|
437
|
+
# Normalize to hyphenated UUID format if it's 32 hex chars
|
|
438
|
+
uuid_hex_length = 32
|
|
439
|
+
if len(musicbrainz_trackid) == uuid_hex_length and all(
|
|
440
|
+
c in "0123456789abcdefABCDEF" for c in musicbrainz_trackid
|
|
441
|
+
):
|
|
442
|
+
musicbrainz_trackid = (
|
|
443
|
+
f"{musicbrainz_trackid[:8]}-{musicbrainz_trackid[8:12]}-"
|
|
444
|
+
f"{musicbrainz_trackid[12:16]}-{musicbrainz_trackid[16:20]}-{musicbrainz_trackid[20:32]}"
|
|
445
|
+
)
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
if musicbrainz_trackid:
|
|
449
|
+
# Use a special key for MusicBrainz Track ID (not a text frame, so use string key)
|
|
450
|
+
result[cast(RawMetadataKey, "MUSICBRAINZ_TRACKID")] = [musicbrainz_trackid]
|
|
451
|
+
|
|
399
452
|
# Special handling for release date: if TDRC is not present, try to construct from TYER + TDAT
|
|
400
453
|
# Only do this for ID3v2 files (not ID3v1) and only when both TYER and TDAT are present
|
|
401
454
|
if self.Id3TextFrame.RECORDING_TIME not in result:
|
|
@@ -443,6 +496,23 @@ class _Id3v2Manager(_RatingSupportingMetadataManager):
|
|
|
443
496
|
if app_metadata_value is not None:
|
|
444
497
|
# Add new TXXX frame with desc 'REPLAYGAIN'
|
|
445
498
|
raw_mutagen_metadata.add(TXXX(encoding=3, desc="REPLAYGAIN", text=str(app_metadata_value)))
|
|
499
|
+
elif unified_metadata_key == UnifiedMetadataKey.MUSICBRAINZ_TRACKID:
|
|
500
|
+
# Remove existing UFID frames with MusicBrainz owner
|
|
501
|
+
raw_mutagen_metadata.delall("UFID:http://musicbrainz.org")
|
|
502
|
+
# Remove existing TXXX frames with MusicBrainz Track Id description
|
|
503
|
+
raw_mutagen_metadata.delall("TXXX:MusicBrainz Track Id")
|
|
504
|
+
|
|
505
|
+
if app_metadata_value is not None and app_metadata_value != "":
|
|
506
|
+
# Normalize UUID: convert 32-char hex to 36-char hyphenated format if needed
|
|
507
|
+
track_id = str(app_metadata_value).strip()
|
|
508
|
+
uuid_hex_length = 32
|
|
509
|
+
if len(track_id) == uuid_hex_length and all(c in "0123456789abcdefABCDEF" for c in track_id):
|
|
510
|
+
track_id = f"{track_id[:8]}-{track_id[8:12]}-{track_id[12:16]}-{track_id[16:20]}-{track_id[20:32]}"
|
|
511
|
+
|
|
512
|
+
# Write as UFID frame with owner "http://musicbrainz.org"
|
|
513
|
+
# UFID data should be the UUID as bytes (without null terminator)
|
|
514
|
+
track_id_bytes = track_id.encode("utf-8")
|
|
515
|
+
raw_mutagen_metadata.add(UFID(owner="http://musicbrainz.org", data=track_id_bytes))
|
|
446
516
|
elif unified_metadata_key in (UnifiedMetadataKey.DISC_NUMBER, UnifiedMetadataKey.DISC_TOTAL):
|
|
447
517
|
tpos_key = self.Id3TextFrame.DISC_NUMBER
|
|
448
518
|
tpos_frame_class = TPOS
|
|
@@ -512,6 +582,17 @@ class _Id3v2Manager(_RatingSupportingMetadataManager):
|
|
|
512
582
|
return None
|
|
513
583
|
first_value = replaygain_value[0]
|
|
514
584
|
return cast(UnifiedMetadataValue, first_value)
|
|
585
|
+
if unified_metadata_key == UnifiedMetadataKey.MUSICBRAINZ_TRACKID:
|
|
586
|
+
musicbrainz_trackid_key = cast(RawMetadataKey, "MUSICBRAINZ_TRACKID")
|
|
587
|
+
if musicbrainz_trackid_key not in raw_clean_metadata:
|
|
588
|
+
return None
|
|
589
|
+
trackid_value = raw_clean_metadata[musicbrainz_trackid_key]
|
|
590
|
+
if trackid_value is None:
|
|
591
|
+
return None
|
|
592
|
+
if len(trackid_value) == 0:
|
|
593
|
+
return None
|
|
594
|
+
first_value = trackid_value[0]
|
|
595
|
+
return cast(UnifiedMetadataValue, first_value)
|
|
515
596
|
if unified_metadata_key == UnifiedMetadataKey.DISC_NUMBER:
|
|
516
597
|
tpos_key = self.Id3TextFrame.DISC_NUMBER
|
|
517
598
|
if tpos_key not in raw_clean_metadata:
|
|
@@ -15,7 +15,13 @@ from ....utils.rating_profiles import RatingWriteProfile
|
|
|
15
15
|
from ....utils.types import RawMetadataDict, RawMetadataKey, UnifiedMetadata, UnifiedMetadataValue
|
|
16
16
|
from ....utils.unified_metadata_key import UnifiedMetadataKey
|
|
17
17
|
from .._RatingSupportingMetadataManager import _RatingSupportingMetadataManager
|
|
18
|
-
from ._riff_bext_chunk import
|
|
18
|
+
from ._riff_bext_chunk import (
|
|
19
|
+
extract_bext_chunk,
|
|
20
|
+
find_bext_chunk,
|
|
21
|
+
find_fmt_chunk,
|
|
22
|
+
update_bext_description_in_riff_data,
|
|
23
|
+
update_bext_originator_in_riff_data,
|
|
24
|
+
)
|
|
19
25
|
from ._riff_constants import (
|
|
20
26
|
RIFF_AUDIO_FORMAT_IEEE_FLOAT,
|
|
21
27
|
RIFF_FORMAT_CHUNK_MIN_SIZE,
|
|
@@ -115,6 +121,7 @@ class _RiffManager(_RatingSupportingMetadataManager):
|
|
|
115
121
|
|
|
116
122
|
# BWF
|
|
117
123
|
ISRC = "ISRC" # International Standard Recording Code
|
|
124
|
+
MBID = "MBID" # MusicBrainz Track ID (Recording ID)
|
|
118
125
|
|
|
119
126
|
def __init__(self, audio_file: "_AudioFile", normalized_rating_max_value: None | int = None):
|
|
120
127
|
# Validate that the file is a WAV file
|
|
@@ -138,7 +145,9 @@ class _RiffManager(_RatingSupportingMetadataManager):
|
|
|
138
145
|
UnifiedMetadataKey.UNSYNCHRONIZED_LYRICS: self.RiffTagKey.UNSYNCHRONIZED_LYRICS,
|
|
139
146
|
UnifiedMetadataKey.TRACK_NUMBER: self.RiffTagKey.TRACK_NUMBER,
|
|
140
147
|
UnifiedMetadataKey.ISRC: self.RiffTagKey.ISRC,
|
|
148
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID: self.RiffTagKey.MBID,
|
|
141
149
|
UnifiedMetadataKey.DESCRIPTION: None,
|
|
150
|
+
UnifiedMetadataKey.ORIGINATOR: None,
|
|
142
151
|
}
|
|
143
152
|
metadata_keys_direct_map_write: dict[UnifiedMetadataKey, RawMetadataKey | None] = {
|
|
144
153
|
UnifiedMetadataKey.TITLE: self.RiffTagKey.TITLE,
|
|
@@ -156,7 +165,9 @@ class _RiffManager(_RatingSupportingMetadataManager):
|
|
|
156
165
|
UnifiedMetadataKey.UNSYNCHRONIZED_LYRICS: self.RiffTagKey.UNSYNCHRONIZED_LYRICS,
|
|
157
166
|
UnifiedMetadataKey.TRACK_NUMBER: self.RiffTagKey.TRACK_NUMBER,
|
|
158
167
|
UnifiedMetadataKey.ISRC: self.RiffTagKey.ISRC,
|
|
168
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID: self.RiffTagKey.MBID,
|
|
159
169
|
UnifiedMetadataKey.DESCRIPTION: None,
|
|
170
|
+
UnifiedMetadataKey.ORIGINATOR: None,
|
|
160
171
|
}
|
|
161
172
|
super().__init__(
|
|
162
173
|
audio_file=audio_file,
|
|
@@ -280,6 +291,17 @@ class _RiffManager(_RatingSupportingMetadataManager):
|
|
|
280
291
|
except Exception:
|
|
281
292
|
pass
|
|
282
293
|
return None
|
|
294
|
+
if unified_metadata_key == UnifiedMetadataKey.ORIGINATOR:
|
|
295
|
+
# Read from bext chunk
|
|
296
|
+
try:
|
|
297
|
+
self.audio_file.seek(0)
|
|
298
|
+
file_data = self.audio_file.read()
|
|
299
|
+
bext_data = self._extract_bext_chunk(file_data)
|
|
300
|
+
if bext_data and "Originator" in bext_data:
|
|
301
|
+
return cast(str, bext_data["Originator"])
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
return None
|
|
283
305
|
msg = f"Metadata key not handled: {unified_metadata_key}"
|
|
284
306
|
raise MetadataFieldNotSupportedByMetadataFormatError(msg)
|
|
285
307
|
|
|
@@ -459,6 +481,9 @@ class _RiffManager(_RatingSupportingMetadataManager):
|
|
|
459
481
|
if UnifiedMetadataKey.DESCRIPTION in merged_metadata:
|
|
460
482
|
description_value = merged_metadata[UnifiedMetadataKey.DESCRIPTION]
|
|
461
483
|
update_bext_description_in_riff_data(riff_data, cast(str | None, description_value))
|
|
484
|
+
if UnifiedMetadataKey.ORIGINATOR in merged_metadata:
|
|
485
|
+
originator_value = merged_metadata[UnifiedMetadataKey.ORIGINATOR]
|
|
486
|
+
update_bext_originator_in_riff_data(riff_data, cast(str | None, originator_value))
|
|
462
487
|
|
|
463
488
|
def _update_riff_chunk_size(self, riff_data: bytearray) -> None:
|
|
464
489
|
"""Update RIFF chunk size in RIFF data.
|
|
@@ -210,25 +210,33 @@ def extract_bext_chunk(file_data: bytes, skip_id3v2_tags_func: Callable[[bytes],
|
|
|
210
210
|
return None
|
|
211
211
|
|
|
212
212
|
|
|
213
|
-
def
|
|
214
|
-
|
|
213
|
+
def _update_bext_field_in_riff_data(
|
|
214
|
+
riff_data: bytearray, value: str | None, field_size: int, field_offset: int
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Update a field in the bext chunk within RIFF data.
|
|
215
217
|
|
|
216
218
|
This function modifies the riff_data bytearray in-place to update or create
|
|
217
|
-
the bext chunk with the
|
|
219
|
+
the bext chunk with the specified field. This is integrated into the main
|
|
218
220
|
update flow to avoid multiple file writes.
|
|
219
221
|
|
|
220
222
|
Args:
|
|
221
223
|
riff_data: RIFF data bytearray (modified in-place)
|
|
222
|
-
value:
|
|
224
|
+
value: Field value to write, or None/empty string to clear
|
|
225
|
+
field_size: Size of the field in bytes
|
|
226
|
+
field_offset: Offset of the field within the bext chunk
|
|
223
227
|
"""
|
|
224
|
-
encoded =
|
|
228
|
+
encoded = (
|
|
229
|
+
b"\x00" * field_size
|
|
230
|
+
if value is None or value == ""
|
|
231
|
+
else value.encode("utf-8")[: field_size - 1].ljust(field_size, b"\x00")
|
|
232
|
+
)
|
|
225
233
|
|
|
226
234
|
# Find bext chunk in RIFF data (no ID3v2 tags in riff_data at this point)
|
|
227
235
|
pos = find_bext_chunk_in_riff_data(riff_data)
|
|
228
236
|
if pos != -1:
|
|
229
|
-
# Update existing bext chunk
|
|
237
|
+
# Update existing bext chunk field
|
|
230
238
|
bext_start = pos + 8
|
|
231
|
-
riff_data[bext_start : bext_start +
|
|
239
|
+
riff_data[bext_start + field_offset : bext_start + field_offset + field_size] = encoded
|
|
232
240
|
else:
|
|
233
241
|
# No bext chunk, create one
|
|
234
242
|
fmt_pos = find_fmt_chunk_in_riff_data(riff_data)
|
|
@@ -237,12 +245,24 @@ def update_bext_description_in_riff_data(riff_data: bytearray, value: str | None
|
|
|
237
245
|
fmt_size = int.from_bytes(bytes(riff_data[fmt_pos + 4 : fmt_pos + 8]), "little")
|
|
238
246
|
insert_pos = fmt_pos + 8 + fmt_size
|
|
239
247
|
# Create bext chunk data: 602 bytes v1
|
|
240
|
-
|
|
248
|
+
# Fill with zeros, then set the specific field
|
|
249
|
+
bext_data = bytearray(b"bext" + (602).to_bytes(4, "little") + b"\x00" * 602)
|
|
250
|
+
bext_data[8 + field_offset : 8 + field_offset + field_size] = encoded
|
|
241
251
|
# Insert bext chunk after fmt chunk
|
|
242
252
|
riff_data[insert_pos:insert_pos] = bext_data
|
|
243
253
|
# Note: RIFF chunk size will be updated by caller after this method
|
|
244
254
|
|
|
245
255
|
|
|
256
|
+
def update_bext_description_in_riff_data(riff_data: bytearray, value: str | None) -> None:
|
|
257
|
+
"""Update the Description field in the bext chunk within RIFF data."""
|
|
258
|
+
_update_bext_field_in_riff_data(riff_data, value, 256, 0)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def update_bext_originator_in_riff_data(riff_data: bytearray, value: str | None) -> None:
|
|
262
|
+
"""Update the Originator field in the bext chunk within RIFF data."""
|
|
263
|
+
_update_bext_field_in_riff_data(riff_data, value, 32, 256)
|
|
264
|
+
|
|
265
|
+
|
|
246
266
|
def find_bext_chunk_in_riff_data(riff_data: bytearray) -> int:
|
|
247
267
|
"""Find the position of the bext chunk in RIFF data.
|
|
248
268
|
|
|
@@ -59,6 +59,7 @@ class _VorbisManager(_RatingSupportingMetadataManager):
|
|
|
59
59
|
LOCATION = "LOCATION" # Recording location
|
|
60
60
|
CONTACT = "CONTACT" # Contact information
|
|
61
61
|
ISRC = "ISRC" # International Standard Recording Code
|
|
62
|
+
MUSICBRAINZ_TRACKID = "MUSICBRAINZ_TRACKID" # MusicBrainz Track ID (Recording ID)
|
|
62
63
|
|
|
63
64
|
# Non-standard
|
|
64
65
|
LANGUAGE = "LANGUAGE"
|
|
@@ -92,6 +93,7 @@ class _VorbisManager(_RatingSupportingMetadataManager):
|
|
|
92
93
|
UnifiedMetadataKey.REPLAYGAIN: self.VorbisKey.REPLAYGAIN,
|
|
93
94
|
UnifiedMetadataKey.PUBLISHER: self.VorbisKey.PUBLISHER,
|
|
94
95
|
UnifiedMetadataKey.ISRC: self.VorbisKey.ISRC,
|
|
96
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID: self.VorbisKey.MUSICBRAINZ_TRACKID,
|
|
95
97
|
UnifiedMetadataKey.DESCRIPTION: self.VorbisKey.DESCRIPTION,
|
|
96
98
|
}
|
|
97
99
|
metadata_keys_direct_map_write = {
|
|
@@ -114,6 +116,7 @@ class _VorbisManager(_RatingSupportingMetadataManager):
|
|
|
114
116
|
UnifiedMetadataKey.REPLAYGAIN: self.VorbisKey.REPLAYGAIN,
|
|
115
117
|
UnifiedMetadataKey.PUBLISHER: self.VorbisKey.PUBLISHER,
|
|
116
118
|
UnifiedMetadataKey.ISRC: self.VorbisKey.ISRC,
|
|
119
|
+
UnifiedMetadataKey.MUSICBRAINZ_TRACKID: self.VorbisKey.MUSICBRAINZ_TRACKID,
|
|
117
120
|
UnifiedMetadataKey.DESCRIPTION: self.VorbisKey.DESCRIPTION,
|
|
118
121
|
}
|
|
119
122
|
super().__init__(
|
|
@@ -398,6 +401,7 @@ class _VorbisManager(_RatingSupportingMetadataManager):
|
|
|
398
401
|
"ENCODER": "ENCODER",
|
|
399
402
|
"URL": "URL",
|
|
400
403
|
"ISRC": "ISRC",
|
|
404
|
+
"MUSICBRAINZ_TRACKID": "MUSICBRAINZ_TRACKID",
|
|
401
405
|
"PUBLISHER": "PUBLISHER",
|
|
402
406
|
}
|
|
403
407
|
|
|
@@ -85,6 +85,60 @@ class ManualID3v2FrameCreator:
|
|
|
85
85
|
|
|
86
86
|
ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
|
|
87
87
|
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _create_ufid_frame(owner: str, data: bytes, version: str = "2.4") -> bytes:
|
|
90
|
+
"""Create a UFID (Unique File Identifier) frame.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
owner: UFID owner identifier (null-terminated string)
|
|
94
|
+
data: UFID data (binary data, no null terminator)
|
|
95
|
+
version: ID3v2 version ("2.3" or "2.4")
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Complete UFID frame as bytes (header + frame data)
|
|
99
|
+
"""
|
|
100
|
+
# UFID frame format:
|
|
101
|
+
# - Owner: null-terminated string (ISO-8859-1)
|
|
102
|
+
# - Data: binary data (no null terminator)
|
|
103
|
+
owner_bytes = owner.encode("latin1", errors="ignore") + b"\x00"
|
|
104
|
+
frame_data = owner_bytes + data
|
|
105
|
+
|
|
106
|
+
# Frame header: ID (4 bytes) + size (4 bytes) + flags (2 bytes)
|
|
107
|
+
frame_id_bytes = b"UFID"
|
|
108
|
+
frame_size = len(frame_data)
|
|
109
|
+
frame_flags = 0x0000 # No flags
|
|
110
|
+
|
|
111
|
+
if version == "2.3":
|
|
112
|
+
frame_header = (
|
|
113
|
+
frame_id_bytes
|
|
114
|
+
+ struct.pack(">I", frame_size) # Big-endian 32-bit size
|
|
115
|
+
+ struct.pack(">H", frame_flags) # Big-endian 16-bit flags
|
|
116
|
+
)
|
|
117
|
+
else: # ID3v2.4
|
|
118
|
+
frame_header = (
|
|
119
|
+
frame_id_bytes
|
|
120
|
+
+ ManualID3v2FrameCreator._synchsafe_int(frame_size) # Synchsafe size
|
|
121
|
+
+ struct.pack(">H", frame_flags) # Big-endian 16-bit flags
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return frame_header + frame_data
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def create_ufid_frame(file_path: Path, owner: str, data: bytes, version: str = "2.4") -> None:
|
|
128
|
+
"""Create a UFID frame in an ID3v2 tag.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
file_path: Path to the audio file
|
|
132
|
+
owner: UFID owner identifier (e.g., "http://musicbrainz.org")
|
|
133
|
+
data: UFID data (binary data)
|
|
134
|
+
version: ID3v2 version ("2.3" or "2.4")
|
|
135
|
+
"""
|
|
136
|
+
if version not in ["2.3", "2.4"]:
|
|
137
|
+
msg = "Version must be '2.3' or '2.4'"
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
frame = ManualID3v2FrameCreator._create_ufid_frame(owner, data, version)
|
|
140
|
+
ManualID3v2FrameCreator._write_id3v2_tag(file_path, [frame], version)
|
|
141
|
+
|
|
88
142
|
@staticmethod
|
|
89
143
|
def _create_text_frame(frame_id: str, text: str, version: str, encoding: int | None = None) -> bytes:
|
|
90
144
|
"""Create a single ID3v2 text frame with the given ID and text."""
|
|
@@ -118,6 +118,29 @@ class ID3v2MetadataSetter:
|
|
|
118
118
|
cmd.extend(["--TDRC", release_date])
|
|
119
119
|
metadata_added = True
|
|
120
120
|
|
|
121
|
+
# Handle musicbrainz_trackid (UFID frame) - needs special handling
|
|
122
|
+
musicbrainz_trackid = None
|
|
123
|
+
for key, value in metadata.items():
|
|
124
|
+
if key.lower() == "musicbrainz_trackid" and not isinstance(value, list) and value:
|
|
125
|
+
musicbrainz_trackid = str(value).strip()
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if musicbrainz_trackid:
|
|
129
|
+
# Normalize UUID: convert 32-char hex to 36-char hyphenated format if needed
|
|
130
|
+
if len(musicbrainz_trackid) == 32 and all(c in "0123456789abcdefABCDEF" for c in musicbrainz_trackid):
|
|
131
|
+
musicbrainz_trackid = (
|
|
132
|
+
f"{musicbrainz_trackid[:8]}-{musicbrainz_trackid[8:12]}-"
|
|
133
|
+
f"{musicbrainz_trackid[12:16]}-{musicbrainz_trackid[16:20]}-{musicbrainz_trackid[20:32]}"
|
|
134
|
+
)
|
|
135
|
+
# Use manual frame creator for UFID frames to maintain test isolation
|
|
136
|
+
# (mid3v2 doesn't handle URLs with colons in UFID owner field correctly)
|
|
137
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
138
|
+
|
|
139
|
+
track_id_bytes = musicbrainz_trackid.encode("utf-8")
|
|
140
|
+
ManualID3v2FrameCreator.create_ufid_frame(
|
|
141
|
+
file_path, "http://musicbrainz.org", track_id_bytes, version=version
|
|
142
|
+
)
|
|
143
|
+
|
|
121
144
|
# Handle non-list values (excluding already handled fields and list fields)
|
|
122
145
|
# Only exclude list fields if they're actually lists (single strings should be processed)
|
|
123
146
|
list_field_keys = set()
|
|
@@ -129,7 +152,7 @@ class ID3v2MetadataSetter:
|
|
|
129
152
|
if (
|
|
130
153
|
key.lower() in key_mapping
|
|
131
154
|
and not isinstance(value, list)
|
|
132
|
-
and key.lower() not in ["disc_number", "disc_total", "release_date", "year"]
|
|
155
|
+
and key.lower() not in ["disc_number", "disc_total", "release_date", "year", "musicbrainz_trackid"]
|
|
133
156
|
and key.lower() not in list_field_keys # Only exclude if it's actually a list
|
|
134
157
|
):
|
|
135
158
|
# Special handling for rating (POPM frame requires app name prefix)
|
|
@@ -506,3 +529,52 @@ class ID3v2MetadataSetter:
|
|
|
506
529
|
creator = ManualID3v2FrameCreator()
|
|
507
530
|
frame_data = creator._create_text_frame("TPE1", text, "2.4", encoding=encoding)
|
|
508
531
|
creator._write_id3v2_tag(file_path, [frame_data], "2.4")
|
|
532
|
+
|
|
533
|
+
@staticmethod
|
|
534
|
+
def set_musicbrainz_trackid_ufid(file_path: Path, track_id: str) -> None:
|
|
535
|
+
"""Set MusicBrainz Track ID using UFID frame (preferred format).
|
|
536
|
+
|
|
537
|
+
Uses manual binary construction to maintain test isolation (avoids using mutagen
|
|
538
|
+
which is also used in the implementation).
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
file_path: Path to the MP3 file
|
|
542
|
+
track_id: MusicBrainz Track ID (UUID string, hyphenated or 32-char hex)
|
|
543
|
+
"""
|
|
544
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
545
|
+
|
|
546
|
+
track_id_bytes = track_id.encode("utf-8")
|
|
547
|
+
ManualID3v2FrameCreator.create_ufid_frame(file_path, "http://musicbrainz.org", track_id_bytes, version="2.4")
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def set_musicbrainz_trackid_txxx(file_path: Path, track_id: str) -> None:
|
|
551
|
+
"""Set MusicBrainz Track ID using TXXX frame (fallback format).
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
file_path: Path to the MP3 file
|
|
555
|
+
track_id: MusicBrainz Track ID (UUID string, hyphenated or 32-char hex)
|
|
556
|
+
"""
|
|
557
|
+
command = [
|
|
558
|
+
get_tool_path("mid3v2"),
|
|
559
|
+
"--TXXX",
|
|
560
|
+
f"MusicBrainz Track Id:{track_id}",
|
|
561
|
+
str(file_path),
|
|
562
|
+
]
|
|
563
|
+
run_external_tool(command, "mid3v2")
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def set_ufid_with_owner(file_path: Path, owner: str, data: str) -> None:
|
|
567
|
+
"""Set UFID frame with a specific owner (for testing purposes).
|
|
568
|
+
|
|
569
|
+
Uses manual binary construction to maintain test isolation (avoids using mutagen
|
|
570
|
+
which is also used in the implementation).
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
file_path: Path to the MP3 file
|
|
574
|
+
owner: UFID owner identifier (e.g., "http://musicbrainz.org", "http://example.com")
|
|
575
|
+
data: UFID data (typically a UUID or identifier string)
|
|
576
|
+
"""
|
|
577
|
+
from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
|
|
578
|
+
|
|
579
|
+
data_bytes = data.encode("utf-8")
|
|
580
|
+
ManualID3v2FrameCreator.create_ufid_frame(file_path, owner, data_bytes, version="2.4")
|
|
@@ -143,6 +143,18 @@ class ManualRIFFMetadataCreator:
|
|
|
143
143
|
all_fields = [*existing_fields, rating_field]
|
|
144
144
|
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, all_fields)
|
|
145
145
|
|
|
146
|
+
@staticmethod
|
|
147
|
+
def create_mbid_field(file_path: Path, mbid: str) -> None:
|
|
148
|
+
"""Create MBID field in the RIFF INFO chunk, preserving existing fields."""
|
|
149
|
+
# Read existing fields and add MBID
|
|
150
|
+
existing_fields = ManualRIFFMetadataCreator._read_existing_info_fields(file_path)
|
|
151
|
+
# Remove existing MBID if present (we'll replace it)
|
|
152
|
+
existing_fields = [f for f in existing_fields if f[:4] != b"MBID"]
|
|
153
|
+
# Add new MBID field
|
|
154
|
+
mbid_field = ManualRIFFMetadataCreator._create_info_field("MBID", mbid)
|
|
155
|
+
all_fields = [*existing_fields, mbid_field]
|
|
156
|
+
ManualRIFFMetadataCreator._write_riff_info_chunk(file_path, all_fields)
|
|
157
|
+
|
|
146
158
|
@staticmethod
|
|
147
159
|
def _create_info_field(field_id: str, text: str) -> bytes:
|
|
148
160
|
"""Create a single RIFF INFO field with the given FourCC and text."""
|
|
@@ -34,7 +34,9 @@ class RIFFMetadataSetter:
|
|
|
34
34
|
"rating": "--IRTD",
|
|
35
35
|
"copyright": "--ICOP",
|
|
36
36
|
"isrc": "--ISRC",
|
|
37
|
+
"musicbrainz_trackid": "--MBID",
|
|
37
38
|
"description": "--Description",
|
|
39
|
+
"originator": "--Originator",
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
# Handle list values - include first value in main command to avoid overwriting
|
|
@@ -50,8 +52,16 @@ class RIFFMetadataSetter:
|
|
|
50
52
|
elif key.lower() == "composer":
|
|
51
53
|
composer_value = value[0] # Store first composer for main command
|
|
52
54
|
|
|
55
|
+
# Handle musicbrainz_trackid (MBID FourCC) - needs special handling with manual creator
|
|
56
|
+
musicbrainz_trackid = None
|
|
57
|
+
for key, value in metadata.items():
|
|
58
|
+
if key.lower() == "musicbrainz_trackid" and not isinstance(value, list) and value:
|
|
59
|
+
musicbrainz_trackid = str(value).strip()
|
|
60
|
+
break
|
|
61
|
+
|
|
53
62
|
# Handle non-list values and include list values in main command
|
|
54
|
-
# Note: Rating, BPM, Language, and
|
|
63
|
+
# Note: Rating, BPM, Language, Composer, and MusicBrainz Track ID need to be set
|
|
64
|
+
# AFTER bwfmetaedit to avoid being overwritten
|
|
55
65
|
rating_value = None
|
|
56
66
|
bpm_value = None
|
|
57
67
|
language_value = None
|
|
@@ -67,6 +77,9 @@ class RIFFMetadataSetter:
|
|
|
67
77
|
language_value = str(value) # Store for later
|
|
68
78
|
elif key.lower() == "composer":
|
|
69
79
|
composer_single_value = str(value) # Store for later
|
|
80
|
+
elif key.lower() == "musicbrainz_trackid":
|
|
81
|
+
# Handled separately after bwfmetaedit
|
|
82
|
+
pass
|
|
70
83
|
else:
|
|
71
84
|
cmd.extend([f"{key_mapping[key.lower()]}={value}"])
|
|
72
85
|
metadata_added = True
|
|
@@ -112,6 +125,12 @@ class RIFFMetadataSetter:
|
|
|
112
125
|
|
|
113
126
|
ManualRIFFMetadataCreator.create_composer_field(file_path, composer_single_value)
|
|
114
127
|
|
|
128
|
+
# Set MusicBrainz Track ID (MBID FourCC) AFTER bwfmetaedit
|
|
129
|
+
if musicbrainz_trackid is not None:
|
|
130
|
+
from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
|
|
131
|
+
|
|
132
|
+
ManualRIFFMetadataCreator.create_mbid_field(file_path, musicbrainz_trackid)
|
|
133
|
+
|
|
115
134
|
@staticmethod
|
|
116
135
|
def set_comment(file_path: Path, comment: str) -> None:
|
|
117
136
|
command = ["bwfmetaedit", f"--ICMT={comment}", str(file_path)]
|