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,115 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TechnicalInfoInspector:
|
|
6
|
+
"""Helper class for inspecting technical audio file information using mediainfo."""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def _run_mediainfo(file_path: str | Path, output_format: str = "JSON") -> dict:
|
|
10
|
+
"""Run mediainfo on a file and return parsed output."""
|
|
11
|
+
cmd = ["mediainfo", f"--Output={output_format}", str(file_path)]
|
|
12
|
+
try:
|
|
13
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
14
|
+
if output_format == "JSON":
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
return json.loads(result.stdout)
|
|
18
|
+
except subprocess.CalledProcessError as e:
|
|
19
|
+
msg = f"Failed to run mediainfo on {file_path}: {e}"
|
|
20
|
+
raise RuntimeError(msg) from e
|
|
21
|
+
except json.JSONDecodeError as e:
|
|
22
|
+
msg = f"Failed to parse mediainfo output: {e}"
|
|
23
|
+
raise RuntimeError(msg) from e
|
|
24
|
+
else:
|
|
25
|
+
return {"text": result.stdout}
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def get_bitrate(file_path: str | Path) -> int | None:
|
|
29
|
+
"""Get the bitrate of an audio file in kb/s using mediainfo."""
|
|
30
|
+
try:
|
|
31
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
32
|
+
tracks = data.get("media", {}).get("track", [])
|
|
33
|
+
for track in tracks:
|
|
34
|
+
if track.get("@type") == "Audio":
|
|
35
|
+
bitrate_str = track.get("BitRate")
|
|
36
|
+
if bitrate_str:
|
|
37
|
+
# Handle formats like "128 kb/s" or "128000"
|
|
38
|
+
if "kb/s" in str(bitrate_str):
|
|
39
|
+
return int(str(bitrate_str).split()[0])
|
|
40
|
+
if str(bitrate_str).isdigit():
|
|
41
|
+
return int(bitrate_str) // 1000
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
else:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def get_duration(file_path: str | Path) -> float | None:
|
|
49
|
+
"""Get the duration of an audio file in seconds using mediainfo."""
|
|
50
|
+
try:
|
|
51
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
52
|
+
tracks = data.get("media", {}).get("track", [])
|
|
53
|
+
for track in tracks:
|
|
54
|
+
if track.get("@type") == "Audio":
|
|
55
|
+
duration_str = track.get("Duration")
|
|
56
|
+
if duration_str:
|
|
57
|
+
# Handle formats like "1.025 s" or just numbers
|
|
58
|
+
if "s" in duration_str:
|
|
59
|
+
return float(duration_str.split()[0])
|
|
60
|
+
return float(duration_str)
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
else:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def get_sample_rate(file_path: str | Path) -> int | None:
|
|
68
|
+
"""Get the sample rate of an audio file in Hz using mediainfo."""
|
|
69
|
+
try:
|
|
70
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
71
|
+
tracks = data.get("media", {}).get("track", [])
|
|
72
|
+
for track in tracks:
|
|
73
|
+
if track.get("@type") == "Audio":
|
|
74
|
+
sample_rate_str = track.get("SamplingRate")
|
|
75
|
+
if sample_rate_str:
|
|
76
|
+
# Handle formats like "44100 Hz"
|
|
77
|
+
if "Hz" in sample_rate_str:
|
|
78
|
+
return int(sample_rate_str.split()[0])
|
|
79
|
+
return int(sample_rate_str)
|
|
80
|
+
except Exception:
|
|
81
|
+
return None
|
|
82
|
+
else:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def get_channels(file_path: str | Path) -> int | None:
|
|
87
|
+
"""Get the number of channels of an audio file using mediainfo."""
|
|
88
|
+
try:
|
|
89
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
90
|
+
tracks = data.get("media", {}).get("track", [])
|
|
91
|
+
for track in tracks:
|
|
92
|
+
if track.get("@type") == "Audio":
|
|
93
|
+
channels_str = track.get("Channels")
|
|
94
|
+
if channels_str:
|
|
95
|
+
return int(channels_str)
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
else:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def get_file_size(file_path: str | Path) -> int | None:
|
|
103
|
+
"""Get the file size of an audio file in bytes using mediainfo."""
|
|
104
|
+
try:
|
|
105
|
+
data = TechnicalInfoInspector._run_mediainfo(file_path, "JSON")
|
|
106
|
+
tracks = data.get("media", {}).get("track", [])
|
|
107
|
+
for track in tracks:
|
|
108
|
+
if track.get("@type") == "General":
|
|
109
|
+
file_size_str = track.get("FileSize")
|
|
110
|
+
if file_size_str:
|
|
111
|
+
return int(file_size_str)
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
114
|
+
else:
|
|
115
|
+
return None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Consolidated temporary file with metadata utilities for testing.
|
|
2
|
+
|
|
3
|
+
This module provides a context manager for test files with metadata using contextlib.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .common import AudioFileCreator
|
|
12
|
+
from .id3v1 import ID3v1MetadataSetter
|
|
13
|
+
from .id3v2 import ID3v2MetadataSetter
|
|
14
|
+
from .riff import RIFFMetadataSetter
|
|
15
|
+
from .vorbis import VorbisMetadataSetter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def temp_file_with_metadata(metadata: dict, format_type: str) -> Generator[Path, None, None]:
|
|
20
|
+
"""Context manager for creating temporary test files with metadata.
|
|
21
|
+
|
|
22
|
+
This function creates a temporary audio file with the specified metadata,
|
|
23
|
+
yields its path for use in tests, and automatically cleans up the file.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
metadata: Dictionary of metadata to set on the test file
|
|
27
|
+
format_type: Audio format ('mp3', 'id3v1', 'id3v2.3', 'id3v2.4', 'flac', 'wav')
|
|
28
|
+
|
|
29
|
+
Yields:
|
|
30
|
+
Path to the created test file with metadata
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
with temp_file_with_metadata({"title": "Test Song"}, "mp3") as test_file:
|
|
34
|
+
metadata = get_unified_metadata(test_file)
|
|
35
|
+
"""
|
|
36
|
+
target_file = _create_test_file_with_metadata(metadata, format_type)
|
|
37
|
+
try:
|
|
38
|
+
yield target_file
|
|
39
|
+
finally:
|
|
40
|
+
if target_file.exists():
|
|
41
|
+
target_file.unlink()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _create_test_file_with_metadata(metadata: dict, format_type: str) -> Path:
|
|
45
|
+
"""Create a test file with specific metadata values.
|
|
46
|
+
|
|
47
|
+
This function uses external tools to set specific metadata values
|
|
48
|
+
without using the app's update functions, improving test isolation.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
metadata: Dictionary of metadata to set
|
|
52
|
+
format_type: Audio format ('mp3', 'id3v1', 'flac', 'wav')
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to the created file with metadata
|
|
56
|
+
"""
|
|
57
|
+
# Create temporary file with correct extension
|
|
58
|
+
# For id3v1, id3v2.3, id3v2.4, use .mp3 extension since they're still MP3 files
|
|
59
|
+
actual_extension = "mp3" if format_type.lower() in ["id3v1", "id3v2.3", "id3v2.4"] else format_type.lower()
|
|
60
|
+
with tempfile.NamedTemporaryFile(suffix=f".{actual_extension}", delete=False) as tmp_file:
|
|
61
|
+
target_file = Path(tmp_file.name)
|
|
62
|
+
|
|
63
|
+
assets_dir = Path(__file__).parent.parent.parent / "test" / "assets"
|
|
64
|
+
AudioFileCreator.create_minimal_audio_file(target_file, format_type, assets_dir)
|
|
65
|
+
|
|
66
|
+
if format_type.lower() == "mp3":
|
|
67
|
+
ID3v2MetadataSetter.set_metadata(target_file, metadata)
|
|
68
|
+
elif format_type.lower() == "id3v1":
|
|
69
|
+
ID3v1MetadataSetter.set_metadata(target_file, metadata)
|
|
70
|
+
elif format_type.lower() in ["id3v2.3", "id3v2.4"]:
|
|
71
|
+
# Use version-specific ID3v2 metadata setting
|
|
72
|
+
version = format_type.lower().replace("id3v2.", "2.")
|
|
73
|
+
ID3v2MetadataSetter.set_metadata(target_file, metadata, version)
|
|
74
|
+
elif format_type.lower() == "flac":
|
|
75
|
+
VorbisMetadataSetter.set_metadata(target_file, metadata)
|
|
76
|
+
elif format_type.lower() == "wav":
|
|
77
|
+
RIFFMetadataSetter.set_metadata(target_file, metadata)
|
|
78
|
+
else:
|
|
79
|
+
msg = f"Unsupported format type: {format_type}"
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
|
|
82
|
+
return target_file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Vorbis metadata format helpers."""
|
|
2
|
+
|
|
3
|
+
from .vorbis_header_verifier import VorbisHeaderVerifier
|
|
4
|
+
from .vorbis_metadata_deleter import VorbisMetadataDeleter
|
|
5
|
+
from .vorbis_metadata_getter import VorbisMetadataGetter
|
|
6
|
+
from .vorbis_metadata_setter import VorbisMetadataSetter
|
|
7
|
+
|
|
8
|
+
__all__ = ["VorbisMetadataGetter", "VorbisHeaderVerifier", "VorbisMetadataDeleter", "VorbisMetadataSetter"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Vorbis metadata header verification and information utilities."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from audiometa.utils.tool_path_resolver import get_tool_path
|
|
7
|
+
|
|
8
|
+
from ..common.external_tool_runner import run_external_tool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VorbisHeaderVerifier:
|
|
12
|
+
"""Utilities for verifying Vorbis metadata headers and retrieving metadata information from audio files."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def has_vorbis_comments(file_path: Path) -> bool:
|
|
16
|
+
"""Check if file has Vorbis comments using metaflac."""
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
[get_tool_path("metaflac"), "--list", str(file_path)], capture_output=True, text=True, check=True
|
|
20
|
+
)
|
|
21
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
22
|
+
return False
|
|
23
|
+
else:
|
|
24
|
+
return "VORBIS_COMMENT" in result.stdout
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def get_metadata_info(file_path: Path) -> str:
|
|
28
|
+
"""Get metadata info using metaflac --list command."""
|
|
29
|
+
command = [get_tool_path("metaflac"), "--list", str(file_path)]
|
|
30
|
+
result = run_external_tool(command, "metaflac")
|
|
31
|
+
return result.stdout
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Vorbis metadata deletion operations."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..common.external_tool_runner import run_external_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VorbisMetadataDeleter:
|
|
10
|
+
"""Static utility class for Vorbis metadata deletion using external metaflac tool."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def delete_tag(file_path: Path, tag_name: str) -> None:
|
|
14
|
+
"""Delete a specific Vorbis comment tag using metaflac tool."""
|
|
15
|
+
command = ["metaflac", "--remove-tag", tag_name, str(file_path)]
|
|
16
|
+
with contextlib.suppress(Exception):
|
|
17
|
+
run_external_tool(command, "metaflac")
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def delete_comment(file_path: Path) -> None:
|
|
21
|
+
VorbisMetadataDeleter.delete_tag(file_path, "COMMENT")
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def delete_title(file_path: Path) -> None:
|
|
25
|
+
VorbisMetadataDeleter.delete_tag(file_path, "TITLE")
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def delete_artist(file_path: Path) -> None:
|
|
29
|
+
VorbisMetadataDeleter.delete_tag(file_path, "ARTIST")
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def delete_album(file_path: Path) -> None:
|
|
33
|
+
VorbisMetadataDeleter.delete_tag(file_path, "ALBUM")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def delete_genre(file_path: Path) -> None:
|
|
37
|
+
VorbisMetadataDeleter.delete_tag(file_path, "GENRE")
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def delete_lyrics(file_path: Path) -> None:
|
|
41
|
+
VorbisMetadataDeleter.delete_tag(file_path, "UNSYNCHRONIZED_LYRICS")
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def delete_language(file_path: Path) -> None:
|
|
45
|
+
VorbisMetadataDeleter.delete_tag(file_path, "LANGUAGE")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def delete_bpm(file_path: Path) -> None:
|
|
49
|
+
VorbisMetadataDeleter.delete_tag(file_path, "BPM")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Vorbis metadata inspection utilities for testing audio file metadata."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from mutagen.flac import FLAC
|
|
7
|
+
|
|
8
|
+
from audiometa.utils.tool_path_resolver import get_tool_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VorbisMetadataGetter:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def get_raw_metadata(file_path: Path) -> str:
|
|
14
|
+
result = subprocess.run(
|
|
15
|
+
[get_tool_path("metaflac"), "--list", str(file_path)], capture_output=True, text=True, check=True
|
|
16
|
+
)
|
|
17
|
+
return result.stdout
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def get_raw_metadata_without_truncating_null_bytes_but_lower_case_keys(file_path: Path) -> str:
|
|
21
|
+
audio = FLAC(str(file_path))
|
|
22
|
+
lines = []
|
|
23
|
+
lines.append("METADATA block #0")
|
|
24
|
+
lines.append(" type: 0 (STREAMINFO)")
|
|
25
|
+
lines.append(" is last: false")
|
|
26
|
+
lines.append(" length: 34")
|
|
27
|
+
lines.append(" minimum blocksize: 4096 samples")
|
|
28
|
+
lines.append(" maximum blocksize: 4096 samples")
|
|
29
|
+
lines.append(" minimum framesize: 0 bytes")
|
|
30
|
+
lines.append(" maximum framesize: 0 bytes")
|
|
31
|
+
lines.append(" sample_rate: 48000 Hz")
|
|
32
|
+
lines.append(" channels: 1")
|
|
33
|
+
lines.append(" bits-per-sample: 24")
|
|
34
|
+
lines.append(" total samples: 26177")
|
|
35
|
+
lines.append(" MD5 signature: 07598496b7623dfea10aafb241fae1a8")
|
|
36
|
+
lines.append("METADATA block #1")
|
|
37
|
+
lines.append(" type: 4 (VORBIS_COMMENT)")
|
|
38
|
+
lines.append(" is last: false")
|
|
39
|
+
lines.append(" length: 72")
|
|
40
|
+
lines.append(" vendor string: ")
|
|
41
|
+
comments = []
|
|
42
|
+
if audio.tags is not None and hasattr(audio.tags, "keys"):
|
|
43
|
+
for key in sorted(audio.tags.keys()): # type: ignore[union-attr]
|
|
44
|
+
values = audio.tags[key] # type: ignore[union-attr]
|
|
45
|
+
if isinstance(values, list):
|
|
46
|
+
for value in values:
|
|
47
|
+
comments.append(f" comment[{len(comments)}]: {key}={value}")
|
|
48
|
+
else:
|
|
49
|
+
comments.append(f" comment[{len(comments)}]: {key}={values}")
|
|
50
|
+
lines.append(f" comments: {len(comments)}")
|
|
51
|
+
lines.extend(comments)
|
|
52
|
+
lines.append("METADATA block #2")
|
|
53
|
+
lines.append(" type: 1 (PADDING)")
|
|
54
|
+
lines.append(" is last: true")
|
|
55
|
+
lines.append(" length: 1076")
|
|
56
|
+
return "\n".join(lines)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def get_title(file_path: Path) -> str:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
[get_tool_path("metaflac"), "--show-tag=TITLE", str(file_path)], capture_output=True, text=True, check=True
|
|
62
|
+
)
|
|
63
|
+
# Output is like "TITLE=Song Title\n"
|
|
64
|
+
lines = result.stdout.strip().split("\n")
|
|
65
|
+
if lines and "=" in lines[0]:
|
|
66
|
+
return lines[0].split("=", 1)[1]
|
|
67
|
+
return ""
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Vorbis metadata setting operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mutagen.flac import FLAC
|
|
7
|
+
|
|
8
|
+
from ..common.external_tool_runner import run_external_tool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VorbisMetadataSetter:
|
|
12
|
+
"""Static utility class for Vorbis metadata setting using external tools."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def set_multiple_tags(
|
|
16
|
+
file_path: Path, tag_name: str, values: list[str], removing_existing=True, key_lower_case=False
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Set multiple Vorbis comment tags with the same name."""
|
|
19
|
+
|
|
20
|
+
if key_lower_case:
|
|
21
|
+
tag_name = tag_name.lower()
|
|
22
|
+
|
|
23
|
+
if removing_existing:
|
|
24
|
+
try:
|
|
25
|
+
command = ["metaflac", "--remove-tag", tag_name, str(file_path)]
|
|
26
|
+
run_external_tool(command, "metaflac")
|
|
27
|
+
except Exception:
|
|
28
|
+
# Ignore if tags don't exist
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
# Add each value as a separate tag
|
|
32
|
+
for value in values:
|
|
33
|
+
command = ["metaflac", "--set-tag", f"{tag_name}={value}", str(file_path)]
|
|
34
|
+
run_external_tool(command, "metaflac")
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def set_metadata(file_path: Path, metadata: dict[str, Any]) -> None:
|
|
38
|
+
"""Set FLAC metadata using metaflac tool."""
|
|
39
|
+
cmd = ["metaflac"]
|
|
40
|
+
|
|
41
|
+
# Map common metadata keys to metaflac arguments
|
|
42
|
+
key_mapping = {
|
|
43
|
+
"title": "TITLE",
|
|
44
|
+
"artist": "ARTIST",
|
|
45
|
+
"album": "ALBUM",
|
|
46
|
+
"date": "DATE",
|
|
47
|
+
"genre": "GENRE",
|
|
48
|
+
"comment": "COMMENT",
|
|
49
|
+
"track_number": "TRACKNUMBER",
|
|
50
|
+
"bpm": "BPM",
|
|
51
|
+
"composer": "COMPOSER",
|
|
52
|
+
"copyright": "COPYRIGHT",
|
|
53
|
+
"lyrics": "LYRICS",
|
|
54
|
+
"language": "LANGUAGE",
|
|
55
|
+
"rating": "RATING",
|
|
56
|
+
"album_artist": "ALBUMARTIST",
|
|
57
|
+
"mood": "MOOD",
|
|
58
|
+
"key": "KEY",
|
|
59
|
+
"encoder": "ENCODER",
|
|
60
|
+
"url": "URL",
|
|
61
|
+
"isrc": "ISRC",
|
|
62
|
+
"publisher": "PUBLISHER",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
metadata_added = False
|
|
66
|
+
for key, value in metadata.items():
|
|
67
|
+
if key.lower() in key_mapping:
|
|
68
|
+
cmd.extend([f"--set-tag={key_mapping[key.lower()]}={value}"])
|
|
69
|
+
metadata_added = True
|
|
70
|
+
|
|
71
|
+
# Only run metaflac if metadata was actually added
|
|
72
|
+
if metadata_added:
|
|
73
|
+
cmd.append(str(file_path))
|
|
74
|
+
run_external_tool(cmd, "metaflac")
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def set_genre(file_path: Path, genre_text: str) -> None:
|
|
78
|
+
command = ["metaflac", "--set-tag", f"GENRE={genre_text}", str(file_path)]
|
|
79
|
+
run_external_tool(command, "metaflac")
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def set_comment(file_path: Path, comment: str) -> None:
|
|
83
|
+
command = ["metaflac", "--set-tag", f"COMMENT={comment}", str(file_path)]
|
|
84
|
+
run_external_tool(command, "metaflac")
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def add_title(file_path: Path, title: str, key_lower_case=False) -> None:
|
|
88
|
+
if key_lower_case:
|
|
89
|
+
command = ["metaflac", "--set-tag", f"title={title}", str(file_path)]
|
|
90
|
+
else:
|
|
91
|
+
command = ["metaflac", "--set-tag", f"TITLE={title}", str(file_path)]
|
|
92
|
+
run_external_tool(command, "metaflac")
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def set_artist(file_path: Path, artist: str) -> None:
|
|
96
|
+
command = ["metaflac", "--set-tag", f"ARTIST={artist}", str(file_path)]
|
|
97
|
+
run_external_tool(command, "metaflac")
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def set_album(file_path: Path, album: str) -> None:
|
|
101
|
+
command = ["metaflac", "--set-tag", f"ALBUM={album}", str(file_path)]
|
|
102
|
+
run_external_tool(command, "metaflac")
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def set_lyrics(file_path: Path, lyrics: str) -> None:
|
|
106
|
+
command = ["metaflac", "--set-tag", f"LYRICS={lyrics}", str(file_path)]
|
|
107
|
+
run_external_tool(command, "metaflac")
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def set_language(file_path: Path, language: str) -> None:
|
|
111
|
+
command = ["metaflac", "--set-tag", f"LANGUAGE={language}", str(file_path)]
|
|
112
|
+
run_external_tool(command, "metaflac")
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def set_bpm(file_path: Path, bpm: int) -> None:
|
|
116
|
+
command = ["metaflac", "--set-tag", f"BPM={bpm}", str(file_path)]
|
|
117
|
+
run_external_tool(command, "metaflac")
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def set_release_date(file_path: Path, date_str: str) -> None:
|
|
121
|
+
"""Set Vorbis release date using DATE tag."""
|
|
122
|
+
command = ["metaflac", "--set-tag", f"DATE={date_str}", str(file_path)]
|
|
123
|
+
run_external_tool(command, "metaflac")
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def set_max_metadata(file_path: Path) -> None:
|
|
127
|
+
from pathlib import Path
|
|
128
|
+
|
|
129
|
+
from ..common.external_tool_runner import run_script
|
|
130
|
+
|
|
131
|
+
scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
|
|
132
|
+
run_script("set-vorbis-max-metadata.sh", file_path, scripts_dir)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def set_artists(
|
|
136
|
+
file_path: Path, artists: list[str], removing_existing=True, key_lower_case=False, in_single_entry=False
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Set multiple Vorbis artists using mutagen."""
|
|
139
|
+
audio = FLAC(str(file_path))
|
|
140
|
+
key = "artist" if key_lower_case else "ARTIST"
|
|
141
|
+
if removing_existing:
|
|
142
|
+
existing = []
|
|
143
|
+
else:
|
|
144
|
+
existing = audio.get(key, [])
|
|
145
|
+
if isinstance(existing, str):
|
|
146
|
+
existing = [existing]
|
|
147
|
+
if in_single_entry:
|
|
148
|
+
value = "\x00".join(artists)
|
|
149
|
+
existing.append(value)
|
|
150
|
+
else:
|
|
151
|
+
existing.extend(artists)
|
|
152
|
+
audio[key] = existing
|
|
153
|
+
audio.save()
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def set_album_artists(file_path: Path, album_artists: list[str]):
|
|
157
|
+
"""Set multiple Vorbis album artists using external metaflac tool."""
|
|
158
|
+
VorbisMetadataSetter.set_multiple_tags(file_path, "ALBUMARTIST", album_artists)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def set_composers(file_path: Path, composers: list[str]):
|
|
162
|
+
"""Set multiple Vorbis composers using external metaflac tool."""
|
|
163
|
+
VorbisMetadataSetter.set_multiple_tags(file_path, "COMPOSER", composers)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def set_genres(file_path: Path, genres: list[str]):
|
|
167
|
+
"""Set multiple Vorbis genres using external metaflac tool."""
|
|
168
|
+
VorbisMetadataSetter.set_multiple_tags(file_path, "GENRE", genres)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def set_performers(file_path: Path, performers: list[str]):
|
|
172
|
+
"""Set multiple Vorbis performers using external metaflac tool."""
|
|
173
|
+
VorbisMetadataSetter.set_multiple_tags(file_path, "PERFORMER", performers)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def set_multiple_comments(file_path: Path, comments: list[str]):
|
|
177
|
+
"""Set multiple Vorbis comments using external metaflac tool."""
|
|
178
|
+
VorbisMetadataSetter.set_multiple_tags(file_path, "COMMENT", comments)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def set_null_bytes_test_metadata(file_path: Path) -> None:
|
|
182
|
+
"""Set test metadata including null bytes for testing null byte handling."""
|
|
183
|
+
# First remove existing ARTIST tags
|
|
184
|
+
try:
|
|
185
|
+
command = ["metaflac", "--remove-tag=ARTIST", str(file_path)]
|
|
186
|
+
run_external_tool(command, "metaflac")
|
|
187
|
+
except Exception:
|
|
188
|
+
# Ignore if tags don't exist
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
# Set ARTIST tags, one with null bytes
|
|
192
|
+
command = ["metaflac", "--set-tag=ARTIST=Artist\x00with\x00nulls", str(file_path)]
|
|
193
|
+
run_external_tool(command, "metaflac")
|
|
194
|
+
|
|
195
|
+
command = ["metaflac", "--set-tag=ARTIST=Normal Artist", str(file_path)]
|
|
196
|
+
run_external_tool(command, "metaflac")
|
|
197
|
+
|
|
198
|
+
# Set TITLE
|
|
199
|
+
command = ["metaflac", "--set-tag=TITLE=Normal Title", str(file_path)]
|
|
200
|
+
run_external_tool(command, "metaflac")
|
|
File without changes
|