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