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,83 @@
1
+ import pytest
2
+
3
+ from audiometa import validate_metadata_for_update
4
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
5
+
6
+
7
+ @pytest.mark.unit
8
+ class TestValidateMetadata:
9
+ """Test the integrate validate_metadata_for_update function.
10
+
11
+ This class tests the high-level validation function that integrates multiple validation layers. Specific field
12
+ validation (type, format) is tested separately in metadata_field/ test files.
13
+ """
14
+
15
+ def test_empty_dict_raises_error(self):
16
+ with pytest.raises(ValueError, match="no metadata fields specified"):
17
+ validate_metadata_for_update({})
18
+
19
+ def test_none_values_allowed(self):
20
+ validate_metadata_for_update({UnifiedMetadataKey.TITLE: None})
21
+
22
+ validate_metadata_for_update(
23
+ {
24
+ UnifiedMetadataKey.TITLE: None,
25
+ UnifiedMetadataKey.ARTISTS: None,
26
+ UnifiedMetadataKey.ALBUM: None,
27
+ }
28
+ )
29
+
30
+ def test_empty_string_allowed(self):
31
+ validate_metadata_for_update({UnifiedMetadataKey.TITLE: ""})
32
+
33
+ def test_empty_list_allowed(self):
34
+ validate_metadata_for_update({UnifiedMetadataKey.ARTISTS: []})
35
+
36
+ def test_list_with_none_values_allowed(self):
37
+ validate_metadata_for_update({UnifiedMetadataKey.ARTISTS: [None, None]})
38
+ validate_metadata_for_update({UnifiedMetadataKey.ARTISTS: [None]})
39
+ validate_metadata_for_update({UnifiedMetadataKey.GENRES_NAMES: [None, None, None]})
40
+
41
+ def test_empty_string_and_empty_list_allowed(self):
42
+ validate_metadata_for_update({UnifiedMetadataKey.TITLE: "", UnifiedMetadataKey.ARTISTS: []})
43
+
44
+ def test_valid_metadata_passes(self):
45
+ validate_metadata_for_update({UnifiedMetadataKey.TITLE: "Song Title"})
46
+ validate_metadata_for_update({UnifiedMetadataKey.ARTISTS: ["Artist 1", "Artist 2"]})
47
+ validate_metadata_for_update({UnifiedMetadataKey.ALBUM: "Album Name"})
48
+
49
+ def test_multiple_valid_fields_passes(self):
50
+ validate_metadata_for_update(
51
+ {
52
+ UnifiedMetadataKey.TITLE: "Song Title",
53
+ UnifiedMetadataKey.ARTISTS: ["Artist"],
54
+ UnifiedMetadataKey.ALBUM: "Album",
55
+ }
56
+ )
57
+
58
+ def test_mixed_none_and_valid_fields_passes(self):
59
+ validate_metadata_for_update(
60
+ {
61
+ UnifiedMetadataKey.TITLE: None,
62
+ UnifiedMetadataKey.ARTISTS: ["Artist"],
63
+ }
64
+ )
65
+
66
+ def test_combined_validation_rating_and_release_date(self):
67
+ validate_metadata_for_update(
68
+ {
69
+ UnifiedMetadataKey.RATING: 50,
70
+ UnifiedMetadataKey.RELEASE_DATE: "2024-01-01",
71
+ },
72
+ normalized_rating_max_value=100,
73
+ )
74
+
75
+ def test_combined_validation_with_empty_fields(self):
76
+ validate_metadata_for_update(
77
+ {
78
+ UnifiedMetadataKey.TITLE: "",
79
+ UnifiedMetadataKey.ARTISTS: [],
80
+ UnifiedMetadataKey.RATING: 50,
81
+ },
82
+ normalized_rating_max_value=100,
83
+ )
@@ -0,0 +1,74 @@
1
+ import pytest
2
+
3
+ from audiometa._audio_file import _AudioFile
4
+ from audiometa.manager._rating_supporting.id3v2._Id3v2Manager import _Id3v2Manager as Id3v2Manager
5
+ from audiometa.manager._rating_supporting.riff._RiffManager import _RiffManager as RiffManager
6
+ from audiometa.manager._rating_supporting.vorbis._VorbisManager import _VorbisManager as VorbisManager
7
+ from audiometa.manager.id3v1._Id3v1Manager import _Id3v1Manager as Id3v1Manager
8
+ from audiometa.manager.id3v1.id3v1_raw_metadata_key import Id3v1RawMetadataKey
9
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
10
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
11
+
12
+
13
+ @pytest.mark.unit
14
+ class TestMetadataFormatManagersWriteAndRead:
15
+ def test_id3v1_manager_write_and_read(self):
16
+ with temp_file_with_metadata({}, "mp3") as test_file:
17
+ audio_file = _AudioFile(test_file)
18
+ manager = Id3v1Manager(audio_file)
19
+
20
+ manager.update_metadata(
21
+ {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.ARTISTS: ["Test Artist"]}
22
+ )
23
+
24
+ raw_metadata = manager._extract_mutagen_metadata()
25
+
26
+ assert raw_metadata.tags.get(Id3v1RawMetadataKey.TITLE) == ["Test Title"]
27
+ assert raw_metadata.tags.get(Id3v1RawMetadataKey.ARTISTS_NAMES_STR) == ["Test Artist"]
28
+
29
+ def test_id3v2_manager_write_and_read(self):
30
+ with temp_file_with_metadata({}, "mp3") as test_file:
31
+ audio_file = _AudioFile(test_file)
32
+ manager = Id3v2Manager(audio_file)
33
+
34
+ manager.update_metadata(
35
+ {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.ARTISTS: ["Test Artist"]}
36
+ )
37
+
38
+ raw_metadata = manager._extract_mutagen_metadata()
39
+
40
+ assert Id3v2Manager.Id3TextFrame.TITLE in raw_metadata
41
+ assert str(raw_metadata[Id3v2Manager.Id3TextFrame.TITLE][0]) == "Test Title"
42
+ assert Id3v2Manager.Id3TextFrame.ARTISTS in raw_metadata
43
+ artists_text = str(raw_metadata[Id3v2Manager.Id3TextFrame.ARTISTS][0])
44
+ assert "Test Artist" in artists_text
45
+
46
+ def test_riff_manager_write_and_read(self):
47
+ with temp_file_with_metadata({}, "wav") as test_file:
48
+ audio_file = _AudioFile(test_file)
49
+ manager = RiffManager(audio_file)
50
+
51
+ manager.update_metadata(
52
+ {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.ARTISTS: ["Test Artist"]}
53
+ )
54
+
55
+ raw_metadata = manager._extract_mutagen_metadata()
56
+
57
+ assert hasattr(raw_metadata, "info")
58
+ info_tags = getattr(raw_metadata, "info", {})
59
+ assert info_tags.get(RiffManager.RiffTagKey.TITLE) == ["Test Title"]
60
+ assert info_tags.get(RiffManager.RiffTagKey.ARTIST) == ["Test Artist"]
61
+
62
+ def test_vorbis_manager_write_and_read(self):
63
+ with temp_file_with_metadata({}, "flac") as test_file:
64
+ audio_file = _AudioFile(test_file)
65
+ manager = VorbisManager(audio_file)
66
+
67
+ manager.update_metadata(
68
+ {UnifiedMetadataKey.TITLE: "Test Title", UnifiedMetadataKey.ARTISTS: ["Test Artist"]}
69
+ )
70
+
71
+ raw_metadata = manager._extract_mutagen_metadata()
72
+
73
+ assert raw_metadata.get(VorbisManager.VorbisKey.TITLE) == ["Test Title"]
74
+ assert raw_metadata.get(VorbisManager.VorbisKey.ARTIST) == ["Test Artist"]
@@ -0,0 +1,26 @@
1
+ from unittest.mock import MagicMock
2
+
3
+ import pytest
4
+
5
+ from audiometa.exceptions import ConfigurationError
6
+ from audiometa.manager._rating_supporting.riff._RiffManager import _RiffManager as RiffManager
7
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
8
+
9
+
10
+ @pytest.mark.unit
11
+ class TestRiffManagerConfigurationError:
12
+ def test_update_not_using_mutagen_metadata_raises_configuration_error_when_metadata_keys_direct_map_write_is_none(
13
+ self,
14
+ ):
15
+ wave_audio_file = MagicMock()
16
+ wave_audio_file.file_extension = ".wav"
17
+ riff_manager = RiffManager(audio_file=wave_audio_file, normalized_rating_max_value=10)
18
+
19
+ # Manually set metadata_keys_direct_map_write to None to test the error condition
20
+ riff_manager.metadata_keys_direct_map_write = None
21
+
22
+ unified_metadata = {UnifiedMetadataKey.TITLE: "Test Title"}
23
+
24
+ with pytest.raises(ConfigurationError) as exc_info:
25
+ riff_manager._update_not_using_mutagen_metadata(unified_metadata)
26
+ assert "metadata_keys_direct_map_write must be set" in str(exc_info.value)
@@ -0,0 +1 @@
1
+ """Utility classes and functions for audio metadata processing."""
@@ -0,0 +1,205 @@
1
+ """Standard ID3v1 genre codes mapping.
2
+
3
+ This is the complete standard genre map used by both ID3v1 and RIFF formats. Genres 0-79 are from the original ID3v1
4
+ spec. Genres 80-125 were added by Winamp. Genres 126-147 were added by other players. Genres 148-191 were added in
5
+ Winamp 5.6 (November 2010).
6
+ """
7
+
8
+ ID3V1_GENRE_CODE_MAP = {
9
+ 0: "Blues",
10
+ 1: "Classic Rock",
11
+ 2: "Country",
12
+ 3: "Dance",
13
+ 4: "Disco",
14
+ 5: "Funk",
15
+ 6: "Grunge",
16
+ 7: "Hip-Hop",
17
+ 8: "Jazz",
18
+ 9: "Metal",
19
+ 10: "New Age",
20
+ 11: "Oldies",
21
+ 12: "Other",
22
+ 13: "Pop",
23
+ 14: "R&B",
24
+ 15: "Rap",
25
+ 16: "Reggae",
26
+ 17: "Rock",
27
+ 18: "Techno",
28
+ 19: "Industrial",
29
+ 20: "Alternative",
30
+ 21: "Ska",
31
+ 22: "Death Metal",
32
+ 23: "Pranks",
33
+ 24: "Soundtrack",
34
+ 25: "Euro-Techno",
35
+ 26: "Ambient",
36
+ 27: "Trip-Hop",
37
+ 28: "Vocal",
38
+ 29: "Jazz+Funk",
39
+ 30: "Fusion",
40
+ 31: "Trance",
41
+ 32: "Classical",
42
+ 33: "Instrumental",
43
+ 34: "Acid",
44
+ 35: "House",
45
+ 36: "Game",
46
+ 37: "Sound Clip",
47
+ 38: "Gospel",
48
+ 39: "Noise",
49
+ 40: "Alternative Rock",
50
+ 41: "Bass",
51
+ 42: "Soul",
52
+ 43: "Punk",
53
+ 44: "Space",
54
+ 45: "Meditative",
55
+ 46: "Instrumental Pop",
56
+ 47: "Instrumental Rock",
57
+ 48: "Ethnic",
58
+ 49: "Gothic",
59
+ 50: "Darkwave",
60
+ 51: "Techno-Industrial",
61
+ 52: "Electronic",
62
+ 53: "Pop-Folk",
63
+ 54: "Eurodance",
64
+ 55: "Dream",
65
+ 56: "Southern Rock",
66
+ 57: "Comedy",
67
+ 58: "Cult",
68
+ 59: "Gangsta",
69
+ 60: "Top 40",
70
+ 61: "Christian Rap",
71
+ 62: "Pop/Funk",
72
+ 63: "Jungle",
73
+ 64: "Native US",
74
+ 65: "Cabaret",
75
+ 66: "New Wave",
76
+ 67: "Psychedelic",
77
+ 68: "Rave",
78
+ 69: "Showtunes",
79
+ 70: "Trailer",
80
+ 71: "Lo-Fi",
81
+ 72: "Tribal",
82
+ 73: "Acid Punk",
83
+ 74: "Acid Jazz",
84
+ 75: "Polka",
85
+ 76: "Retro",
86
+ 77: "Musical",
87
+ 78: "Rock & Roll",
88
+ 79: "Hard Rock",
89
+ # Winamp extensions
90
+ 80: "Folk",
91
+ 81: "Folk-Rock",
92
+ 82: "National Folk",
93
+ 83: "Swing",
94
+ 84: "Fast Fusion",
95
+ 85: "Bebop",
96
+ 86: "Latin",
97
+ 87: "Revival",
98
+ 88: "Celtic",
99
+ 89: "Bluegrass",
100
+ 90: "Avantgarde",
101
+ 91: "Gothic Rock",
102
+ 92: "Progressive Rock",
103
+ 93: "Psychedelic Rock",
104
+ 94: "Symphonic Rock",
105
+ 95: "Slow Rock",
106
+ 96: "Big Band",
107
+ 97: "Chorus",
108
+ 98: "Easy Listening",
109
+ 99: "Acoustic",
110
+ 100: "Humour",
111
+ 101: "Speech",
112
+ 102: "Chanson",
113
+ 103: "Opera",
114
+ 104: "Chamber Music",
115
+ 105: "Sonata",
116
+ 106: "Symphony",
117
+ 107: "Booty Bass",
118
+ 108: "Primus",
119
+ 109: "Porn Groove",
120
+ 110: "Satire",
121
+ 111: "Slow Jam",
122
+ 112: "Club",
123
+ 113: "Tango",
124
+ 114: "Samba",
125
+ 115: "Folklore",
126
+ 116: "Ballad",
127
+ 117: "Power Ballad",
128
+ 118: "Rhythmic Soul",
129
+ 119: "Freestyle",
130
+ 120: "Duet",
131
+ 121: "Punk Rock",
132
+ 122: "Drum Solo",
133
+ 123: "A Cappella",
134
+ 124: "Euro-House",
135
+ 125: "Dance Hall",
136
+ # Other extensions
137
+ 126: "Goa",
138
+ 127: "Drum & Bass",
139
+ 128: "Club-House",
140
+ 129: "Hardcore",
141
+ 130: "Terror",
142
+ 131: "Indie",
143
+ 132: "BritPop",
144
+ 133: "Negerpunk",
145
+ 134: "Polsk Punk",
146
+ 135: "Beat",
147
+ 136: "Christian Gangsta Rap",
148
+ 137: "Heavy Metal",
149
+ 138: "Black Metal",
150
+ 139: "Crossover",
151
+ 140: "Contemporary Christian",
152
+ 141: "Christian Rock",
153
+ 142: "Merengue",
154
+ 143: "Salsa",
155
+ 144: "Thrash Metal",
156
+ 145: "Anime",
157
+ 146: "JPop",
158
+ 147: "Synthpop",
159
+ # Winamp 5.6 extensions (November 2010)
160
+ 148: "Christmas",
161
+ 149: "Art Rock",
162
+ 150: "Baroque",
163
+ 151: "Bhangra",
164
+ 152: "Big Beat",
165
+ 153: "Breakbeat",
166
+ 154: "Chillout",
167
+ 155: "Downtempo",
168
+ 156: "Dub",
169
+ 157: "EBM",
170
+ 158: "Eclectic",
171
+ 159: "Electro",
172
+ 160: "Electroclash",
173
+ 161: "Emo",
174
+ 162: "Experimental",
175
+ 163: "Garage",
176
+ 164: "Global",
177
+ 165: "IDM",
178
+ 166: "Illbient",
179
+ 167: "Industro-Goth",
180
+ 168: "Jam Band",
181
+ 169: "Krautrock",
182
+ 170: "Leftfield",
183
+ 171: "Lounge",
184
+ 172: "Math Rock",
185
+ 173: "New Romantic",
186
+ 174: "Nu-Breakz",
187
+ 175: "Post-Punk",
188
+ 176: "Post-Rock",
189
+ 177: "Psytrance",
190
+ 178: "Shoegaze",
191
+ 179: "Space Rock",
192
+ 180: "Trop Rock",
193
+ 181: "World Music",
194
+ 182: "Neoclassical",
195
+ 183: "Audiobook",
196
+ 184: "Audio Theatre",
197
+ 185: "Neue Deutsche Welle",
198
+ 186: "Podcast",
199
+ 187: "Indie Rock",
200
+ 188: "G-Funk",
201
+ 189: "Dubstep",
202
+ 190: "Garage Rock",
203
+ 191: "Psybient",
204
+ 255: None,
205
+ }
@@ -0,0 +1,31 @@
1
+ """Tag type constants for audio metadata handling.
2
+
3
+ This module defines the supported metadata formats and their file extension priorities for reading and writing audio
4
+ metadata across different file types.
5
+ """
6
+
7
+ from enum import Enum
8
+
9
+
10
+ class MetadataFormat(str, Enum):
11
+ """Enumeration of supported audio metadata formats."""
12
+
13
+ ID3V2 = "id3v2"
14
+ ID3V1 = "id3v1"
15
+ VORBIS = "vorbis"
16
+ RIFF = "riff"
17
+
18
+ @classmethod
19
+ def get_priorities(cls) -> dict[str, list["MetadataFormat"]]:
20
+ """Get tag format priorities for different file formats.
21
+
22
+ First tag format in each list has highest priority.
23
+
24
+ Returns:
25
+ dictionary mapping file extensions to ordered list of tag types
26
+ """
27
+ return {
28
+ ".flac": [cls.VORBIS, cls.ID3V2, cls.ID3V1],
29
+ ".mp3": [cls.ID3V2, cls.ID3V1],
30
+ ".wav": [cls.RIFF, cls.ID3V2, cls.ID3V1],
31
+ }
@@ -0,0 +1,16 @@
1
+ """Metadata writing strategy constants for audio metadata handling."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class MetadataWritingStrategy(str, Enum):
7
+ """Strategy for handling metadata when writing to files with existing metadata in other formats."""
8
+
9
+ SYNC = "sync"
10
+ """Write to native format and synchronize other metadata formats that are already present (default)."""
11
+
12
+ PRESERVE = "preserve"
13
+ """Write to native format only, preserve existing metadata in other formats."""
14
+
15
+ CLEANUP = "cleanup"
16
+ """Write to native format and remove all non-native metadata formats."""
@@ -0,0 +1,24 @@
1
+ """Utility functions for handling mutagen exceptions."""
2
+
3
+ from audiometa.exceptions import FileCorruptedError
4
+
5
+
6
+ def handle_mutagen_exception(operation: str, file_path: str, exception: Exception) -> None:
7
+ """Handle exceptions from mutagen operations.
8
+
9
+ Re-raises standard I/O exceptions as-is and converts other exceptions
10
+ (including mutagen-specific ones) to FileCorruptedError.
11
+
12
+ Args:
13
+ operation: Description of the operation being performed (e.g., "save metadata", "extract RIFF metadata from")
14
+ file_path: Path to the file being operated on
15
+ exception: The exception that was raised
16
+
17
+ Raises:
18
+ IOError, OSError, PermissionError: Re-raised as-is for standard I/O errors
19
+ FileCorruptedError: Raised for mutagen-specific or other unexpected exceptions
20
+ """
21
+ if isinstance(exception, IOError | OSError | PermissionError):
22
+ raise exception
23
+ msg = f"Failed to {operation} {file_path}: {exception!s}"
24
+ raise FileCorruptedError(msg) from exception
@@ -0,0 +1,24 @@
1
+ """OS-specific dependency checkers for verifying system dependencies."""
2
+
3
+ import platform
4
+
5
+ from audiometa.utils.os_dependencies_checker.base import OsDependenciesChecker
6
+ from audiometa.utils.os_dependencies_checker.macos import MacOSDependenciesChecker
7
+ from audiometa.utils.os_dependencies_checker.ubuntu import UbuntuDependenciesChecker
8
+ from audiometa.utils.os_dependencies_checker.windows import WindowsDependenciesChecker
9
+
10
+
11
+ def get_dependencies_checker() -> OsDependenciesChecker | None:
12
+ """Get the appropriate OS-specific dependencies checker.
13
+
14
+ Returns:
15
+ OS-specific checker instance, or None if OS not supported
16
+ """
17
+ system = platform.system().lower()
18
+ if system == "darwin":
19
+ return MacOSDependenciesChecker()
20
+ if system == "linux":
21
+ return UbuntuDependenciesChecker()
22
+ if system == "windows":
23
+ return WindowsDependenciesChecker()
24
+ return None
@@ -0,0 +1,62 @@
1
+ """Base class for OS-specific dependency checkers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class OsDependenciesChecker(ABC):
7
+ """Base class for OS-specific dependency checkers."""
8
+
9
+ @classmethod
10
+ @abstractmethod
11
+ def get_os_type(cls) -> str:
12
+ """Get OS type identifier."""
13
+
14
+ @abstractmethod
15
+ def check_tool_available(self, tool_name: str) -> bool:
16
+ """Check if a tool is available (in PATH or default locations).
17
+
18
+ Args:
19
+ tool_name: Name of the tool to check
20
+
21
+ Returns:
22
+ True if tool is available, False otherwise
23
+ """
24
+
25
+ @abstractmethod
26
+ def get_installed_version(self, package: str, expected_version: str | None = None) -> str | None:
27
+ """Get installed version of a package.
28
+
29
+ Args:
30
+ package: Package name
31
+ expected_version: Optional expected/pinned version
32
+
33
+ Returns:
34
+ Installed version string, or None if not found
35
+ """
36
+
37
+ @staticmethod
38
+ def _normalize_version(version: str) -> str:
39
+ """Normalize version string by removing revision suffix.
40
+
41
+ Args:
42
+ version: Version string (e.g., "7.1_4" or "1.5.0")
43
+
44
+ Returns:
45
+ Normalized version without revision suffix
46
+ """
47
+ return version.split("_")[0]
48
+
49
+ @staticmethod
50
+ def _versions_match(version1: str, version2: str) -> bool:
51
+ """Check if two version strings match (handles different precision).
52
+
53
+ Args:
54
+ version1: First version string
55
+ version2: Second version string
56
+
57
+ Returns:
58
+ True if versions match, False otherwise
59
+ """
60
+ v1_normalized = OsDependenciesChecker._normalize_version(version1)
61
+ v2_normalized = OsDependenciesChecker._normalize_version(version2)
62
+ return v1_normalized == v2_normalized or v2_normalized.startswith(v1_normalized + ".")
@@ -0,0 +1,77 @@
1
+ """Configuration loading for OS-specific dependency checkers."""
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+
6
+
7
+ def _load_config_file(project_root: Path, filename: str) -> dict | None:
8
+ """Load a TOML configuration file."""
9
+ config_path = project_root / filename
10
+ if not config_path.exists():
11
+ return None
12
+
13
+ try:
14
+ with config_path.open("rb") as f:
15
+ return tomllib.load(f)
16
+ except Exception:
17
+ return None
18
+
19
+
20
+ def load_dependencies_pinned_versions() -> dict[str, dict[str, str]] | None:
21
+ """Load pinned versions from system-dependencies-prod.toml and system-dependencies-test-only.toml.
22
+
23
+ Returns:
24
+ Dictionary mapping tool names to OS-specific versions, or None if config not found
25
+ """
26
+ # Try to find config files relative to this file
27
+ # This file is in audiometa/utils/os_dependencies_checker/, so go up to project root
28
+ project_root = Path(__file__).parent.parent.parent.parent
29
+
30
+ # Load prod and test configs
31
+ prod_config = _load_config_file(project_root, "system-dependencies-prod.toml")
32
+ test_config = _load_config_file(project_root, "system-dependencies-test-only.toml")
33
+
34
+ if not prod_config and not test_config:
35
+ return None
36
+
37
+ try:
38
+ # Merge configs (test can override prod if needed, though they shouldn't overlap)
39
+ config = {}
40
+ if prod_config:
41
+ config.update(prod_config)
42
+ if test_config:
43
+ # Merge OS sections
44
+ for os_type in ["ubuntu", "macos", "windows"]:
45
+ if os_type in test_config:
46
+ if os_type not in config:
47
+ config[os_type] = {}
48
+ config[os_type].update(test_config[os_type])
49
+
50
+ pinned_versions: dict[str, dict[str, str]] = {}
51
+
52
+ # Extract versions for each OS
53
+ for os_type in ["ubuntu", "macos", "windows"]:
54
+ if os_type not in config:
55
+ continue
56
+
57
+ os_config = config[os_type]
58
+ for tool in ["ffmpeg", "flac", "mediainfo", "id3v2", "bwfmetaedit", "exiftool"]:
59
+ if tool not in os_config:
60
+ continue
61
+
62
+ version_value = os_config[tool]
63
+ # Handle both string values and dict values (for bwfmetaedit, exiftool on Windows)
64
+ if isinstance(version_value, str):
65
+ version = version_value
66
+ elif isinstance(version_value, dict) and "pinned_version" in version_value:
67
+ version = version_value["pinned_version"]
68
+ else:
69
+ continue
70
+
71
+ if tool not in pinned_versions:
72
+ pinned_versions[tool] = {}
73
+ pinned_versions[tool][os_type] = version
74
+ except Exception:
75
+ return None
76
+ else:
77
+ return pinned_versions