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.
Files changed (318) hide show
  1. audiometa/__init__.py +1240 -0
  2. audiometa/__main__.py +6 -0
  3. audiometa/_audio_file.py +602 -0
  4. audiometa/cli.py +347 -0
  5. audiometa/exceptions.py +167 -0
  6. audiometa/manager/_MetadataManager.py +665 -0
  7. audiometa/manager/__init__.py +1 -0
  8. audiometa/manager/_rating_supporting/_RatingSupportingMetadataManager.py +250 -0
  9. audiometa/manager/_rating_supporting/__init__.py +1 -0
  10. audiometa/manager/_rating_supporting/id3v2/_Id3v2Manager.py +945 -0
  11. audiometa/manager/_rating_supporting/id3v2/__init__.py +25 -0
  12. audiometa/manager/_rating_supporting/id3v2/_id3v2_constants.py +11 -0
  13. audiometa/manager/_rating_supporting/riff/_RiffManager.py +778 -0
  14. audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
  15. audiometa/manager/_rating_supporting/riff/_riff_constants.py +10 -0
  16. audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +525 -0
  17. audiometa/manager/_rating_supporting/vorbis/__init__.py +17 -0
  18. audiometa/manager/_rating_supporting/vorbis/_vorbis_constants.py +6 -0
  19. audiometa/manager/id3v1/_Id3v1Manager.py +512 -0
  20. audiometa/manager/id3v1/__init__.py +1 -0
  21. audiometa/manager/id3v1/_constants.py +8 -0
  22. audiometa/manager/id3v1/id3v1_raw_metadata.py +242 -0
  23. audiometa/manager/id3v1/id3v1_raw_metadata_key.py +13 -0
  24. audiometa/test/__init__.py +1 -0
  25. audiometa/test/assets/create_test_files.py +72 -0
  26. audiometa/test/helpers/__init__.py +51 -0
  27. audiometa/test/helpers/common/__init__.py +6 -0
  28. audiometa/test/helpers/common/audio_file_creator.py +68 -0
  29. audiometa/test/helpers/common/external_tool_runner.py +74 -0
  30. audiometa/test/helpers/id3v1/__init__.py +8 -0
  31. audiometa/test/helpers/id3v1/id3v1_header_verifier.py +18 -0
  32. audiometa/test/helpers/id3v1/id3v1_metadata_deleter.py +37 -0
  33. audiometa/test/helpers/id3v1/id3v1_metadata_getter.py +61 -0
  34. audiometa/test/helpers/id3v1/id3v1_metadata_setter.py +82 -0
  35. audiometa/test/helpers/id3v2/__init__.py +28 -0
  36. audiometa/test/helpers/id3v2/id3v2_frame_manual_creator.py +305 -0
  37. audiometa/test/helpers/id3v2/id3v2_header_verifier.py +38 -0
  38. audiometa/test/helpers/id3v2/id3v2_metadata_deleter.py +56 -0
  39. audiometa/test/helpers/id3v2/id3v2_metadata_getter.py +188 -0
  40. audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +428 -0
  41. audiometa/test/helpers/riff/__init__.py +8 -0
  42. audiometa/test/helpers/riff/riff_header_verifier.py +85 -0
  43. audiometa/test/helpers/riff/riff_manual_metadata_creator.py +216 -0
  44. audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
  45. audiometa/test/helpers/riff/riff_metadata_getter.py +118 -0
  46. audiometa/test/helpers/riff/riff_metadata_setter.py +196 -0
  47. audiometa/test/helpers/scripts/__init__.py +0 -0
  48. audiometa/test/helpers/technical_info_inspector.py +115 -0
  49. audiometa/test/helpers/temp_file_with_metadata.py +82 -0
  50. audiometa/test/helpers/vorbis/__init__.py +8 -0
  51. audiometa/test/helpers/vorbis/vorbis_header_verifier.py +31 -0
  52. audiometa/test/helpers/vorbis/vorbis_metadata_deleter.py +49 -0
  53. audiometa/test/helpers/vorbis/vorbis_metadata_getter.py +67 -0
  54. audiometa/test/helpers/vorbis/vorbis_metadata_setter.py +200 -0
  55. audiometa/test/tests/__init__.py +0 -0
  56. audiometa/test/tests/conftest.py +261 -0
  57. audiometa/test/tests/e2e/__init__.py +0 -0
  58. audiometa/test/tests/e2e/cli/__init__.py +0 -0
  59. audiometa/test/tests/e2e/cli/error_handling/__init__.py +1 -0
  60. audiometa/test/tests/e2e/cli/error_handling/test_command_structure_errors.py +77 -0
  61. audiometa/test/tests/e2e/cli/error_handling/test_file_access_errors.py +130 -0
  62. audiometa/test/tests/e2e/cli/error_handling/test_format_output_errors.py +118 -0
  63. audiometa/test/tests/e2e/cli/error_handling/test_input_validation_errors.py +172 -0
  64. audiometa/test/tests/e2e/cli/error_handling/test_missing_fields_validation.py +49 -0
  65. audiometa/test/tests/e2e/cli/error_handling/test_multiple_files_errors.py +160 -0
  66. audiometa/test/tests/e2e/cli/error_handling/test_rating_validation.py +90 -0
  67. audiometa/test/tests/e2e/cli/error_handling/test_year_validation.py +51 -0
  68. audiometa/test/tests/e2e/cli/test_cli_delete.py +20 -0
  69. audiometa/test/tests/e2e/cli/test_cli_formatting.py +31 -0
  70. audiometa/test/tests/e2e/cli/test_cli_help.py +40 -0
  71. audiometa/test/tests/e2e/cli/test_cli_read.py +79 -0
  72. audiometa/test/tests/e2e/cli/test_cli_unified.py +33 -0
  73. audiometa/test/tests/e2e/cli/test_cli_write.py +116 -0
  74. audiometa/test/tests/e2e/scenarios/__init__.py +0 -0
  75. audiometa/test/tests/e2e/scenarios/test_user_scenarios.py +166 -0
  76. audiometa/test/tests/e2e/workflows/__init__.py +0 -0
  77. audiometa/test/tests/e2e/workflows/test_core_workflows.py +166 -0
  78. audiometa/test/tests/e2e/workflows/test_deletion_workflows.py +318 -0
  79. audiometa/test/tests/e2e/workflows/test_error_handling_workflows.py +165 -0
  80. audiometa/test/tests/e2e/workflows/test_format_specific_workflows.py +129 -0
  81. audiometa/test/tests/e2e/workflows/test_rating_workflows.py +124 -0
  82. audiometa/test/tests/integration/__init__.py +0 -0
  83. audiometa/test/tests/integration/audio_format/__init__.py +0 -0
  84. audiometa/test/tests/integration/audio_format/flac/__init__.py +0 -0
  85. audiometa/test/tests/integration/audio_format/flac/test_flac_delete_all.py +108 -0
  86. audiometa/test/tests/integration/audio_format/flac/test_flac_reading_all.py +61 -0
  87. audiometa/test/tests/integration/audio_format/flac/test_flac_reading_field.py +65 -0
  88. audiometa/test/tests/integration/audio_format/flac/test_flac_writing.py +69 -0
  89. audiometa/test/tests/integration/audio_format/mp3/__init__.py +0 -0
  90. audiometa/test/tests/integration/audio_format/mp3/test_mp3_delete_all.py +79 -0
  91. audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_all.py +61 -0
  92. audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_field.py +67 -0
  93. audiometa/test/tests/integration/audio_format/mp3/test_mp3_writing.py +60 -0
  94. audiometa/test/tests/integration/audio_format/wav/__init__.py +0 -0
  95. audiometa/test/tests/integration/audio_format/wav/test_wav_delete_all.py +87 -0
  96. audiometa/test/tests/integration/audio_format/wav/test_wav_reading_all.py +62 -0
  97. audiometa/test/tests/integration/audio_format/wav/test_wav_reading_field.py +57 -0
  98. audiometa/test/tests/integration/audio_format/wav/test_wav_with_id3v2_tags.py +83 -0
  99. audiometa/test/tests/integration/audio_format/wav/test_wav_writing.py +62 -0
  100. audiometa/test/tests/integration/conftest.py +29 -0
  101. audiometa/test/tests/integration/delete_all_metadata/__init__.py +1 -0
  102. audiometa/test/tests/integration/delete_all_metadata/test_audio_format_all.py +102 -0
  103. audiometa/test/tests/integration/delete_all_metadata/test_audio_format_header_deletion.py +77 -0
  104. audiometa/test/tests/integration/delete_all_metadata/test_basic_functionality.py +47 -0
  105. audiometa/test/tests/integration/delete_all_metadata/test_error_handling.py +24 -0
  106. audiometa/test/tests/integration/encoding/__init__.py +1 -0
  107. audiometa/test/tests/integration/encoding/test_encoding.py +88 -0
  108. audiometa/test/tests/integration/encoding/test_special_characters_edge_cases.py +223 -0
  109. audiometa/test/tests/integration/get_full_metadata/__init__.py +0 -0
  110. audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +250 -0
  111. audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +297 -0
  112. audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata_edge_cases.py +231 -0
  113. audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
  114. audiometa/test/tests/integration/get_full_metadata/test_parsed_fields_keys.py +85 -0
  115. audiometa/test/tests/integration/metadata_field/__init__.py +0 -0
  116. audiometa/test/tests/integration/metadata_field/album/__init__.py +0 -0
  117. audiometa/test/tests/integration/metadata_field/album/test_deleting.py +73 -0
  118. audiometa/test/tests/integration/metadata_field/album/test_reading.py +36 -0
  119. audiometa/test/tests/integration/metadata_field/album/test_writing.py +50 -0
  120. audiometa/test/tests/integration/metadata_field/album_artists/__init__.py +0 -0
  121. audiometa/test/tests/integration/metadata_field/album_artists/test_deleting.py +83 -0
  122. audiometa/test/tests/integration/metadata_field/album_artists/test_reading.py +38 -0
  123. audiometa/test/tests/integration/metadata_field/album_artists/test_writing.py +52 -0
  124. audiometa/test/tests/integration/metadata_field/artists/__init__.py +0 -0
  125. audiometa/test/tests/integration/metadata_field/artists/test_deleting.py +68 -0
  126. audiometa/test/tests/integration/metadata_field/artists/test_reading.py +36 -0
  127. audiometa/test/tests/integration/metadata_field/artists/test_writing.py +46 -0
  128. audiometa/test/tests/integration/metadata_field/bpm/__init__.py +0 -0
  129. audiometa/test/tests/integration/metadata_field/bpm/test_deleting.py +75 -0
  130. audiometa/test/tests/integration/metadata_field/bpm/test_reading.py +32 -0
  131. audiometa/test/tests/integration/metadata_field/bpm/test_writing.py +56 -0
  132. audiometa/test/tests/integration/metadata_field/comment/__init__.py +0 -0
  133. audiometa/test/tests/integration/metadata_field/comment/test_deleting.py +68 -0
  134. audiometa/test/tests/integration/metadata_field/comment/test_reading.py +36 -0
  135. audiometa/test/tests/integration/metadata_field/comment/test_writing.py +49 -0
  136. audiometa/test/tests/integration/metadata_field/composer/__init__.py +0 -0
  137. audiometa/test/tests/integration/metadata_field/composer/test_deleting.py +75 -0
  138. audiometa/test/tests/integration/metadata_field/composer/test_reading.py +34 -0
  139. audiometa/test/tests/integration/metadata_field/composer/test_writing.py +41 -0
  140. audiometa/test/tests/integration/metadata_field/copyright/__init__.py +0 -0
  141. audiometa/test/tests/integration/metadata_field/copyright/test_deleting.py +81 -0
  142. audiometa/test/tests/integration/metadata_field/copyright/test_reading.py +35 -0
  143. audiometa/test/tests/integration/metadata_field/copyright/test_writing.py +41 -0
  144. audiometa/test/tests/integration/metadata_field/field_not_supported/__init__.py +0 -0
  145. audiometa/test/tests/integration/metadata_field/field_not_supported/test_deleting.py +56 -0
  146. audiometa/test/tests/integration/metadata_field/field_not_supported/test_reading.py +54 -0
  147. audiometa/test/tests/integration/metadata_field/field_not_supported/test_writing.py +61 -0
  148. audiometa/test/tests/integration/metadata_field/genre/__init__.py +0 -0
  149. audiometa/test/tests/integration/metadata_field/genre/reading/__init__.py +0 -0
  150. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/__init__.py +0 -0
  151. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v1_reading.py +65 -0
  152. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v2_reading.py +25 -0
  153. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_riff_reading.py +58 -0
  154. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_vorbis_reading.py +61 -0
  155. audiometa/test/tests/integration/metadata_field/genre/reading/test_smart_reading.py +191 -0
  156. audiometa/test/tests/integration/metadata_field/genre/test_deleting.py +62 -0
  157. audiometa/test/tests/integration/metadata_field/genre/test_writing.py +64 -0
  158. audiometa/test/tests/integration/metadata_field/language/__init__.py +0 -0
  159. audiometa/test/tests/integration/metadata_field/language/test_deleting.py +75 -0
  160. audiometa/test/tests/integration/metadata_field/language/test_reading.py +39 -0
  161. audiometa/test/tests/integration/metadata_field/language/test_writing.py +43 -0
  162. audiometa/test/tests/integration/metadata_field/lyrics/__init__.py +0 -0
  163. audiometa/test/tests/integration/metadata_field/lyrics/test_deleting.py +129 -0
  164. audiometa/test/tests/integration/metadata_field/lyrics/test_reading.py +57 -0
  165. audiometa/test/tests/integration/metadata_field/lyrics/test_writing.py +59 -0
  166. audiometa/test/tests/integration/metadata_field/publisher/__init__.py +0 -0
  167. audiometa/test/tests/integration/metadata_field/publisher/test_deleting.py +88 -0
  168. audiometa/test/tests/integration/metadata_field/publisher/test_reading.py +32 -0
  169. audiometa/test/tests/integration/metadata_field/publisher/test_writing.py +47 -0
  170. audiometa/test/tests/integration/metadata_field/rating/__init__.py +0 -0
  171. audiometa/test/tests/integration/metadata_field/rating/reading/__init__.py +0 -0
  172. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_100_proportional.py +81 -0
  173. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_non_proportional.py +33 -0
  174. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_proportional.py +58 -0
  175. audiometa/test/tests/integration/metadata_field/rating/test_deleting.py +117 -0
  176. audiometa/test/tests/integration/metadata_field/rating/test_error_handling.py +137 -0
  177. audiometa/test/tests/integration/metadata_field/rating/writing/__init__.py +0 -0
  178. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/__init__.py +0 -0
  179. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_id3v2.py +77 -0
  180. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_riff.py +55 -0
  181. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_vorbis.py +57 -0
  182. audiometa/test/tests/integration/metadata_field/rating/writing/test_comprehensive.py +192 -0
  183. audiometa/test/tests/integration/metadata_field/release_date/__init__.py +0 -0
  184. audiometa/test/tests/integration/metadata_field/release_date/test_deleting.py +74 -0
  185. audiometa/test/tests/integration/metadata_field/release_date/test_error_handling.py +82 -0
  186. audiometa/test/tests/integration/metadata_field/release_date/test_reading.py +59 -0
  187. audiometa/test/tests/integration/metadata_field/release_date/test_writing.py +49 -0
  188. audiometa/test/tests/integration/metadata_field/test_metadata_field_validation.py +134 -0
  189. audiometa/test/tests/integration/metadata_field/title/__init__.py +0 -0
  190. audiometa/test/tests/integration/metadata_field/title/test_deleting.py +73 -0
  191. audiometa/test/tests/integration/metadata_field/title/test_error_handling.py +47 -0
  192. audiometa/test/tests/integration/metadata_field/title/test_reading.py +36 -0
  193. audiometa/test/tests/integration/metadata_field/title/test_writing.py +64 -0
  194. audiometa/test/tests/integration/metadata_field/track_number/__init__.py +0 -0
  195. audiometa/test/tests/integration/metadata_field/track_number/reading/__init__.py +0 -0
  196. audiometa/test/tests/integration/metadata_field/track_number/reading/test_edge_cases.py +43 -0
  197. audiometa/test/tests/integration/metadata_field/track_number/reading/test_metadata_format.py +32 -0
  198. audiometa/test/tests/integration/metadata_field/track_number/test_deleting.py +59 -0
  199. audiometa/test/tests/integration/metadata_field/track_number/test_writing.py +73 -0
  200. audiometa/test/tests/integration/multiple_values/__init__.py +1 -0
  201. audiometa/test/tests/integration/multiple_values/reading/__init__.py +1 -0
  202. audiometa/test/tests/integration/multiple_values/reading/metadata_format/__init__.py +1 -0
  203. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v1.py +23 -0
  204. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_3.py +92 -0
  205. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_4.py +216 -0
  206. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_riff.py +84 -0
  207. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_vorbis.py +169 -0
  208. audiometa/test/tests/integration/multiple_values/reading/test_performance_large_data.py +209 -0
  209. audiometa/test/tests/integration/multiple_values/reading/test_smart_parsing_scenarios.py +198 -0
  210. audiometa/test/tests/integration/multiple_values/reading/test_unicode_handling.py +24 -0
  211. audiometa/test/tests/integration/multiple_values/writing/__init__.py +1 -0
  212. audiometa/test/tests/integration/multiple_values/writing/metadata_format/__init__.py +1 -0
  213. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v1.py +62 -0
  214. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_3.py +36 -0
  215. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_4.py +34 -0
  216. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_riff.py +32 -0
  217. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_vorbis.py +54 -0
  218. audiometa/test/tests/integration/multiple_values/writing/test_error_handling.py +42 -0
  219. audiometa/test/tests/integration/multiple_values/writing/test_large_values.py +98 -0
  220. audiometa/test/tests/integration/reading/__init__.py +1 -0
  221. audiometa/test/tests/integration/reading/test_read_multiple_metadata.py +80 -0
  222. audiometa/test/tests/integration/reading/test_reading_error_handling.py +36 -0
  223. audiometa/test/tests/integration/real_audio_files/__init__.py +0 -0
  224. audiometa/test/tests/integration/real_audio_files/test_reading.py +146 -0
  225. audiometa/test/tests/integration/real_audio_files/test_writing.py +198 -0
  226. audiometa/test/tests/integration/technical_info/__init__.py +0 -0
  227. audiometa/test/tests/integration/technical_info/flac_md5/__init__.py +0 -0
  228. audiometa/test/tests/integration/technical_info/flac_md5/conftest.py +103 -0
  229. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/__init__.py +0 -0
  230. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_audio_data_corruption.py +21 -0
  231. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_flipped_md5.py +29 -0
  232. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_non_flac_error.py +13 -0
  233. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_partial_md5.py +29 -0
  234. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_random_md5.py +29 -0
  235. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_unset_md5.py +56 -0
  236. audiometa/test/tests/integration/technical_info/flac_md5/test_valid_md5.py +21 -0
  237. audiometa/test/tests/integration/technical_info/test_bitrate.py +38 -0
  238. audiometa/test/tests/integration/technical_info/test_channels.py +38 -0
  239. audiometa/test/tests/integration/technical_info/test_duration_in_sec.py +38 -0
  240. audiometa/test/tests/integration/technical_info/test_sample_rate.py +40 -0
  241. audiometa/test/tests/integration/test_audio_file.py +23 -0
  242. audiometa/test/tests/integration/test_audio_format_readable_after_update_all_metadata_formats.py +95 -0
  243. audiometa/test/tests/integration/writing/__init__.py +0 -0
  244. audiometa/test/tests/integration/writing/test_error_handling.py +44 -0
  245. audiometa/test/tests/integration/writing/test_forced_format.py +224 -0
  246. audiometa/test/tests/integration/writing/test_multiple_format_preservation.py +223 -0
  247. audiometa/test/tests/integration/writing/test_partial_update.py +36 -0
  248. audiometa/test/tests/integration/writing/writing_strategies/__init__.py +0 -0
  249. audiometa/test/tests/integration/writing/writing_strategies/test_cleanup_strategy.py +79 -0
  250. audiometa/test/tests/integration/writing/writing_strategies/test_preserve_strategy.py +76 -0
  251. audiometa/test/tests/integration/writing/writing_strategies/test_sync_strategy.py +215 -0
  252. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/__init__.py +0 -0
  253. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_fail_behavior.py +42 -0
  254. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_no_writing_on_failure.py +93 -0
  255. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_strategy_specific.py +99 -0
  256. audiometa/test/tests/unit/__init__.py +0 -0
  257. audiometa/test/tests/unit/audio_file/__init__.py +0 -0
  258. audiometa/test/tests/unit/audio_file/technical_info/__init__.py +0 -0
  259. audiometa/test/tests/unit/audio_file/technical_info/test_bitrate.py +26 -0
  260. audiometa/test/tests/unit/audio_file/technical_info/test_channels.py +31 -0
  261. audiometa/test/tests/unit/audio_file/technical_info/test_duration_in_sec.py +38 -0
  262. audiometa/test/tests/unit/audio_file/technical_info/test_error_handling.py +130 -0
  263. audiometa/test/tests/unit/audio_file/technical_info/test_file_size.py +51 -0
  264. audiometa/test/tests/unit/audio_file/technical_info/test_format_name.py +28 -0
  265. audiometa/test/tests/unit/audio_file/technical_info/test_sample_rate.py +31 -0
  266. audiometa/test/tests/unit/audio_file/test_context_manager.py +30 -0
  267. audiometa/test/tests/unit/audio_file/test_file_validation.py +40 -0
  268. audiometa/test/tests/unit/audio_file/test_operations.py +20 -0
  269. audiometa/test/tests/unit/audio_file/test_path_handling.py +23 -0
  270. audiometa/test/tests/unit/cli/__init__.py +0 -0
  271. audiometa/test/tests/unit/cli/test_expand_file_patterns.py +234 -0
  272. audiometa/test/tests/unit/metadata_managers/__init__.py +0 -0
  273. audiometa/test/tests/unit/metadata_managers/conftest.py +142 -0
  274. audiometa/test/tests/unit/metadata_managers/header_info/__init__.py +0 -0
  275. audiometa/test/tests/unit/metadata_managers/header_info/test_id3v1.py +49 -0
  276. audiometa/test/tests/unit/metadata_managers/header_info/test_id3v2.py +66 -0
  277. audiometa/test/tests/unit/metadata_managers/header_info/test_riff.py +57 -0
  278. audiometa/test/tests/unit/metadata_managers/header_info/test_vorbis.py +53 -0
  279. audiometa/test/tests/unit/metadata_managers/metadata_field/__init__.py +0 -0
  280. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/__init__.py +0 -0
  281. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/__init__.py +0 -0
  282. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/test_smart_parsing.py +186 -0
  283. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/__init__.py +0 -0
  284. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_separator_selection.py +142 -0
  285. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_value_filtering.py +76 -0
  286. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/__init__.py +0 -0
  287. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/__init__.py +0 -0
  288. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_normalization.py +152 -0
  289. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_profiles_values.py +23 -0
  290. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/test_rating_validation.py +77 -0
  291. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/__init__.py +0 -0
  292. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_validation.py +151 -0
  293. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_writing_profiles.py +61 -0
  294. audiometa/test/tests/unit/metadata_managers/metadata_field/test_date_format_validation.py +135 -0
  295. audiometa/test/tests/unit/metadata_managers/metadata_field/test_track_number_validation.py +46 -0
  296. audiometa/test/tests/unit/metadata_managers/metadata_field/test_type_validation_exception.py +22 -0
  297. audiometa/test/tests/unit/metadata_managers/metadata_field/test_validation.py +83 -0
  298. audiometa/test/tests/unit/metadata_managers/test_metadata_format_managers_write_and_read.py +74 -0
  299. audiometa/utils/__init__.py +1 -0
  300. audiometa/utils/id3v1_genre_code_map.py +205 -0
  301. audiometa/utils/metadata_format.py +31 -0
  302. audiometa/utils/metadata_writing_strategy.py +16 -0
  303. audiometa/utils/os_dependencies_checker/__init__.py +24 -0
  304. audiometa/utils/os_dependencies_checker/base.py +62 -0
  305. audiometa/utils/os_dependencies_checker/config.py +51 -0
  306. audiometa/utils/os_dependencies_checker/macos.py +236 -0
  307. audiometa/utils/os_dependencies_checker/ubuntu.py +95 -0
  308. audiometa/utils/os_dependencies_checker/windows.py +227 -0
  309. audiometa/utils/rating_profiles.py +110 -0
  310. audiometa/utils/tool_path_resolver.py +135 -0
  311. audiometa/utils/types.py +82 -0
  312. audiometa/utils/unified_metadata_key.py +81 -0
  313. audiometa_python-0.2.2.dist-info/METADATA +1464 -0
  314. audiometa_python-0.2.2.dist-info/RECORD +318 -0
  315. audiometa_python-0.2.2.dist-info/WHEEL +5 -0
  316. audiometa_python-0.2.2.dist-info/entry_points.txt +2 -0
  317. audiometa_python-0.2.2.dist-info/licenses/LICENSE +202 -0
  318. audiometa_python-0.2.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from audiometa import get_sample_rate
6
+ from audiometa.test.helpers.technical_info_inspector import TechnicalInfoInspector
7
+
8
+
9
+ @pytest.mark.integration
10
+ class TestGetSampleRate:
11
+ def test_get_sample_rate_works_with_path_string(self, sample_mp3_file: Path):
12
+ sample_rate = get_sample_rate(str(sample_mp3_file))
13
+ assert isinstance(sample_rate, int)
14
+ assert sample_rate > 0
15
+
16
+ def test_get_sample_rate_works_with_pathlib_path(self, sample_mp3_file: Path):
17
+ sample_rate = get_sample_rate(sample_mp3_file)
18
+ assert isinstance(sample_rate, int)
19
+ assert sample_rate > 0
20
+
21
+ def test_get_sample_rate_matches_external_tool(self, sample_mp3_file: Path):
22
+ external_sample_rate = TechnicalInfoInspector.get_sample_rate(sample_mp3_file)
23
+ assert external_sample_rate is not None
24
+ assert isinstance(external_sample_rate, int)
25
+ assert external_sample_rate == 44100
26
+
27
+ sample_rate = get_sample_rate(sample_mp3_file)
28
+ assert sample_rate == external_sample_rate
29
+
30
+ def test_get_sample_rate_supports_all_formats(
31
+ self, sample_mp3_file: Path, sample_flac_file: Path, sample_wav_file: Path
32
+ ):
33
+ mp3_sample_rate = get_sample_rate(sample_mp3_file)
34
+ flac_sample_rate = get_sample_rate(sample_flac_file)
35
+ wav_sample_rate = get_sample_rate(sample_wav_file)
36
+
37
+ assert isinstance(mp3_sample_rate, int)
38
+ assert isinstance(flac_sample_rate, int)
39
+ assert isinstance(wav_sample_rate, int)
40
+ assert all(r > 0 for r in [mp3_sample_rate, flac_sample_rate, wav_sample_rate])
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from audiometa import get_unified_metadata, get_unified_metadata_field
6
+ from audiometa.utils.metadata_format import MetadataFormat
7
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
8
+
9
+
10
+ @pytest.mark.integration
11
+ class TestAudioFileIntegration:
12
+ def test_functional_api_with_file_path(self, sample_mp3_file: Path):
13
+ # Test that functional APIs work with file paths (string)
14
+ metadata = get_unified_metadata(str(sample_mp3_file))
15
+ assert isinstance(metadata, dict)
16
+
17
+ # Test single format with file path
18
+ id3v2_metadata = get_unified_metadata(str(sample_mp3_file), metadata_format=MetadataFormat.ID3V2)
19
+ assert isinstance(id3v2_metadata, dict)
20
+
21
+ # Test specific metadata with file path
22
+ title = get_unified_metadata_field(str(sample_mp3_file), UnifiedMetadataKey.TITLE)
23
+ assert title is None or isinstance(title, str)
@@ -0,0 +1,95 @@
1
+ import pytest
2
+
3
+ from audiometa import update_metadata
4
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
5
+ from audiometa.utils.metadata_format import MetadataFormat
6
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
7
+
8
+
9
+ @pytest.mark.integration
10
+ class TestAudioFormatReadableAfterUpdate:
11
+ def test_mp3_playable_after_id3v1_and_id3v2_updates(self):
12
+ """Test that MP3 file remains playable after updating with ID3v1 and ID3v2 tags."""
13
+ sf = pytest.importorskip("soundfile")
14
+
15
+ with temp_file_with_metadata({}, "mp3") as test_file:
16
+ # Update with ID3v2 metadata first
17
+ id3v2_metadata = {
18
+ UnifiedMetadataKey.TITLE: "ID3v2 Test Title",
19
+ UnifiedMetadataKey.ARTISTS: ["ID3v2 Test Artist"],
20
+ UnifiedMetadataKey.ALBUM: "ID3v2 Test Album",
21
+ UnifiedMetadataKey.RATING: 80,
22
+ }
23
+ update_metadata(
24
+ test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2, normalized_rating_max_value=100
25
+ )
26
+
27
+ # Update with ID3v1 metadata
28
+ id3v1_metadata = {
29
+ UnifiedMetadataKey.TITLE: "ID3v1 Test Title",
30
+ UnifiedMetadataKey.ARTISTS: ["ID3v1 Test Artist"],
31
+ UnifiedMetadataKey.ALBUM: "ID3v1 Test Album",
32
+ }
33
+ update_metadata(test_file, id3v1_metadata, metadata_format=MetadataFormat.ID3V1)
34
+
35
+ # Verify the track is still playable by trying to read audio frames
36
+ try:
37
+ with sf.SoundFile(str(test_file)) as f:
38
+ # Just try reading a few frames
39
+ frames = f.read(frames=1024)
40
+ assert f.samplerate > 0
41
+ assert f.channels > 0
42
+ assert len(frames) > 0
43
+ except RuntimeError as e:
44
+ pytest.fail(f"Audio file {test_file} could not be opened or decoded: {e}")
45
+
46
+ def test_flac_playable_after_vorbis_updates(self):
47
+ """Test that FLAC file remains playable after updating with Vorbis tags."""
48
+ sf = pytest.importorskip("soundfile")
49
+
50
+ with temp_file_with_metadata({}, "flac") as test_file:
51
+ # Update with Vorbis metadata
52
+ vorbis_metadata = {
53
+ UnifiedMetadataKey.TITLE: "Vorbis Test Title",
54
+ UnifiedMetadataKey.ARTISTS: ["Vorbis Test Artist"],
55
+ UnifiedMetadataKey.ALBUM: "Vorbis Test Album",
56
+ UnifiedMetadataKey.RATING: 80,
57
+ }
58
+ update_metadata(
59
+ test_file, vorbis_metadata, metadata_format=MetadataFormat.VORBIS, normalized_rating_max_value=100
60
+ )
61
+
62
+ # Verify the track is still playable by trying to read audio frames
63
+ try:
64
+ with sf.SoundFile(str(test_file)) as f:
65
+ # Just try reading a few frames
66
+ frames = f.read(frames=1024)
67
+ assert f.samplerate > 0
68
+ assert f.channels > 0
69
+ assert len(frames) > 0
70
+ except RuntimeError as e:
71
+ pytest.fail(f"Audio file {test_file} could not be opened or decoded: {e}")
72
+
73
+ def test_wav_playable_after_riff_updates(self):
74
+ """Test that WAV file remains playable after updating with RIFF tags."""
75
+ sf = pytest.importorskip("soundfile")
76
+
77
+ with temp_file_with_metadata({}, "wav") as test_file:
78
+ # Update with RIFF metadata
79
+ riff_metadata = {
80
+ UnifiedMetadataKey.TITLE: "RIFF Test Title",
81
+ UnifiedMetadataKey.ARTISTS: ["RIFF Test Artist"],
82
+ UnifiedMetadataKey.ALBUM: "RIFF Test Album",
83
+ }
84
+ update_metadata(test_file, riff_metadata, metadata_format=MetadataFormat.RIFF)
85
+
86
+ # Verify the track is still playable by trying to read audio frames
87
+ try:
88
+ with sf.SoundFile(str(test_file)) as f:
89
+ # Just try reading a few frames
90
+ frames = f.read(frames=1024)
91
+ assert f.samplerate > 0
92
+ assert f.channels > 0
93
+ assert len(frames) > 0
94
+ except RuntimeError as e:
95
+ pytest.fail(f"Audio file {test_file} could not be opened or decoded: {e}")
File without changes
@@ -0,0 +1,44 @@
1
+ import pytest
2
+
3
+ from audiometa import delete_all_metadata, update_metadata
4
+ from audiometa.exceptions import FileTypeNotSupportedError, MetadataWritingConflictParametersError
5
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
6
+ from audiometa.utils.metadata_format import MetadataFormat
7
+ from audiometa.utils.metadata_writing_strategy import MetadataWritingStrategy
8
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
9
+
10
+
11
+ @pytest.mark.integration
12
+ class TestWritingErrorHandling:
13
+ def test_unsupported_file_type_raises_error(self):
14
+ # Create a file with unsupported extension using temp_file_with_metadata
15
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
16
+ temp_file_path.write_bytes(b"fake audio content")
17
+ temp_txt = temp_file_path.with_suffix(".txt")
18
+ temp_txt.write_bytes(b"fake audio content")
19
+
20
+ with pytest.raises(FileTypeNotSupportedError):
21
+ update_metadata(str(temp_txt), {UnifiedMetadataKey.TITLE: "Test"})
22
+
23
+ with pytest.raises(FileTypeNotSupportedError):
24
+ delete_all_metadata(str(temp_txt))
25
+
26
+ def test_nonexistent_file_raises_error(self):
27
+ nonexistent_file = "nonexistent_file.mp3"
28
+
29
+ with pytest.raises(FileNotFoundError):
30
+ update_metadata(nonexistent_file, {UnifiedMetadataKey.TITLE: "Test"})
31
+
32
+ # Note: delete_all_metadata error handling tests have been moved to test_delete_all_metadata.py
33
+
34
+ def test_metadata_writing_conflict_parameters_error_both_strategy_and_format(self):
35
+ with temp_file_with_metadata({}, "mp3") as test_file:
36
+ with pytest.raises(MetadataWritingConflictParametersError) as exc_info:
37
+ update_metadata(
38
+ test_file,
39
+ {UnifiedMetadataKey.TITLE: "Test"},
40
+ metadata_strategy=MetadataWritingStrategy.SYNC,
41
+ metadata_format=MetadataFormat.ID3V2,
42
+ )
43
+ assert "metadata_strategy" in str(exc_info.value).lower()
44
+ assert "metadata_format" in str(exc_info.value).lower()
@@ -0,0 +1,224 @@
1
+ """Tests for forced format parameter functionality.
2
+
3
+ This module tests the behavior when a specific metadata format is forced using the metadata_format parameter in
4
+ update_metadata.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from audiometa import get_unified_metadata, update_metadata
10
+ from audiometa.exceptions import (
11
+ MetadataFieldNotSupportedByMetadataFormatError,
12
+ MetadataFormatNotSupportedByAudioFormatError,
13
+ MetadataWritingConflictParametersError,
14
+ )
15
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
16
+ from audiometa.utils.metadata_format import MetadataFormat
17
+ from audiometa.utils.metadata_writing_strategy import MetadataWritingStrategy
18
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
19
+
20
+
21
+ @pytest.mark.integration
22
+ class TestForcedFormat:
23
+ def test_forced_format_writes_only_to_specified_format(self):
24
+ # Create WAV file with initial RIFF metadata
25
+ initial_metadata = {"title": "Original RIFF Title", "artist": "Original RIFF Artist"}
26
+ with temp_file_with_metadata(initial_metadata, "wav") as test_file:
27
+ # Add ID3v2 metadata using the library directly
28
+ id3v2_metadata = {
29
+ UnifiedMetadataKey.TITLE: "Original ID3v2 Title",
30
+ UnifiedMetadataKey.ARTISTS: ["Original ID3v2 Artist"],
31
+ }
32
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
33
+
34
+ # Verify both formats have their respective metadata
35
+ riff_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
36
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
37
+ assert riff_before.get(UnifiedMetadataKey.TITLE) == "Original RIFF Title"
38
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) == "Original ID3v2 Title"
39
+
40
+ # Force ID3v2 format (should write only to ID3v2, leave RIFF untouched)
41
+ new_metadata = {
42
+ UnifiedMetadataKey.TITLE: "New ID3v2 Title",
43
+ UnifiedMetadataKey.ARTISTS: ["New ID3v2 Artist"],
44
+ }
45
+
46
+ # This should write only to ID3v2 format, leaving RIFF unchanged
47
+ update_metadata(test_file, new_metadata, metadata_format=MetadataFormat.ID3V2)
48
+
49
+ # Verify ID3v2 has new metadata
50
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
51
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "New ID3v2 Title"
52
+ assert id3v2_after.get(UnifiedMetadataKey.ARTISTS) == ["New ID3v2 Artist"]
53
+
54
+ # Verify RIFF still has original metadata (forced format doesn't affect other formats)
55
+ riff_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
56
+ assert riff_after.get(UnifiedMetadataKey.TITLE) == "Original RIFF Title"
57
+ assert riff_after.get(UnifiedMetadataKey.ARTISTS) == ["Original RIFF Artist"]
58
+
59
+ def test_forced_format_fails_fast_on_unsupported_fields(self):
60
+ # Create WAV file with basic metadata
61
+ initial_metadata = {"title": "Test Title", "artist": "Test Artist"}
62
+ with temp_file_with_metadata(initial_metadata, "wav") as test_file:
63
+ # Try to write ARCHIVAL_LOCATION to RIFF format (not supported)
64
+ unsupported_metadata = {
65
+ UnifiedMetadataKey.TITLE: "Test Title",
66
+ UnifiedMetadataKey.ARCHIVAL_LOCATION: "some location", # ARCHIVAL_LOCATION not supported by RIFF format
67
+ }
68
+
69
+ # This should raise MetadataFieldNotSupportedByMetadataFormatError because format is forced
70
+ with pytest.raises(
71
+ MetadataFieldNotSupportedByMetadataFormatError,
72
+ match="UnifiedMetadataKey.ARCHIVAL_LOCATION metadata not supported by RIFF format",
73
+ ):
74
+ update_metadata(test_file, unsupported_metadata, metadata_format=MetadataFormat.RIFF)
75
+
76
+ def test_forced_format_succeeds_with_supported_fields(self):
77
+ # Create WAV file with basic metadata
78
+ initial_metadata = {"title": "Test Title", "artist": "Test Artist"}
79
+ with temp_file_with_metadata(initial_metadata, "wav") as test_file:
80
+ # Write supported fields to RIFF format
81
+ supported_metadata = {
82
+ UnifiedMetadataKey.TITLE: "New RIFF Title",
83
+ UnifiedMetadataKey.ARTISTS: ["New RIFF Artist"],
84
+ UnifiedMetadataKey.ALBUM: "New RIFF Album",
85
+ }
86
+
87
+ # This should succeed because all fields are supported by RIFF
88
+ update_metadata(test_file, supported_metadata, metadata_format=MetadataFormat.RIFF)
89
+
90
+ # Verify RIFF has new metadata
91
+ riff_metadata = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
92
+ assert riff_metadata.get(UnifiedMetadataKey.TITLE) == "New RIFF Title"
93
+ assert riff_metadata.get(UnifiedMetadataKey.ARTISTS) == ["New RIFF Artist"]
94
+ assert riff_metadata.get(UnifiedMetadataKey.ALBUM) == "New RIFF Album"
95
+
96
+ def test_forced_format_parameter_conflict_error(self):
97
+ # Create WAV file with basic metadata
98
+ initial_metadata = {"title": "Test Title", "artist": "Test Artist"}
99
+ with temp_file_with_metadata(initial_metadata, "wav") as test_file:
100
+ metadata = {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.ARTISTS: ["Test Artist"]}
101
+
102
+ # This should raise error because both parameters are specified
103
+ with pytest.raises(
104
+ MetadataWritingConflictParametersError,
105
+ match="Cannot specify both metadata_strategy and metadata_format",
106
+ ):
107
+ update_metadata(
108
+ test_file,
109
+ metadata,
110
+ metadata_format=MetadataFormat.RIFF,
111
+ metadata_strategy=MetadataWritingStrategy.SYNC,
112
+ )
113
+
114
+ def test_forced_format_with_unsupported_file_type(self):
115
+ # Create MP3 file
116
+ initial_metadata = {"title": "Test Title", "artist": "Test Artist"}
117
+ with temp_file_with_metadata(initial_metadata, "mp3") as test_file:
118
+ metadata = {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.ARTISTS: ["Test Artist"]}
119
+
120
+ # Try to force Vorbis format on MP3 file (not supported)
121
+ with pytest.raises(
122
+ MetadataFormatNotSupportedByAudioFormatError,
123
+ match="Tag format MetadataFormat.VORBIS not supported for file extension .mp3",
124
+ ):
125
+ update_metadata(test_file, metadata, metadata_format=MetadataFormat.VORBIS)
126
+
127
+ def test_forced_format_id3v1_writing_support(self):
128
+ # Create MP3 file with basic metadata
129
+ initial_metadata = {"title": "Test Title", "artist": "Test Artist"}
130
+ with temp_file_with_metadata(initial_metadata, "mp3") as test_file:
131
+ metadata = {UnifiedMetadataKey.TITLE: "New ID3v1 Title", UnifiedMetadataKey.ARTISTS: ["New ID3v1 Artist"]}
132
+
133
+ update_metadata(test_file, metadata, metadata_format=MetadataFormat.ID3V1)
134
+
135
+ # Verify the metadata was written
136
+ from audiometa import get_unified_metadata
137
+
138
+ result = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
139
+ assert result.get(UnifiedMetadataKey.TITLE) == "New ID3v1 Title"
140
+ assert result.get(UnifiedMetadataKey.ARTISTS) == ["New ID3v1 Artist"]
141
+
142
+ def test_forced_format_multiple_formats_present(self):
143
+ # Create WAV file with both RIFF and ID3v2 metadata
144
+ initial_metadata = {"title": "Original RIFF Title", "artist": "Original RIFF Artist"}
145
+ with temp_file_with_metadata(initial_metadata, "wav") as test_file:
146
+ # Add ID3v2 metadata using the library directly
147
+ id3v2_metadata = {
148
+ UnifiedMetadataKey.TITLE: "Original ID3v2 Title",
149
+ UnifiedMetadataKey.ARTISTS: ["Original ID3v2 Artist"],
150
+ }
151
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
152
+
153
+ # Verify both formats have original metadata
154
+ riff_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
155
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
156
+ assert riff_before.get(UnifiedMetadataKey.TITLE) == "Original RIFF Title"
157
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) == "Original ID3v2 Title"
158
+
159
+ # Force ID3v2 format - should only update ID3v2
160
+ new_metadata = {
161
+ UnifiedMetadataKey.TITLE: "New ID3v2 Title",
162
+ UnifiedMetadataKey.ARTISTS: ["New ID3v2 Artist"],
163
+ }
164
+
165
+ update_metadata(test_file, new_metadata, metadata_format=MetadataFormat.ID3V2)
166
+
167
+ # Verify ID3v2 was updated
168
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
169
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "New ID3v2 Title"
170
+ assert id3v2_after.get(UnifiedMetadataKey.ARTISTS) == ["New ID3v2 Artist"]
171
+
172
+ # Verify RIFF was NOT updated (forced format only affects specified format)
173
+ riff_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
174
+ assert riff_after.get(UnifiedMetadataKey.TITLE) == "Original RIFF Title"
175
+ assert riff_after.get(UnifiedMetadataKey.ARTISTS) == ["Original RIFF Artist"]
176
+
177
+ def test_forced_format_with_rating_field(self):
178
+ # Create MP3 file with basic metadata
179
+ initial_metadata = {"title": "Test Title", "artist": "Test Artist"}
180
+ with temp_file_with_metadata(initial_metadata, "mp3") as test_file:
181
+ # Write rating to ID3v2 format with proper configuration
182
+ metadata_with_rating = {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.RATING: 80}
183
+
184
+ # This should succeed with proper configuration
185
+ update_metadata(
186
+ test_file,
187
+ metadata_with_rating,
188
+ metadata_format=MetadataFormat.ID3V2,
189
+ normalized_rating_max_value=100,
190
+ )
191
+
192
+ # Verify rating was written (converted from 0-100 to 0-255 scale)
193
+ id3v2_metadata = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
194
+ assert id3v2_metadata.get(UnifiedMetadataKey.TITLE) == "Test Title"
195
+ # Rating 85 on 0-100 scale becomes 196 on 0-255 scale (85 * 255 / 100 = 216.75, rounded to 196)
196
+ assert id3v2_metadata.get(UnifiedMetadataKey.RATING) == 196
197
+
198
+ # Note: delete_all_metadata tests have been moved to test_delete_all_metadata.py
199
+
200
+ def test_forced_format_validation_before_writing(self):
201
+ # Create WAV file with initial metadata
202
+ initial_metadata = {"title": "Original Title", "artist": "Original Artist"}
203
+ with temp_file_with_metadata(initial_metadata, "wav") as test_file:
204
+ # Try to write mixed supported/unsupported fields to RIFF format
205
+ mixed_metadata = {
206
+ UnifiedMetadataKey.TITLE: "New Title", # Supported
207
+ UnifiedMetadataKey.ARTISTS: ["New Artist"], # Supported
208
+ UnifiedMetadataKey.ARCHIVAL_LOCATION: "some location", # NOT supported by RIFF
209
+ UnifiedMetadataKey.ALBUM: "New Album", # Supported
210
+ }
211
+
212
+ # This should fail because ARCHIVAL_LOCATION is not supported by RIFF
213
+ with pytest.raises(
214
+ MetadataFieldNotSupportedByMetadataFormatError,
215
+ match="UnifiedMetadataKey.ARCHIVAL_LOCATION metadata not supported by RIFF format",
216
+ ):
217
+ update_metadata(test_file, mixed_metadata, metadata_format=MetadataFormat.RIFF)
218
+
219
+ # Verify NO changes were made to the file (validation happens before writing)
220
+ final_metadata = get_unified_metadata(test_file)
221
+ assert final_metadata.get(UnifiedMetadataKey.TITLE) == "Original Title"
222
+ assert final_metadata.get(UnifiedMetadataKey.ARTISTS) == ["Original Artist"]
223
+ assert final_metadata.get(UnifiedMetadataKey.BPM) is None
224
+ assert final_metadata.get(UnifiedMetadataKey.ALBUM) is None
@@ -0,0 +1,223 @@
1
+ """Tests for multiple format preservation when updating metadata.
2
+
3
+ This module tests that updating fields in one format with the PRESERVE strategy does not affect other formats. It covers
4
+ the main working combinations.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from audiometa import get_unified_metadata, update_metadata
10
+ from audiometa.test.helpers.id3v1 import ID3v1MetadataSetter
11
+ from audiometa.test.helpers.id3v2 import ID3v2MetadataSetter
12
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
13
+ from audiometa.utils.metadata_format import MetadataFormat
14
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
15
+
16
+
17
+ @pytest.mark.integration
18
+ class TestMultipleFormatPreservation:
19
+ def test_id3v1_preserves_id3v2(self):
20
+ with temp_file_with_metadata({"title": "ID3v2 Title", "artist": "ID3v2 Artist"}, "mp3") as test_file:
21
+ ID3v2MetadataSetter.set_metadata(test_file, {"title": "ID3v2 Title"})
22
+ ID3v2MetadataSetter.set_metadata(test_file, {"artist": "ID3v2 Artist"})
23
+
24
+ # Verify initial state
25
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
26
+ id3v1_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
27
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
28
+ assert id3v1_before.get(UnifiedMetadataKey.TITLE) is None
29
+
30
+ # Update ID3v1 with PRESERVE strategy (specify ID3v1 format)
31
+ id3v1_metadata = {UnifiedMetadataKey.TITLE: "ID3v1 Title", UnifiedMetadataKey.ARTISTS: ["ID3v1 Artist"]}
32
+ update_metadata(test_file, id3v1_metadata, metadata_format=MetadataFormat.ID3V1)
33
+
34
+ # Verify ID3v1 was updated and ID3v2 was preserved
35
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
36
+ id3v1_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
37
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
38
+ assert id3v1_after.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
39
+
40
+ def test_id3v2_preserves_id3v1(self):
41
+ with temp_file_with_metadata({"title": "ID3v1 Title", "artist": "ID3v1 Artist"}, "id3v1") as test_file:
42
+ # Verify initial state
43
+ id3v1_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
44
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
45
+ assert id3v1_before.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
46
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) is None
47
+
48
+ id3v2_metadata = {UnifiedMetadataKey.TITLE: "ID3v2 Title", UnifiedMetadataKey.ARTISTS: ["ID3v2 Artist"]}
49
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
50
+
51
+ # Verify ID3v2 was updated and ID3v1 was preserved
52
+ id3v1_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
53
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
54
+ assert id3v1_after.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
55
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
56
+
57
+ def test_riff_preserves_id3v1(self):
58
+ with temp_file_with_metadata({}, "wav") as test_file:
59
+ ID3v1MetadataSetter.set_title(test_file, "ID3v1 Title")
60
+ ID3v1MetadataSetter.set_artist(test_file, "ID3v1 Artist")
61
+
62
+ # Verify initial state
63
+ id3v1_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
64
+ riff_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
65
+ assert id3v1_before.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
66
+ assert riff_before.get(UnifiedMetadataKey.TITLE) is None
67
+
68
+ riff_metadata = {UnifiedMetadataKey.TITLE: "RIFF Title", UnifiedMetadataKey.ARTISTS: ["RIFF Artist"]}
69
+ update_metadata(test_file, riff_metadata, metadata_format=MetadataFormat.RIFF)
70
+
71
+ # Verify RIFF was updated and ID3v1 was preserved
72
+ id3v1_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
73
+ riff_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
74
+ assert id3v1_after.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
75
+ assert riff_after.get(UnifiedMetadataKey.TITLE) == "RIFF Title"
76
+
77
+ def test_id3v1_preserves_riff(self):
78
+ with temp_file_with_metadata({"title": "RIFF Title", "artist": "RIFF Artist"}, "wav") as test_file:
79
+ # Verify initial state
80
+ riff_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
81
+ id3v1_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
82
+ assert riff_before.get(UnifiedMetadataKey.TITLE) == "RIFF Title"
83
+ assert id3v1_before.get(UnifiedMetadataKey.TITLE) is None
84
+
85
+ # Update ID3v1 with PRESERVE strategy (specify ID3v1 format)
86
+ id3v1_metadata = {UnifiedMetadataKey.TITLE: "ID3v1 Title", UnifiedMetadataKey.ARTISTS: ["ID3v1 Artist"]}
87
+ update_metadata(test_file, id3v1_metadata, metadata_format=MetadataFormat.ID3V1)
88
+
89
+ # Verify ID3v1 was updated and RIFF was preserved
90
+ riff_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
91
+ id3v1_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
92
+ assert riff_after.get(UnifiedMetadataKey.TITLE) == "RIFF Title"
93
+ assert id3v1_after.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
94
+
95
+ def test_riff_preserves_id3v2(self):
96
+ with temp_file_with_metadata({}, "wav") as test_file:
97
+ ID3v2MetadataSetter.set_metadata(test_file, {"title": "ID3v2 Title"})
98
+ ID3v2MetadataSetter.set_metadata(test_file, {"artist": "ID3v2 Artist"})
99
+
100
+ # Verify initial state
101
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
102
+ riff_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
103
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
104
+ assert riff_before.get(UnifiedMetadataKey.TITLE) is None
105
+
106
+ # Update RIFF with PRESERVE strategy (specify RIFF format)
107
+ riff_metadata = {UnifiedMetadataKey.TITLE: "RIFF Title", UnifiedMetadataKey.ARTISTS: ["RIFF Artist"]}
108
+ update_metadata(test_file, riff_metadata, metadata_format=MetadataFormat.RIFF)
109
+
110
+ # Verify RIFF was updated and ID3v2 was preserved
111
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
112
+ riff_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
113
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
114
+ assert riff_after.get(UnifiedMetadataKey.TITLE) == "RIFF Title"
115
+
116
+ def test_id3v2_preserves_riff(self):
117
+ with temp_file_with_metadata({"title": "RIFF Title", "artist": "RIFF Artist"}, "wav") as test_file:
118
+ # Verify initial state
119
+ riff_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
120
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
121
+ assert riff_before.get(UnifiedMetadataKey.TITLE) == "RIFF Title"
122
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) is None
123
+
124
+ # Update ID3v2 with PRESERVE strategy (specify ID3v2 format)
125
+ id3v2_metadata = {UnifiedMetadataKey.TITLE: "ID3v2 Title", UnifiedMetadataKey.ARTISTS: ["ID3v2 Artist"]}
126
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
127
+
128
+ # Verify ID3v2 was updated and RIFF was preserved
129
+ riff_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.RIFF)
130
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
131
+ assert riff_after.get(UnifiedMetadataKey.TITLE) == "RIFF Title"
132
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
133
+
134
+ def test_vorbis_preserves_id3v2(self):
135
+ with temp_file_with_metadata({}, "flac") as test_file:
136
+ ID3v2MetadataSetter.set_metadata(test_file, {"title": "ID3v2 Title"})
137
+ ID3v2MetadataSetter.set_metadata(test_file, {"artist": "ID3v2 Artist"})
138
+
139
+ # Verify initial state
140
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
141
+ vorbis_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.VORBIS)
142
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
143
+ assert vorbis_before.get(UnifiedMetadataKey.TITLE) is None
144
+
145
+ # Update Vorbis with PRESERVE strategy (specify Vorbis format)
146
+ vorbis_metadata = {UnifiedMetadataKey.TITLE: "Vorbis Title", UnifiedMetadataKey.ARTISTS: ["Vorbis Artist"]}
147
+ update_metadata(test_file, vorbis_metadata, metadata_format=MetadataFormat.VORBIS)
148
+
149
+ # Verify Vorbis was updated and ID3v2 was preserved
150
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
151
+ vorbis_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.VORBIS)
152
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
153
+ assert vorbis_after.get(UnifiedMetadataKey.TITLE) == "Vorbis Title"
154
+
155
+ def test_id3v2_preserves_vorbis(self):
156
+ with temp_file_with_metadata({"title": "Vorbis Title", "artist": "Vorbis Artist"}, "flac") as test_file:
157
+ # Verify initial state
158
+ vorbis_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.VORBIS)
159
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
160
+ assert vorbis_before.get(UnifiedMetadataKey.TITLE) == "Vorbis Title"
161
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) is None
162
+
163
+ # Update ID3v2 with PRESERVE strategy (specify ID3v2 format)
164
+ id3v2_metadata = {UnifiedMetadataKey.TITLE: "ID3v2 Title", UnifiedMetadataKey.ARTISTS: ["ID3v2 Artist"]}
165
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
166
+
167
+ # Verify ID3v2 was updated and Vorbis was preserved
168
+ vorbis_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.VORBIS)
169
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
170
+ assert vorbis_after.get(UnifiedMetadataKey.TITLE) == "Vorbis Title"
171
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
172
+
173
+ def test_multiple_fields_preservation(self):
174
+ with temp_file_with_metadata({"title": "Original Title", "artist": "Original Artist"}, "mp3") as test_file:
175
+ ID3v1MetadataSetter.set_title(test_file, "ID3v1 Title")
176
+ ID3v1MetadataSetter.set_artist(test_file, "ID3v1 Artist")
177
+ # Use ID3v2.3 instead of ID3v2.4 to preserve ID3v1 metadata
178
+ ID3v2MetadataSetter.set_metadata(test_file, {"title": "ID3v2 Title"}, version="2.3")
179
+ ID3v2MetadataSetter.set_metadata(test_file, {"artist": "ID3v2 Artist"}, version="2.3")
180
+
181
+ # Verify initial state
182
+ id3v1_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
183
+ id3v2_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
184
+ assert id3v1_before.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
185
+ assert id3v2_before.get(UnifiedMetadataKey.TITLE) == "ID3v2 Title"
186
+
187
+ # Update only ID3v2 with multiple fields
188
+ id3v2_metadata = {
189
+ UnifiedMetadataKey.TITLE: "Updated ID3v2 Title",
190
+ UnifiedMetadataKey.ARTISTS: ["Updated ID3v2 Artist"],
191
+ UnifiedMetadataKey.ALBUM: "ID3v2 Album",
192
+ }
193
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
194
+
195
+ # Verify ID3v2 was updated and ID3v1 was preserved
196
+ id3v1_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
197
+ id3v2_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V2)
198
+ assert id3v1_after.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
199
+ assert id3v1_after.get(UnifiedMetadataKey.ARTISTS) == ["ID3v1 Artist"]
200
+ assert id3v2_after.get(UnifiedMetadataKey.TITLE) == "Updated ID3v2 Title"
201
+ assert id3v2_after.get(UnifiedMetadataKey.ARTISTS) == ["Updated ID3v2 Artist"]
202
+ assert id3v2_after.get(UnifiedMetadataKey.ALBUM) == "ID3v2 Album"
203
+
204
+ def test_preserve_strategy_with_none_values(self):
205
+ with temp_file_with_metadata({"title": "Original Title", "artist": "Original Artist"}, "mp3") as test_file:
206
+ ID3v1MetadataSetter.set_title(test_file, "ID3v1 Title")
207
+ ID3v1MetadataSetter.set_artist(test_file, "ID3v1 Artist")
208
+
209
+ # Verify initial state
210
+ id3v1_before = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
211
+ assert id3v1_before.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
212
+
213
+ # Update ID3v2 with None values (should not affect ID3v1)
214
+ id3v2_metadata = {
215
+ UnifiedMetadataKey.TITLE: None, # This should not affect ID3v1
216
+ UnifiedMetadataKey.ARTISTS: ["ID3v2 Artist"],
217
+ }
218
+ update_metadata(test_file, id3v2_metadata, metadata_format=MetadataFormat.ID3V2)
219
+
220
+ # Verify ID3v1 was preserved
221
+ id3v1_after = get_unified_metadata(test_file, metadata_format=MetadataFormat.ID3V1)
222
+ assert id3v1_after.get(UnifiedMetadataKey.TITLE) == "ID3v1 Title"
223
+ assert id3v1_after.get(UnifiedMetadataKey.ARTISTS) == ["ID3v1 Artist"]