audiometa-python 0.6.0__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 (352) hide show
  1. audiometa/__init__.py +1297 -0
  2. audiometa/__main__.py +6 -0
  3. audiometa/_audio_file.py +607 -0
  4. audiometa/cli.py +476 -0
  5. audiometa/exceptions.py +167 -0
  6. audiometa/manager/_MetadataManager.py +768 -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 +1032 -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 +1002 -0
  14. audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
  15. audiometa/manager/_rating_supporting/riff/_riff_constants.py +17 -0
  16. audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +542 -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 +349 -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 +189 -0
  40. audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +506 -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 +298 -0
  44. audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
  45. audiometa/test/helpers/riff/riff_metadata_getter.py +219 -0
  46. audiometa/test/helpers/riff/riff_metadata_setter.py +374 -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 +221 -0
  55. audiometa/test/tests/__init__.py +0 -0
  56. audiometa/test/tests/conftest.py +276 -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/read/__init__.py +0 -0
  69. audiometa/test/tests/e2e/cli/read/test_basic.py +58 -0
  70. audiometa/test/tests/e2e/cli/read/test_comprehensive.py +240 -0
  71. audiometa/test/tests/e2e/cli/read/test_formats.py +55 -0
  72. audiometa/test/tests/e2e/cli/read/test_metadata_content.py +164 -0
  73. audiometa/test/tests/e2e/cli/read/test_multiple_files.py +149 -0
  74. audiometa/test/tests/e2e/cli/read/test_options.py +88 -0
  75. audiometa/test/tests/e2e/cli/read/test_unified.py +84 -0
  76. audiometa/test/tests/e2e/cli/test_delete.py +20 -0
  77. audiometa/test/tests/e2e/cli/test_formatting.py +31 -0
  78. audiometa/test/tests/e2e/cli/test_help.py +41 -0
  79. audiometa/test/tests/e2e/cli/write/__init__.py +0 -0
  80. audiometa/test/tests/e2e/cli/write/test_basic.py +51 -0
  81. audiometa/test/tests/e2e/cli/write/test_comprehensive.py +210 -0
  82. audiometa/test/tests/e2e/cli/write/test_force_format.py +336 -0
  83. audiometa/test/tests/e2e/cli/write/test_integer_fields.py +145 -0
  84. audiometa/test/tests/e2e/cli/write/test_list_fields.py +107 -0
  85. audiometa/test/tests/e2e/cli/write/test_rating.py +74 -0
  86. audiometa/test/tests/e2e/cli/write/test_string_fields.py +54 -0
  87. audiometa/test/tests/e2e/cli/write/test_validation.py +85 -0
  88. audiometa/test/tests/e2e/scenarios/__init__.py +0 -0
  89. audiometa/test/tests/e2e/scenarios/test_user_scenarios.py +166 -0
  90. audiometa/test/tests/e2e/workflows/__init__.py +0 -0
  91. audiometa/test/tests/e2e/workflows/test_core_workflows.py +166 -0
  92. audiometa/test/tests/e2e/workflows/test_deletion_workflows.py +318 -0
  93. audiometa/test/tests/e2e/workflows/test_error_handling_workflows.py +165 -0
  94. audiometa/test/tests/e2e/workflows/test_format_specific_workflows.py +129 -0
  95. audiometa/test/tests/e2e/workflows/test_rating_workflows.py +124 -0
  96. audiometa/test/tests/integration/__init__.py +0 -0
  97. audiometa/test/tests/integration/audio_format/__init__.py +0 -0
  98. audiometa/test/tests/integration/audio_format/flac/__init__.py +0 -0
  99. audiometa/test/tests/integration/audio_format/flac/test_flac_delete_all.py +108 -0
  100. audiometa/test/tests/integration/audio_format/flac/test_flac_reading_all.py +61 -0
  101. audiometa/test/tests/integration/audio_format/flac/test_flac_reading_field.py +65 -0
  102. audiometa/test/tests/integration/audio_format/flac/test_flac_writing.py +69 -0
  103. audiometa/test/tests/integration/audio_format/mp3/__init__.py +0 -0
  104. audiometa/test/tests/integration/audio_format/mp3/test_mp3_delete_all.py +79 -0
  105. audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_all.py +61 -0
  106. audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_field.py +67 -0
  107. audiometa/test/tests/integration/audio_format/mp3/test_mp3_writing.py +60 -0
  108. audiometa/test/tests/integration/audio_format/wav/__init__.py +0 -0
  109. audiometa/test/tests/integration/audio_format/wav/test_wav_delete_all.py +87 -0
  110. audiometa/test/tests/integration/audio_format/wav/test_wav_reading_all.py +62 -0
  111. audiometa/test/tests/integration/audio_format/wav/test_wav_reading_field.py +57 -0
  112. audiometa/test/tests/integration/audio_format/wav/test_wav_with_id3v2_tags.py +83 -0
  113. audiometa/test/tests/integration/audio_format/wav/test_wav_writing.py +62 -0
  114. audiometa/test/tests/integration/conftest.py +29 -0
  115. audiometa/test/tests/integration/delete_all_metadata/__init__.py +1 -0
  116. audiometa/test/tests/integration/delete_all_metadata/test_audio_format_all.py +102 -0
  117. audiometa/test/tests/integration/delete_all_metadata/test_audio_format_header_deletion.py +77 -0
  118. audiometa/test/tests/integration/delete_all_metadata/test_basic_functionality.py +47 -0
  119. audiometa/test/tests/integration/delete_all_metadata/test_error_handling.py +24 -0
  120. audiometa/test/tests/integration/encoding/__init__.py +1 -0
  121. audiometa/test/tests/integration/encoding/test_encoding.py +88 -0
  122. audiometa/test/tests/integration/encoding/test_special_characters_edge_cases.py +223 -0
  123. audiometa/test/tests/integration/get_full_metadata/__init__.py +0 -0
  124. audiometa/test/tests/integration/get_full_metadata/test_audio_formats.py +122 -0
  125. audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +250 -0
  126. audiometa/test/tests/integration/get_full_metadata/test_consistency.py +67 -0
  127. audiometa/test/tests/integration/get_full_metadata/test_edge_cases.py +123 -0
  128. audiometa/test/tests/integration/get_full_metadata/test_error_handling.py +40 -0
  129. audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +43 -0
  130. audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
  131. audiometa/test/tests/integration/get_full_metadata/test_performance.py +95 -0
  132. audiometa/test/tests/integration/get_full_metadata/test_riff_bext.py +128 -0
  133. audiometa/test/tests/integration/get_full_metadata/test_structure.py +161 -0
  134. audiometa/test/tests/integration/metadata_field/__init__.py +0 -0
  135. audiometa/test/tests/integration/metadata_field/album/__init__.py +0 -0
  136. audiometa/test/tests/integration/metadata_field/album/test_deleting.py +73 -0
  137. audiometa/test/tests/integration/metadata_field/album/test_reading.py +36 -0
  138. audiometa/test/tests/integration/metadata_field/album/test_writing.py +50 -0
  139. audiometa/test/tests/integration/metadata_field/album_artists/__init__.py +0 -0
  140. audiometa/test/tests/integration/metadata_field/album_artists/test_deleting.py +83 -0
  141. audiometa/test/tests/integration/metadata_field/album_artists/test_reading.py +38 -0
  142. audiometa/test/tests/integration/metadata_field/album_artists/test_writing.py +52 -0
  143. audiometa/test/tests/integration/metadata_field/artists/__init__.py +0 -0
  144. audiometa/test/tests/integration/metadata_field/artists/test_deleting.py +68 -0
  145. audiometa/test/tests/integration/metadata_field/artists/test_reading.py +36 -0
  146. audiometa/test/tests/integration/metadata_field/artists/test_writing.py +46 -0
  147. audiometa/test/tests/integration/metadata_field/bpm/__init__.py +0 -0
  148. audiometa/test/tests/integration/metadata_field/bpm/test_deleting.py +75 -0
  149. audiometa/test/tests/integration/metadata_field/bpm/test_reading.py +32 -0
  150. audiometa/test/tests/integration/metadata_field/bpm/test_writing.py +56 -0
  151. audiometa/test/tests/integration/metadata_field/comment/__init__.py +0 -0
  152. audiometa/test/tests/integration/metadata_field/comment/test_deleting.py +68 -0
  153. audiometa/test/tests/integration/metadata_field/comment/test_reading.py +36 -0
  154. audiometa/test/tests/integration/metadata_field/comment/test_writing.py +49 -0
  155. audiometa/test/tests/integration/metadata_field/composer/__init__.py +0 -0
  156. audiometa/test/tests/integration/metadata_field/composer/test_deleting.py +75 -0
  157. audiometa/test/tests/integration/metadata_field/composer/test_reading.py +34 -0
  158. audiometa/test/tests/integration/metadata_field/composer/test_writing.py +41 -0
  159. audiometa/test/tests/integration/metadata_field/copyright/__init__.py +0 -0
  160. audiometa/test/tests/integration/metadata_field/copyright/test_deleting.py +81 -0
  161. audiometa/test/tests/integration/metadata_field/copyright/test_reading.py +35 -0
  162. audiometa/test/tests/integration/metadata_field/copyright/test_writing.py +41 -0
  163. audiometa/test/tests/integration/metadata_field/disc_number/__init__.py +0 -0
  164. audiometa/test/tests/integration/metadata_field/disc_number/test_deleting.py +97 -0
  165. audiometa/test/tests/integration/metadata_field/disc_number/test_reading.py +92 -0
  166. audiometa/test/tests/integration/metadata_field/disc_number/test_writing.py +153 -0
  167. audiometa/test/tests/integration/metadata_field/field_not_supported/__init__.py +0 -0
  168. audiometa/test/tests/integration/metadata_field/field_not_supported/test_deleting.py +56 -0
  169. audiometa/test/tests/integration/metadata_field/field_not_supported/test_reading.py +54 -0
  170. audiometa/test/tests/integration/metadata_field/field_not_supported/test_writing.py +61 -0
  171. audiometa/test/tests/integration/metadata_field/genre/__init__.py +0 -0
  172. audiometa/test/tests/integration/metadata_field/genre/reading/__init__.py +0 -0
  173. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/__init__.py +0 -0
  174. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v1_reading.py +65 -0
  175. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v2_reading.py +25 -0
  176. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_riff_reading.py +58 -0
  177. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_vorbis_reading.py +61 -0
  178. audiometa/test/tests/integration/metadata_field/genre/reading/test_smart_reading.py +191 -0
  179. audiometa/test/tests/integration/metadata_field/genre/test_deleting.py +62 -0
  180. audiometa/test/tests/integration/metadata_field/genre/test_writing.py +64 -0
  181. audiometa/test/tests/integration/metadata_field/isrc/__init__.py +1 -0
  182. audiometa/test/tests/integration/metadata_field/isrc/test_deleting.py +31 -0
  183. audiometa/test/tests/integration/metadata_field/isrc/test_reading.py +35 -0
  184. audiometa/test/tests/integration/metadata_field/isrc/test_writing.py +165 -0
  185. audiometa/test/tests/integration/metadata_field/language/__init__.py +0 -0
  186. audiometa/test/tests/integration/metadata_field/language/test_deleting.py +75 -0
  187. audiometa/test/tests/integration/metadata_field/language/test_reading.py +39 -0
  188. audiometa/test/tests/integration/metadata_field/language/test_writing.py +43 -0
  189. audiometa/test/tests/integration/metadata_field/lyrics/__init__.py +0 -0
  190. audiometa/test/tests/integration/metadata_field/lyrics/test_deleting.py +129 -0
  191. audiometa/test/tests/integration/metadata_field/lyrics/test_reading.py +57 -0
  192. audiometa/test/tests/integration/metadata_field/lyrics/test_writing.py +59 -0
  193. audiometa/test/tests/integration/metadata_field/publisher/__init__.py +0 -0
  194. audiometa/test/tests/integration/metadata_field/publisher/test_deleting.py +88 -0
  195. audiometa/test/tests/integration/metadata_field/publisher/test_reading.py +32 -0
  196. audiometa/test/tests/integration/metadata_field/publisher/test_writing.py +47 -0
  197. audiometa/test/tests/integration/metadata_field/rating/__init__.py +0 -0
  198. audiometa/test/tests/integration/metadata_field/rating/reading/__init__.py +0 -0
  199. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_100_proportional.py +81 -0
  200. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_non_proportional.py +33 -0
  201. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_proportional.py +58 -0
  202. audiometa/test/tests/integration/metadata_field/rating/test_deleting.py +117 -0
  203. audiometa/test/tests/integration/metadata_field/rating/test_error_handling.py +137 -0
  204. audiometa/test/tests/integration/metadata_field/rating/writing/__init__.py +0 -0
  205. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/__init__.py +0 -0
  206. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_id3v2.py +77 -0
  207. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_riff.py +55 -0
  208. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_vorbis.py +57 -0
  209. audiometa/test/tests/integration/metadata_field/rating/writing/test_comprehensive.py +192 -0
  210. audiometa/test/tests/integration/metadata_field/release_date/__init__.py +0 -0
  211. audiometa/test/tests/integration/metadata_field/release_date/test_deleting.py +74 -0
  212. audiometa/test/tests/integration/metadata_field/release_date/test_error_handling.py +82 -0
  213. audiometa/test/tests/integration/metadata_field/release_date/test_reading.py +59 -0
  214. audiometa/test/tests/integration/metadata_field/release_date/test_writing.py +49 -0
  215. audiometa/test/tests/integration/metadata_field/test_metadata_field_validation.py +135 -0
  216. audiometa/test/tests/integration/metadata_field/title/__init__.py +0 -0
  217. audiometa/test/tests/integration/metadata_field/title/test_deleting.py +73 -0
  218. audiometa/test/tests/integration/metadata_field/title/test_error_handling.py +47 -0
  219. audiometa/test/tests/integration/metadata_field/title/test_reading.py +36 -0
  220. audiometa/test/tests/integration/metadata_field/title/test_writing.py +64 -0
  221. audiometa/test/tests/integration/metadata_field/track_number/__init__.py +0 -0
  222. audiometa/test/tests/integration/metadata_field/track_number/reading/__init__.py +0 -0
  223. audiometa/test/tests/integration/metadata_field/track_number/reading/test_edge_cases.py +43 -0
  224. audiometa/test/tests/integration/metadata_field/track_number/reading/test_metadata_format.py +32 -0
  225. audiometa/test/tests/integration/metadata_field/track_number/test_deleting.py +59 -0
  226. audiometa/test/tests/integration/metadata_field/track_number/test_writing.py +73 -0
  227. audiometa/test/tests/integration/multiple_values/__init__.py +1 -0
  228. audiometa/test/tests/integration/multiple_values/reading/__init__.py +1 -0
  229. audiometa/test/tests/integration/multiple_values/reading/metadata_format/__init__.py +1 -0
  230. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v1.py +23 -0
  231. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_3.py +92 -0
  232. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_4.py +216 -0
  233. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_riff.py +84 -0
  234. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_vorbis.py +169 -0
  235. audiometa/test/tests/integration/multiple_values/reading/test_performance_large_data.py +209 -0
  236. audiometa/test/tests/integration/multiple_values/reading/test_smart_parsing_scenarios.py +198 -0
  237. audiometa/test/tests/integration/multiple_values/reading/test_unicode_handling.py +24 -0
  238. audiometa/test/tests/integration/multiple_values/writing/__init__.py +1 -0
  239. audiometa/test/tests/integration/multiple_values/writing/metadata_format/__init__.py +1 -0
  240. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v1.py +62 -0
  241. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_3.py +36 -0
  242. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_4.py +34 -0
  243. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_riff.py +32 -0
  244. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_vorbis.py +54 -0
  245. audiometa/test/tests/integration/multiple_values/writing/test_error_handling.py +42 -0
  246. audiometa/test/tests/integration/multiple_values/writing/test_large_values.py +98 -0
  247. audiometa/test/tests/integration/reading/__init__.py +1 -0
  248. audiometa/test/tests/integration/reading/test_read_multiple_metadata.py +80 -0
  249. audiometa/test/tests/integration/reading/test_reading_error_handling.py +36 -0
  250. audiometa/test/tests/integration/real_audio_files/__init__.py +0 -0
  251. audiometa/test/tests/integration/real_audio_files/test_reading.py +146 -0
  252. audiometa/test/tests/integration/real_audio_files/test_writing.py +198 -0
  253. audiometa/test/tests/integration/technical_info/__init__.py +0 -0
  254. audiometa/test/tests/integration/technical_info/flac_md5/__init__.py +0 -0
  255. audiometa/test/tests/integration/technical_info/flac_md5/conftest.py +103 -0
  256. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/__init__.py +0 -0
  257. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_audio_data_corruption.py +21 -0
  258. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_flipped_md5.py +29 -0
  259. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_non_flac_error.py +13 -0
  260. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_partial_md5.py +29 -0
  261. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_random_md5.py +29 -0
  262. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_unset_md5.py +56 -0
  263. audiometa/test/tests/integration/technical_info/flac_md5/test_valid_md5.py +21 -0
  264. audiometa/test/tests/integration/technical_info/test_bitrate.py +79 -0
  265. audiometa/test/tests/integration/technical_info/test_channels.py +38 -0
  266. audiometa/test/tests/integration/technical_info/test_duration_in_sec.py +38 -0
  267. audiometa/test/tests/integration/technical_info/test_sample_rate.py +40 -0
  268. audiometa/test/tests/integration/test_audio_file.py +35 -0
  269. audiometa/test/tests/integration/test_audio_format_readable_after_update_all_metadata_formats.py +95 -0
  270. audiometa/test/tests/integration/writing/__init__.py +0 -0
  271. audiometa/test/tests/integration/writing/test_error_handling.py +44 -0
  272. audiometa/test/tests/integration/writing/test_forced_format.py +224 -0
  273. audiometa/test/tests/integration/writing/test_multiple_format_preservation.py +223 -0
  274. audiometa/test/tests/integration/writing/test_partial_update.py +36 -0
  275. audiometa/test/tests/integration/writing/writing_strategies/__init__.py +0 -0
  276. audiometa/test/tests/integration/writing/writing_strategies/test_cleanup_strategy.py +79 -0
  277. audiometa/test/tests/integration/writing/writing_strategies/test_preserve_strategy.py +76 -0
  278. audiometa/test/tests/integration/writing/writing_strategies/test_sync_strategy.py +215 -0
  279. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/__init__.py +0 -0
  280. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_fail_behavior.py +42 -0
  281. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_no_writing_on_failure.py +93 -0
  282. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_strategy_specific.py +99 -0
  283. audiometa/test/tests/unit/__init__.py +0 -0
  284. audiometa/test/tests/unit/audio_file/__init__.py +0 -0
  285. audiometa/test/tests/unit/audio_file/technical_info/__init__.py +0 -0
  286. audiometa/test/tests/unit/audio_file/technical_info/test_bitrate.py +26 -0
  287. audiometa/test/tests/unit/audio_file/technical_info/test_channels.py +31 -0
  288. audiometa/test/tests/unit/audio_file/technical_info/test_duration_in_sec.py +38 -0
  289. audiometa/test/tests/unit/audio_file/technical_info/test_error_handling.py +190 -0
  290. audiometa/test/tests/unit/audio_file/technical_info/test_file_size.py +51 -0
  291. audiometa/test/tests/unit/audio_file/technical_info/test_format_name.py +28 -0
  292. audiometa/test/tests/unit/audio_file/technical_info/test_sample_rate.py +31 -0
  293. audiometa/test/tests/unit/audio_file/test_context_manager.py +30 -0
  294. audiometa/test/tests/unit/audio_file/test_file_validation.py +40 -0
  295. audiometa/test/tests/unit/audio_file/test_is_audio_file.py +49 -0
  296. audiometa/test/tests/unit/audio_file/test_operations.py +20 -0
  297. audiometa/test/tests/unit/audio_file/test_path_handling.py +23 -0
  298. audiometa/test/tests/unit/cli/__init__.py +0 -0
  299. audiometa/test/tests/unit/cli/test_expand_file_patterns.py +234 -0
  300. audiometa/test/tests/unit/metadata_managers/__init__.py +0 -0
  301. audiometa/test/tests/unit/metadata_managers/conftest.py +142 -0
  302. audiometa/test/tests/unit/metadata_managers/header_info/__init__.py +0 -0
  303. audiometa/test/tests/unit/metadata_managers/header_info/test_id3v1.py +49 -0
  304. audiometa/test/tests/unit/metadata_managers/header_info/test_id3v2.py +66 -0
  305. audiometa/test/tests/unit/metadata_managers/header_info/test_riff.py +343 -0
  306. audiometa/test/tests/unit/metadata_managers/header_info/test_vorbis.py +53 -0
  307. audiometa/test/tests/unit/metadata_managers/metadata_field/__init__.py +0 -0
  308. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/__init__.py +0 -0
  309. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/__init__.py +0 -0
  310. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/test_smart_parsing.py +186 -0
  311. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/__init__.py +0 -0
  312. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_separator_selection.py +142 -0
  313. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_value_filtering.py +76 -0
  314. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/__init__.py +0 -0
  315. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/__init__.py +0 -0
  316. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_normalization.py +152 -0
  317. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_profiles_values.py +23 -0
  318. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/test_rating_validation.py +77 -0
  319. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/__init__.py +0 -0
  320. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_configuration_error.py +43 -0
  321. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_validation.py +151 -0
  322. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_writing_profiles.py +61 -0
  323. audiometa/test/tests/unit/metadata_managers/metadata_field/test_date_format_validation.py +135 -0
  324. audiometa/test/tests/unit/metadata_managers/metadata_field/test_disc_number_validation.py +75 -0
  325. audiometa/test/tests/unit/metadata_managers/metadata_field/test_isrc_format_validation.py +121 -0
  326. audiometa/test/tests/unit/metadata_managers/metadata_field/test_isrc_type_validation.py +30 -0
  327. audiometa/test/tests/unit/metadata_managers/metadata_field/test_track_number_validation.py +46 -0
  328. audiometa/test/tests/unit/metadata_managers/metadata_field/test_type_validation_exception.py +22 -0
  329. audiometa/test/tests/unit/metadata_managers/metadata_field/test_validation.py +83 -0
  330. audiometa/test/tests/unit/metadata_managers/test_metadata_format_managers_write_and_read.py +74 -0
  331. audiometa/test/tests/unit/metadata_managers/test_riff_configuration_error.py +26 -0
  332. audiometa/utils/__init__.py +1 -0
  333. audiometa/utils/id3v1_genre_code_map.py +205 -0
  334. audiometa/utils/metadata_format.py +31 -0
  335. audiometa/utils/metadata_writing_strategy.py +16 -0
  336. audiometa/utils/mutagen_exception_handler.py +24 -0
  337. audiometa/utils/os_dependencies_checker/__init__.py +24 -0
  338. audiometa/utils/os_dependencies_checker/base.py +62 -0
  339. audiometa/utils/os_dependencies_checker/config.py +77 -0
  340. audiometa/utils/os_dependencies_checker/macos.py +236 -0
  341. audiometa/utils/os_dependencies_checker/ubuntu.py +95 -0
  342. audiometa/utils/os_dependencies_checker/windows.py +227 -0
  343. audiometa/utils/rating_profiles.py +110 -0
  344. audiometa/utils/tool_path_resolver.py +135 -0
  345. audiometa/utils/types.py +82 -0
  346. audiometa/utils/unified_metadata_key.py +87 -0
  347. audiometa_python-0.6.0.dist-info/METADATA +1593 -0
  348. audiometa_python-0.6.0.dist-info/RECORD +352 -0
  349. audiometa_python-0.6.0.dist-info/WHEEL +5 -0
  350. audiometa_python-0.6.0.dist-info/entry_points.txt +2 -0
  351. audiometa_python-0.6.0.dist-info/licenses/LICENSE +202 -0
  352. audiometa_python-0.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,374 @@
1
+ """RIFF metadata setting operations."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from ..common.external_tool_runner import ExternalMetadataToolError, run_external_tool
7
+
8
+
9
+ class RIFFMetadataSetter:
10
+ """Static utility class for RIFF metadata setting using external tools."""
11
+
12
+ @staticmethod
13
+ def set_metadata(file_path: Path, metadata: dict[str, Any]) -> None:
14
+ """Set WAV metadata using bwfmetaedit tool."""
15
+ cmd = ["bwfmetaedit"]
16
+
17
+ # Map common metadata keys to bwfmetaedit arguments
18
+ key_mapping = {
19
+ "title": "--INAM",
20
+ "artist": "--IART",
21
+ "album": "--IPRD",
22
+ "genre": "--IGNR",
23
+ "date": "--ICRD",
24
+ "year": "--ICRD",
25
+ "release_date": "--ICRD",
26
+ "comment": "--ICMT",
27
+ "track": "--ITRK",
28
+ "track_number": "--ITRK",
29
+ "bpm": "--TBPM",
30
+ "composer": "--ICMP",
31
+ "lyrics": "--ILYR",
32
+ "language": "--ILNG",
33
+ "album_artist": "--IAAR",
34
+ "rating": "--IRTD",
35
+ "copyright": "--ICOP",
36
+ "isrc": "--ISRC",
37
+ }
38
+
39
+ # Handle list values - include first value in main command to avoid overwriting
40
+ artist_value = None
41
+ genre_value = None
42
+ composer_value = None
43
+ for key, value in metadata.items():
44
+ if isinstance(value, list) and value:
45
+ if key.lower() == "artist":
46
+ artist_value = value[0] # Store first artist for main command
47
+ elif key.lower() == "genre":
48
+ genre_value = value[0] # Store first genre for main command
49
+ elif key.lower() == "composer":
50
+ composer_value = value[0] # Store first composer for main command
51
+
52
+ # Handle non-list values and include list values in main command
53
+ # Note: Rating, BPM, Language, and Composer need to be set AFTER bwfmetaedit to avoid being overwritten
54
+ rating_value = None
55
+ bpm_value = None
56
+ language_value = None
57
+ composer_single_value = None
58
+ metadata_added = False
59
+ for key, value in metadata.items():
60
+ if key.lower() in key_mapping and not isinstance(value, list):
61
+ if key.lower() == "bpm":
62
+ bpm_value = str(value) # Store for later
63
+ elif key.lower() == "rating":
64
+ rating_value = str(value) # Store for later
65
+ elif key.lower() == "language":
66
+ language_value = str(value) # Store for later
67
+ elif key.lower() == "composer":
68
+ composer_single_value = str(value) # Store for later
69
+ else:
70
+ cmd.extend([f"{key_mapping[key.lower()]}={value}"])
71
+ metadata_added = True
72
+
73
+ # Add artist if it was provided as a list
74
+ if artist_value is not None:
75
+ cmd.extend([f"{key_mapping['artist']}={artist_value}"])
76
+ metadata_added = True
77
+
78
+ # Add genre if it was provided as a list
79
+ if genre_value is not None:
80
+ cmd.extend([f"{key_mapping['genre']}={genre_value}"])
81
+ metadata_added = True
82
+
83
+ # Run bwfmetaedit first if metadata was actually added
84
+ if metadata_added:
85
+ cmd.append(str(file_path))
86
+ run_external_tool(cmd, "bwfmetaedit")
87
+
88
+ # Set BPM, rating, and language AFTER bwfmetaedit to avoid being overwritten
89
+ if bpm_value is not None:
90
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
91
+
92
+ ManualRIFFMetadataCreator.create_bpm_field(file_path, bpm_value)
93
+
94
+ if rating_value is not None:
95
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
96
+
97
+ ManualRIFFMetadataCreator.create_rating_field(file_path, rating_value)
98
+
99
+ if language_value is not None:
100
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
101
+
102
+ ManualRIFFMetadataCreator.create_language_field(file_path, language_value)
103
+
104
+ # Set composer (from list or single value) AFTER bwfmetaedit
105
+ if composer_value is not None:
106
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
107
+
108
+ ManualRIFFMetadataCreator.create_composer_field(file_path, composer_value)
109
+ elif composer_single_value is not None:
110
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
111
+
112
+ ManualRIFFMetadataCreator.create_composer_field(file_path, composer_single_value)
113
+
114
+ @staticmethod
115
+ def set_comment(file_path: Path, comment: str) -> None:
116
+ command = ["bwfmetaedit", f"--ICMT={comment}", str(file_path)]
117
+ run_external_tool(command, "bwfmetaedit")
118
+
119
+ @staticmethod
120
+ def set_title(file_path: Path, title: str) -> None:
121
+ command = ["bwfmetaedit", f"--INAM={title}", str(file_path)]
122
+ run_external_tool(command, "bwfmetaedit")
123
+
124
+ @staticmethod
125
+ def set_multiple_titles(file_path: Path, titles: list[str], in_separate_frames: bool = False):
126
+ """Set multiple titles, optionally in separate INAM frames."""
127
+ if in_separate_frames:
128
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
129
+
130
+ ManualRIFFMetadataCreator.create_multiple_title_fields(file_path, titles)
131
+ # For now, just set the first title
132
+ elif titles:
133
+ command = ["bwfmetaedit", f"--INAM={titles[0]}", str(file_path)]
134
+ run_external_tool(command, "bwfmetaedit")
135
+
136
+ @staticmethod
137
+ def set_artist(file_path: Path, artist: str) -> None:
138
+ command = ["bwfmetaedit", f"--IART={artist}", str(file_path)]
139
+ run_external_tool(command, "bwfmetaedit")
140
+
141
+ @staticmethod
142
+ def set_album(file_path: Path, album: str) -> None:
143
+ command = ["bwfmetaedit", f"--IPRD={album}", str(file_path)]
144
+ run_external_tool(command, "bwfmetaedit")
145
+
146
+ @staticmethod
147
+ def set_genres(file_path: Path, genres: list[str]) -> None:
148
+ command = ["bwfmetaedit", f"--IGNR={','.join(genres)}", str(file_path)]
149
+ run_external_tool(command, "bwfmetaedit")
150
+
151
+ @staticmethod
152
+ def set_genre_text(file_path: Path, genre_text: str) -> None:
153
+ """Set RIFF genre using external exiftool or bwfmetaedit tool."""
154
+ try:
155
+ # Try exiftool first
156
+ RIFFMetadataSetter.set_riff_genre(file_path, genre_text)
157
+ except ExternalMetadataToolError:
158
+ try:
159
+ # Fallback to bwfmetaedit - split genre_text by semicolon and strip whitespace
160
+ genres = [genre.strip() for genre in genre_text.split(";") if genre.strip()]
161
+ RIFFMetadataSetter.set_genres(file_path, genres)
162
+ except ExternalMetadataToolError as e:
163
+ msg = f"Failed to set RIFF genre: {e}"
164
+ raise RuntimeError(msg) from e
165
+
166
+ @staticmethod
167
+ def set_lyrics(file_path: Path, lyrics: str) -> None:
168
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
169
+
170
+ ManualRIFFMetadataCreator.create_lyrics_field(file_path, lyrics)
171
+
172
+ @staticmethod
173
+ def set_language(file_path: Path, language: str) -> None:
174
+ import tempfile
175
+
176
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
177
+ tmp_path = Path(tmp_file.name)
178
+ try:
179
+ command = [
180
+ "ffmpeg",
181
+ "-i",
182
+ str(file_path),
183
+ "-c",
184
+ "copy",
185
+ "-metadata",
186
+ f"language={language}",
187
+ "-y",
188
+ str(tmp_path),
189
+ ]
190
+ run_external_tool(command, "ffmpeg")
191
+ tmp_path.replace(file_path)
192
+ finally:
193
+ if tmp_path.exists():
194
+ tmp_path.unlink()
195
+
196
+ @staticmethod
197
+ def set_max_metadata(file_path: Path) -> None:
198
+ from pathlib import Path
199
+
200
+ from ..common.external_tool_runner import run_script
201
+
202
+ scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
203
+ run_script("set-riff-max-metadata.sh", file_path, scripts_dir)
204
+
205
+ @staticmethod
206
+ def set_riff_genre(file_path: Path, genre: str) -> None:
207
+ command = ["exiftool", "-overwrite_original", f"-Genre={genre}", str(file_path)]
208
+ run_external_tool(command, "exiftool")
209
+
210
+ @staticmethod
211
+ def set_artists(file_path: Path, artists: list[str], in_separate_frames: bool = False):
212
+ """Set multiple artists, optionally in separate IART frames."""
213
+ if in_separate_frames:
214
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
215
+
216
+ ManualRIFFMetadataCreator.create_multiple_artist_fields(file_path, artists)
217
+ # For testing multiple instances, we'd need to use a more sophisticated approach
218
+ # For now, just set the first artist
219
+ elif artists:
220
+ command = ["bwfmetaedit", f"--IART={artists[0]}", str(file_path)]
221
+ run_external_tool(command, "bwfmetaedit")
222
+
223
+ @staticmethod
224
+ def set_multiple_genres(file_path: Path, genres: list[str], in_separate_frames: bool = False):
225
+ """Set multiple genres, optionally in separate IGNR frames."""
226
+ if in_separate_frames:
227
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
228
+
229
+ ManualRIFFMetadataCreator.create_multiple_genre_fields(file_path, genres)
230
+ # For now, just set the first genre
231
+ elif genres:
232
+ command = ["bwfmetaedit", f"--IGNR={genres[0]}", str(file_path)]
233
+ run_external_tool(command, "bwfmetaedit")
234
+
235
+ @staticmethod
236
+ def set_multiple_composers(file_path: Path, composers: list[str], in_separate_frames: bool = False):
237
+ """Set multiple composers, optionally in separate ICMP frames."""
238
+ if in_separate_frames:
239
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
240
+
241
+ ManualRIFFMetadataCreator.create_multiple_composer_fields(file_path, composers)
242
+ # For now, just set the first composer
243
+ elif composers:
244
+ command = ["bwfmetaedit", f"--ICMP={composers[0]}", str(file_path)]
245
+ run_external_tool(command, "bwfmetaedit")
246
+
247
+ @staticmethod
248
+ def set_multiple_album_artists(file_path: Path, album_artists: list[str], _in_separate_frames: bool = False):
249
+ """Set multiple album artists, optionally in separate IAAR frames."""
250
+ # IAAR is not a standard RIFF INFO chunk field, so external tools don't support it.
251
+ # Use the manual metadata creator which can create non-standard RIFF fields.
252
+ from .riff_manual_metadata_creator import ManualRIFFMetadataCreator
253
+
254
+ ManualRIFFMetadataCreator.create_multiple_album_artist_fields(file_path, album_artists)
255
+
256
+ @staticmethod
257
+ def set_release_date(file_path: Path, release_date: str) -> None:
258
+ command = ["bwfmetaedit", f"--ICRD={release_date}", str(file_path)]
259
+ run_external_tool(command, "bwfmetaedit")
260
+
261
+ @staticmethod
262
+ def set_bext_description(file_path: Path, description: str) -> None:
263
+ """Set BWF bext Description field."""
264
+ command = ["bwfmetaedit", f"--Description={description}", str(file_path)]
265
+ run_external_tool(command, "bwfmetaedit")
266
+
267
+ @staticmethod
268
+ def set_bext_originator(file_path: Path, originator: str) -> None:
269
+ """Set BWF bext Originator field."""
270
+ command = ["bwfmetaedit", f"--Originator={originator}", str(file_path)]
271
+ run_external_tool(command, "bwfmetaedit")
272
+
273
+ @staticmethod
274
+ def set_bext_originator_reference(file_path: Path, originator_reference: str) -> None:
275
+ """Set BWF bext OriginatorReference field."""
276
+ command = ["bwfmetaedit", f"--OriginatorReference={originator_reference}", str(file_path)]
277
+ run_external_tool(command, "bwfmetaedit")
278
+
279
+ @staticmethod
280
+ def set_bext_origination_date(file_path: Path, origination_date: str) -> None:
281
+ """Set BWF bext OriginationDate field (YYYY-MM-DD format)."""
282
+ command = ["bwfmetaedit", f"--OriginationDate={origination_date}", str(file_path)]
283
+ run_external_tool(command, "bwfmetaedit")
284
+
285
+ @staticmethod
286
+ def set_bext_origination_time(file_path: Path, origination_time: str) -> None:
287
+ """Set BWF bext OriginationTime field (HH:MM:SS format)."""
288
+ command = ["bwfmetaedit", f"--OriginationTime={origination_time}", str(file_path)]
289
+ run_external_tool(command, "bwfmetaedit")
290
+
291
+ @staticmethod
292
+ def set_bext_time_reference(file_path: Path, time_reference: int) -> None:
293
+ """Set BWF bext TimeReference field."""
294
+ command = ["bwfmetaedit", f"--Timereference={time_reference}", str(file_path)]
295
+ run_external_tool(command, "bwfmetaedit")
296
+
297
+ @staticmethod
298
+ def set_bext_coding_history(file_path: Path, coding_history: str) -> None:
299
+ """Set BWF bext CodingHistory field."""
300
+ command = ["bwfmetaedit", f"--History={coding_history}", str(file_path)]
301
+ run_external_tool(command, "bwfmetaedit")
302
+
303
+ @staticmethod
304
+ def set_bext_metadata(file_path: Path, bext_metadata: dict[str, Any]) -> None:
305
+ """Set multiple BWF bext metadata fields at once.
306
+
307
+ Args:
308
+ file_path: Path to WAV file
309
+ bext_metadata: Dictionary with bext field names as keys:
310
+ - Description: str
311
+ - Originator: str
312
+ - OriginatorReference: str
313
+ - OriginationDate: str (YYYY-MM-DD format)
314
+ - OriginationTime: str (HH:MM:SS format)
315
+ - TimeReference: int (uint64)
316
+ - UMID: str (hex string)
317
+ - CodingHistory: str
318
+ - LoudnessValue: float (LU, converted to 0.1 LU units)
319
+ - LoudnessRange: float (LU, converted to 0.1 LU units)
320
+ - MaxTruePeakLevel: float (dB, converted to 0.1 dB units)
321
+ - MaxMomentaryLoudness: float (LU, converted to 0.1 LU units)
322
+ - MaxShortTermLoudness: float (LU, converted to 0.1 LU units)
323
+ """
324
+ # Map bext field names to bwfmetaedit arguments
325
+ field_mapping = {
326
+ "Description": "--Description",
327
+ "Originator": "--Originator",
328
+ "OriginatorReference": "--OriginatorReference",
329
+ "OriginationDate": "--OriginationDate",
330
+ "OriginationTime": "--OriginationTime",
331
+ "TimeReference": "--Timereference",
332
+ "UMID": "--UMID",
333
+ "CodingHistory": "--History",
334
+ "LoudnessValue": "--LoudnessValue",
335
+ "LoudnessRange": "--LoudnessRange",
336
+ "MaxTruePeakLevel": "--MaxTruePeakLevel",
337
+ "MaxMomentaryLoudness": "--MaxMomentaryLoudness",
338
+ "MaxShortTermLoudness": "--MaxShortTermLoudness",
339
+ }
340
+
341
+ # Separate loudness fields from other fields
342
+ # Loudness fields need to be set after other fields to ensure BWF v2 is created
343
+ loudness_fields = [
344
+ "LoudnessValue",
345
+ "LoudnessRange",
346
+ "MaxTruePeakLevel",
347
+ "MaxMomentaryLoudness",
348
+ "MaxShortTermLoudness",
349
+ ]
350
+ regular_fields = {}
351
+ loudness_metadata = {}
352
+
353
+ for field_name, value in bext_metadata.items():
354
+ if field_name in field_mapping and value is not None:
355
+ if field_name in loudness_fields:
356
+ loudness_metadata[field_name] = value
357
+ else:
358
+ regular_fields[field_name] = value
359
+
360
+ # Set regular fields first
361
+ if regular_fields:
362
+ cmd_regular = ["bwfmetaedit"]
363
+ for field_name, value in regular_fields.items():
364
+ cmd_regular.extend([f"{field_mapping[field_name]}={value}"])
365
+ cmd_regular.append(str(file_path))
366
+ run_external_tool(cmd_regular, "bwfmetaedit")
367
+
368
+ # Set loudness fields in separate command (ensures BWF v2 is properly created)
369
+ if loudness_metadata:
370
+ cmd_loudness = ["bwfmetaedit"]
371
+ for field_name, value in loudness_metadata.items():
372
+ cmd_loudness.extend([f"{field_mapping[field_name]}={value}"])
373
+ cmd_loudness.append(str(file_path))
374
+ run_external_tool(cmd_loudness, "bwfmetaedit")
File without changes
@@ -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")