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
audiometa/cli.py ADDED
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env python3
2
+ """AudioMeta CLI - Command-line interface for audio metadata operations."""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from audiometa import (
11
+ UnifiedMetadataKey,
12
+ delete_all_metadata,
13
+ get_full_metadata,
14
+ get_unified_metadata,
15
+ update_metadata,
16
+ validate_metadata_for_update,
17
+ )
18
+ from audiometa.exceptions import (
19
+ FileTypeNotSupportedError,
20
+ InvalidRatingValueError,
21
+ MetadataFormatNotSupportedByAudioFormatError,
22
+ )
23
+ from audiometa.utils.metadata_format import MetadataFormat
24
+ from audiometa.utils.types import UnifiedMetadata
25
+
26
+
27
+ def format_output(data: Any, output_format: str) -> str:
28
+ """Format output data according to specified format."""
29
+ if output_format == "json":
30
+ return json.dumps(data, indent=2)
31
+ if output_format == "yaml":
32
+ try:
33
+ import yaml # type: ignore[import-untyped]
34
+
35
+ result = yaml.dump(data, default_flow_style=False)
36
+ return str(result) if result is not None else ""
37
+ except ImportError:
38
+ sys.stderr.write("Warning: PyYAML not installed, falling back to JSON\n")
39
+ return json.dumps(data, indent=2)
40
+ elif output_format == "table":
41
+ return format_as_table(data)
42
+ else:
43
+ return str(data)
44
+
45
+
46
+ def _handle_file_operation_error(exception: Exception, file_path: Path | str, continue_on_error: bool) -> None:
47
+ """Handle exceptions from file operations and write appropriate error messages to stderr.
48
+
49
+ Args:
50
+ exception: The exception that was caught
51
+ file_path: The path to the file being operated on
52
+ continue_on_error: Whether to continue on errors or exit
53
+ """
54
+ if isinstance(exception, FileNotFoundError):
55
+ error_msg = f"Error: File not found: {file_path}\n"
56
+ elif isinstance(exception, FileTypeNotSupportedError):
57
+ error_msg = f"Error: File type not supported: {file_path}\n"
58
+ elif isinstance(exception, PermissionError | OSError):
59
+ error_msg = f"Error: {exception!s}\n"
60
+ else:
61
+ error_msg = f"Error: {exception!s}\n"
62
+
63
+ sys.stderr.write(error_msg)
64
+
65
+ if not continue_on_error:
66
+ sys.exit(1)
67
+
68
+
69
+ def format_as_table(data: dict[str, Any]) -> str:
70
+ """Format metadata as a simple table."""
71
+ lines = []
72
+
73
+ # Handle unified metadata dict directly (from unified command)
74
+ if "unified_metadata" not in data and isinstance(data, dict):
75
+ # Check if this is a unified metadata dict (has UnifiedMetadataKey values)
76
+ unified_keys = {
77
+ "title",
78
+ "artists",
79
+ "album",
80
+ "album_artists",
81
+ "genres_names",
82
+ "release_date",
83
+ "track_number",
84
+ "disc_number",
85
+ "disc_total",
86
+ "rating",
87
+ "bpm",
88
+ "language",
89
+ "composer",
90
+ "publisher",
91
+ "copyright",
92
+ "unsynchronized_lyrics",
93
+ "comment",
94
+ "replaygain",
95
+ "archival_location",
96
+ "isrc",
97
+ }
98
+ if unified_keys.intersection(set(data.keys())):
99
+ # This is a unified metadata dict, wrap it
100
+ data = {"unified_metadata": data}
101
+
102
+ if "unified_metadata" in data:
103
+ lines.append("=== UNIFIED METADATA ===")
104
+ for key, value in data["unified_metadata"].items():
105
+ if value is not None:
106
+ lines.append(f"{key:20}: {value}")
107
+ lines.append("")
108
+
109
+ if "technical_info" in data:
110
+ lines.append("=== TECHNICAL INFO ===")
111
+ for key, value in data["technical_info"].items():
112
+ if value is not None:
113
+ lines.append(f"{key:20}: {value}")
114
+ lines.append("")
115
+
116
+ if "metadata_format" in data:
117
+ lines.append("=== FORMAT METADATA ===")
118
+ for metadata_format_name, format_data in data["metadata_format"].items():
119
+ if format_data:
120
+ lines.append(f"\n{metadata_format_name.upper()}:")
121
+ for key, value in format_data.items():
122
+ if value is not None:
123
+ lines.append(f" {key:18}: {value}")
124
+
125
+ return "\n".join(lines)
126
+
127
+
128
+ def _read_metadata(args: argparse.Namespace) -> None:
129
+ """Read and display metadata from audio file(s)."""
130
+ files = expand_file_patterns(
131
+ args.files, getattr(args, "recursive", False), getattr(args, "continue_on_error", False)
132
+ )
133
+
134
+ if not files:
135
+ return # No files found, but continue_on_error was set
136
+
137
+ for file_path in files:
138
+ try:
139
+ if getattr(args, "format_type", None) == "unified":
140
+ metadata: Any = get_unified_metadata(file_path)
141
+ else:
142
+ metadata = get_full_metadata(
143
+ file_path,
144
+ include_headers=not getattr(args, "no_headers", False),
145
+ include_technical=not getattr(args, "no_technical", False),
146
+ )
147
+
148
+ output = format_output(metadata, args.output_format)
149
+
150
+ if args.output:
151
+ try:
152
+ with Path(args.output).open("w") as f:
153
+ f.write(output)
154
+ except (PermissionError, OSError) as e:
155
+ _handle_file_operation_error(e, args.output, args.continue_on_error)
156
+ else:
157
+ sys.stdout.write(output)
158
+ if not output.endswith("\n"):
159
+ sys.stdout.write("\n")
160
+
161
+ except (FileTypeNotSupportedError, FileNotFoundError, PermissionError, OSError, Exception) as e:
162
+ _handle_file_operation_error(e, file_path, args.continue_on_error)
163
+
164
+
165
+ def _write_metadata(args: argparse.Namespace) -> None:
166
+ """Write metadata to audio file(s)."""
167
+ files = expand_file_patterns(
168
+ args.files, getattr(args, "recursive", False), getattr(args, "continue_on_error", False)
169
+ )
170
+
171
+ # Build metadata dictionary from command line arguments
172
+ metadata: UnifiedMetadata = {}
173
+
174
+ # String fields
175
+ if args.title and args.title.strip():
176
+ metadata[UnifiedMetadataKey.TITLE] = args.title
177
+ if args.album and args.album.strip():
178
+ metadata[UnifiedMetadataKey.ALBUM] = args.album
179
+ if args.language and args.language.strip():
180
+ metadata[UnifiedMetadataKey.LANGUAGE] = args.language
181
+ if args.publisher and args.publisher.strip():
182
+ metadata[UnifiedMetadataKey.PUBLISHER] = args.publisher
183
+ if args.copyright and args.copyright.strip():
184
+ metadata[UnifiedMetadataKey.COPYRIGHT] = args.copyright
185
+ if args.lyrics and args.lyrics.strip():
186
+ metadata[UnifiedMetadataKey.UNSYNCHRONIZED_LYRICS] = args.lyrics
187
+ if args.comment and args.comment.strip():
188
+ metadata[UnifiedMetadataKey.COMMENT] = args.comment
189
+ if args.replaygain and args.replaygain.strip():
190
+ metadata[UnifiedMetadataKey.REPLAYGAIN] = args.replaygain
191
+ if args.archival_location and args.archival_location.strip():
192
+ metadata[UnifiedMetadataKey.ARCHIVAL_LOCATION] = args.archival_location
193
+ if args.isrc and args.isrc.strip():
194
+ metadata[UnifiedMetadataKey.ISRC] = args.isrc
195
+
196
+ # List fields (can be specified multiple times)
197
+ if args.artist:
198
+ artists = [a.strip() for a in args.artist if a and a.strip()]
199
+ if artists:
200
+ metadata[UnifiedMetadataKey.ARTISTS] = artists
201
+ if args.album_artists:
202
+ album_artists = [a.strip() for a in args.album_artists if a and a.strip()]
203
+ if album_artists:
204
+ metadata[UnifiedMetadataKey.ALBUM_ARTISTS] = album_artists
205
+ if args.genre:
206
+ genres = [g.strip() for g in args.genre if g and g.strip()]
207
+ if genres:
208
+ metadata[UnifiedMetadataKey.GENRES_NAMES] = genres
209
+ if args.composer:
210
+ composers = [c.strip() for c in args.composer if c and c.strip()]
211
+ if composers:
212
+ metadata[UnifiedMetadataKey.COMPOSERS] = composers
213
+
214
+ # Integer fields
215
+ if args.rating is not None:
216
+ if args.rating < 0:
217
+ sys.stderr.write("Error: rating cannot be negative\n")
218
+ sys.exit(1)
219
+ metadata[UnifiedMetadataKey.RATING] = args.rating
220
+ if args.disc_number is not None:
221
+ if args.disc_number < 0:
222
+ sys.stderr.write("Error: disc-number cannot be negative\n")
223
+ sys.exit(1)
224
+ metadata[UnifiedMetadataKey.DISC_NUMBER] = args.disc_number
225
+ if args.disc_total is not None:
226
+ if args.disc_total < 0:
227
+ sys.stderr.write("Error: disc-total cannot be negative\n")
228
+ sys.exit(1)
229
+ metadata[UnifiedMetadataKey.DISC_TOTAL] = args.disc_total
230
+ if args.bpm is not None:
231
+ if args.bpm < 0:
232
+ sys.stderr.write("Error: bpm cannot be negative\n")
233
+ sys.exit(1)
234
+ metadata[UnifiedMetadataKey.BPM] = args.bpm
235
+
236
+ # Release date (year takes precedence over release-date if both specified)
237
+ if args.year is not None:
238
+ if args.year < 0:
239
+ sys.stderr.write("Error: year cannot be negative\n")
240
+ sys.exit(1)
241
+ metadata[UnifiedMetadataKey.RELEASE_DATE] = str(args.year)
242
+ elif args.release_date and args.release_date.strip():
243
+ metadata[UnifiedMetadataKey.RELEASE_DATE] = args.release_date
244
+
245
+ # Track number (string, can be "5" or "5/12")
246
+ if args.track_number and args.track_number.strip():
247
+ metadata[UnifiedMetadataKey.TRACK_NUMBER] = args.track_number
248
+
249
+ # Check if any metadata was provided
250
+ if not metadata:
251
+ sys.stderr.write("Error: No metadata fields specified\n")
252
+ sys.exit(1)
253
+
254
+ try:
255
+ validate_metadata_for_update(metadata)
256
+ except (ValueError, InvalidRatingValueError) as e:
257
+ sys.stderr.write(f"Error: {e}\n")
258
+ sys.exit(1)
259
+
260
+ for file_path in files:
261
+ try:
262
+ update_kwargs: dict[str, Any] = {}
263
+ if hasattr(args, "force_format") and args.force_format:
264
+ format_map = {
265
+ "id3v2": MetadataFormat.ID3V2,
266
+ "id3v1": MetadataFormat.ID3V1,
267
+ "vorbis": MetadataFormat.VORBIS,
268
+ "riff": MetadataFormat.RIFF,
269
+ }
270
+ update_kwargs["metadata_format"] = format_map[args.force_format]
271
+ update_metadata(file_path, metadata, **update_kwargs)
272
+ if len(files) > 1:
273
+ sys.stdout.write(f"Updated metadata for: {file_path}\n")
274
+ else:
275
+ sys.stdout.write("Updated metadata\n")
276
+
277
+ except (FileTypeNotSupportedError, FileNotFoundError, PermissionError, OSError, Exception) as e:
278
+ if isinstance(e, MetadataFormatNotSupportedByAudioFormatError):
279
+ sys.stderr.write(f"Error: {e}\n")
280
+ if not args.continue_on_error:
281
+ sys.exit(1)
282
+ else:
283
+ _handle_file_operation_error(e, file_path, args.continue_on_error)
284
+
285
+
286
+ def _delete_metadata(args: argparse.Namespace) -> None:
287
+ """Delete metadata from audio file(s)."""
288
+ files = expand_file_patterns(
289
+ args.files, getattr(args, "recursive", False), getattr(args, "continue_on_error", False)
290
+ )
291
+
292
+ for file_path in files:
293
+ try:
294
+ success = delete_all_metadata(file_path)
295
+ if success:
296
+ if len(files) > 1:
297
+ sys.stdout.write(f"Deleted metadata from: {file_path}\n")
298
+ else:
299
+ sys.stdout.write("Deleted metadata\n")
300
+ else:
301
+ sys.stderr.write(f"Warning: No metadata found in: {file_path}\n")
302
+
303
+ except (FileTypeNotSupportedError, FileNotFoundError, PermissionError, OSError, Exception) as e:
304
+ _handle_file_operation_error(e, file_path, args.continue_on_error)
305
+
306
+
307
+ def expand_file_patterns(patterns: list[str], recursive: bool = False, continue_on_error: bool = False) -> list[Path]:
308
+ """Expand file patterns and globs into a list of Path objects."""
309
+ files = []
310
+
311
+ for pattern in patterns:
312
+ path = Path(pattern)
313
+
314
+ if path.exists():
315
+ if path.is_file():
316
+ files.append(path)
317
+ elif path.is_dir() and recursive:
318
+ # Recursively find audio files
319
+ for ext in [".mp3", ".flac", ".wav"]:
320
+ files.extend(path.rglob(f"*{ext}"))
321
+ else:
322
+ # Try glob pattern
323
+ pattern_path = Path(pattern)
324
+ if "*" in pattern or "?" in pattern or "[" in pattern:
325
+ # Use glob for patterns
326
+ if pattern_path.is_absolute():
327
+ matches = list(pattern_path.parent.glob(pattern_path.name))
328
+ else:
329
+ matches = list(Path().glob(pattern))
330
+ else:
331
+ matches = [pattern_path]
332
+ for match in matches:
333
+ # Skip hidden files (those starting with .)
334
+ if not match.name.startswith(".") and match.is_file():
335
+ files.append(match)
336
+
337
+ if not files:
338
+ if continue_on_error:
339
+ sys.stderr.write("Warning: No valid audio files found\n")
340
+ return []
341
+ error_msg = "Error: No valid audio files found\n"
342
+ sys.stderr.write(error_msg)
343
+ sys.exit(1)
344
+
345
+ return files
346
+
347
+
348
+ def _create_parser() -> argparse.ArgumentParser:
349
+ """Create and configure the argument parser."""
350
+ parser = argparse.ArgumentParser(
351
+ description="AudioMeta CLI - Command-line interface for audio metadata operations",
352
+ formatter_class=argparse.RawDescriptionHelpFormatter,
353
+ epilog="""
354
+ Examples:
355
+ audiometa read song.mp3 # Read full metadata
356
+ audiometa unified song.mp3 # Read unified metadata only
357
+ audiometa read *.mp3 --format table # Read multiple files as table
358
+ audiometa write song.mp3 --title "New Title" --artist "Artist"
359
+ audiometa delete song.mp3 # Delete all metadata
360
+ audiometa read music/ --recursive # Process directory recursively
361
+ """,
362
+ )
363
+
364
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
365
+
366
+ # Read command
367
+ read_parser = subparsers.add_parser("read", help="Read metadata from audio file(s)")
368
+ read_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
369
+ read_parser.add_argument(
370
+ "--format",
371
+ choices=["json", "yaml", "table"],
372
+ default="json",
373
+ dest="output_format",
374
+ help="Output format (default: json)",
375
+ )
376
+ read_parser.add_argument("--output", "-o", help="Output file (default: stdout)")
377
+ read_parser.add_argument("--no-headers", action="store_true", help="Exclude header information")
378
+ read_parser.add_argument("--no-technical", action="store_true", help="Exclude technical information")
379
+ read_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
380
+ read_parser.add_argument(
381
+ "--continue-on-error", action="store_true", help="Continue processing other files on error"
382
+ )
383
+ read_parser.set_defaults(func=_read_metadata)
384
+
385
+ # Unified command
386
+ unified_parser = subparsers.add_parser("unified", help="Read unified metadata only")
387
+ unified_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
388
+ unified_parser.add_argument(
389
+ "--format",
390
+ choices=["json", "yaml", "table"],
391
+ default="json",
392
+ dest="output_format",
393
+ help="Output format (default: json)",
394
+ )
395
+ unified_parser.add_argument("--output", "-o", help="Output file (default: stdout)")
396
+ unified_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
397
+ unified_parser.add_argument(
398
+ "--continue-on-error", action="store_true", help="Continue processing other files on error"
399
+ )
400
+ unified_parser.set_defaults(func=_read_metadata, format_type="unified")
401
+
402
+ # Write command
403
+ write_parser = subparsers.add_parser("write", help="Write metadata to audio file(s)")
404
+ write_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
405
+ write_parser.add_argument("--title", help="Song title")
406
+ write_parser.add_argument(
407
+ "--artist", action="append", help="Artist name (can be specified multiple times for multiple artists)"
408
+ )
409
+ write_parser.add_argument("--album", help="Album name")
410
+ write_parser.add_argument(
411
+ "--album-artist",
412
+ action="append",
413
+ dest="album_artists",
414
+ help="Album artist name (can be specified multiple times)",
415
+ )
416
+ write_parser.add_argument("--year", type=int, help="Release year")
417
+ write_parser.add_argument("--release-date", help="Release date in YYYY or YYYY-MM-DD format")
418
+ write_parser.add_argument(
419
+ "--genre", action="append", help="Genre (can be specified multiple times for multiple genres)"
420
+ )
421
+ write_parser.add_argument("--track-number", help="Track number (e.g., '5' or '5/12')")
422
+ write_parser.add_argument("--disc-number", type=int, help="Disc number")
423
+ write_parser.add_argument("--disc-total", type=int, help="Total number of discs")
424
+ write_parser.add_argument("--rating", type=float, help="Rating value (integer or whole-number float like 196.0)")
425
+ write_parser.add_argument("--bpm", type=int, help="Beats per minute")
426
+ write_parser.add_argument("--language", help="Language code (3 characters, e.g., 'eng')")
427
+ write_parser.add_argument("--composer", action="append", help="Composer name (can be specified multiple times)")
428
+ write_parser.add_argument("--publisher", help="Publisher name")
429
+ write_parser.add_argument("--copyright", help="Copyright information")
430
+ write_parser.add_argument("--lyrics", help="Unsynchronized lyrics text")
431
+ write_parser.add_argument("--comment", help="Comment")
432
+ write_parser.add_argument("--replaygain", help="ReplayGain information")
433
+ write_parser.add_argument("--archival-location", help="Archival location")
434
+ write_parser.add_argument("--isrc", help="International Standard Recording Code (12 characters)")
435
+ write_parser.add_argument(
436
+ "--force-format",
437
+ choices=["id3v2", "id3v1", "vorbis", "riff"],
438
+ help="Force a specific metadata format (id3v2, id3v1, vorbis, or riff)",
439
+ )
440
+ write_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
441
+ write_parser.add_argument(
442
+ "--continue-on-error", action="store_true", help="Continue processing other files on error"
443
+ )
444
+ write_parser.set_defaults(func=_write_metadata)
445
+
446
+ # Delete command
447
+ delete_parser = subparsers.add_parser("delete", help="Delete all metadata from audio file(s)")
448
+ delete_parser.add_argument("files", nargs="+", help="Audio file(s) or pattern(s)")
449
+ delete_parser.add_argument("--recursive", "-r", action="store_true", help="Process directories recursively")
450
+ delete_parser.add_argument(
451
+ "--continue-on-error", action="store_true", help="Continue processing other files on error"
452
+ )
453
+ delete_parser.set_defaults(func=_delete_metadata)
454
+
455
+ return parser
456
+
457
+
458
+ def main() -> None:
459
+ """Main CLI entry point."""
460
+ parser = _create_parser()
461
+ args = parser.parse_args()
462
+
463
+ if not args.command:
464
+ parser.print_help()
465
+ sys.exit(1)
466
+
467
+ try:
468
+ args.func(args)
469
+ except KeyboardInterrupt:
470
+ sys.exit(1)
471
+ except Exception:
472
+ sys.exit(1)
473
+
474
+
475
+ if __name__ == "__main__":
476
+ main()
@@ -0,0 +1,167 @@
1
+ """Custom exceptions for the audiometa library.
2
+
3
+ This module defines all custom exception classes used throughout the audiometa library for handling various error
4
+ conditions related to audio file metadata processing.
5
+ """
6
+
7
+
8
+ class FileCorruptedError(Exception):
9
+ """Raised when an audio file appears to be corrupted or invalid."""
10
+
11
+
12
+ class FlacMd5CheckFailedError(FileCorruptedError):
13
+ """Raised when FLAC MD5 checksum verification fails."""
14
+
15
+
16
+ class FileByteMismatchError(FileCorruptedError):
17
+ """Raised when file bytes do not match expected content."""
18
+
19
+
20
+ class InvalidChunkDecodeError(FileCorruptedError):
21
+ """Raised when a chunk cannot be decoded properly."""
22
+
23
+
24
+ class DurationNotFoundError(FileCorruptedError):
25
+ """Raised when audio duration cannot be determined."""
26
+
27
+
28
+ class AudioFileMetadataParseError(FileCorruptedError):
29
+ """Raised when audio file metadata cannot be parsed from external tools.
30
+
31
+ This error indicates that the output from tools like ffprobe could not be
32
+ parsed as valid JSON or metadata format.
33
+
34
+ Examples:
35
+ - ffprobe returns invalid JSON when probing audio files
36
+ - Metadata parsing fails due to unexpected output format
37
+ """
38
+
39
+
40
+ class FileTypeNotSupportedError(Exception):
41
+ """Raised when the audio file type is not supported by the library."""
42
+
43
+
44
+ class ConfigurationError(Exception):
45
+ """Raised when there is a configuration error in the metadata manager.
46
+
47
+ This error indicates that the metadata manager was not properly configured or initialized with the required
48
+ parameters.
49
+ """
50
+
51
+
52
+ class MetadataFormatNotSupportedByAudioFormatError(Exception):
53
+ """Raised when attempting to read metadata from a format not supported by the audio format of the file.
54
+
55
+ This error indicates that the requested metadata format is not supported by the audio format of the file.
56
+
57
+ Examples:
58
+ - Trying to read metadata from RIFF format from an MP3 file
59
+ - Trying to read metadata from Vorbis format from a WAV file
60
+ """
61
+
62
+
63
+ class MetadataFieldNotSupportedByMetadataFormatError(Exception):
64
+ """Raised when attempting to read or write metadata not supported by the format.
65
+
66
+ This error indicates a format limitation (e.g., trying to write BPM to RIFF),
67
+ not a code error. The format simply does not support the requested metadata field.
68
+
69
+ Examples:
70
+ - Trying to read/write ratings to RIFF
71
+ - Trying to read/write BPM to ID3v1
72
+ - Trying to read/write album artist to ID3v1
73
+ """
74
+
75
+
76
+ class MetadataFieldNotSupportedByLibError(Exception):
77
+ """Raised when attempting to read or write a metadata field that is not supported by the library at all.
78
+
79
+ This error indicates that the requested metadata field is not implemented or supported
80
+ by any format in the library, regardless of the audio file format.
81
+
82
+ Examples:
83
+ - Trying to read/write a custom field that doesn't exist in UnifiedMetadataKey
84
+ - Trying to read/write a field that is not implemented in any metadata manager
85
+ """
86
+
87
+
88
+ class MetadataWritingConflictParametersError(Exception):
89
+ """Raised when conflicting metadata writing parameters are specified.
90
+
91
+ This error indicates that the user has provided parameters that cannot
92
+ be used together for metadata writing operations.
93
+
94
+ Examples:
95
+ - Specifying both metadata_strategy and metadata_format
96
+ - Other mutually exclusive metadata writing parameters
97
+ """
98
+
99
+
100
+ class InvalidMetadataFieldTypeError(TypeError):
101
+ """Raised when a metadata field value has an unexpected type.
102
+
103
+ Attributes:
104
+ field: str - the unified metadata field name (e.g. 'artists')
105
+ expected_type: str - human-readable expected type (e.g. 'list[str]')
106
+ actual_type: str - name of the actual type received
107
+ value: object - the actual value passed
108
+ """
109
+
110
+ def __init__(self, field: str, expected_type: str, actual_value: object):
111
+ """Initialize the exception with field details.
112
+
113
+ Args:
114
+ field: The unified metadata field name.
115
+ expected_type: Human-readable expected type.
116
+ actual_value: The actual value that was passed.
117
+ """
118
+ actual_type = type(actual_value).__name__
119
+ message = (
120
+ f"Invalid type for metadata field '{field}': expected {expected_type}, "
121
+ f"got {actual_type} (value={actual_value!r})"
122
+ )
123
+ super().__init__(message)
124
+ self.field = field
125
+ self.expected_type = expected_type
126
+ self.actual_type = actual_type
127
+ self.value = actual_value
128
+
129
+
130
+ class InvalidRatingValueError(Exception):
131
+ """Raised when an invalid rating value is provided.
132
+
133
+ This error indicates that the rating value cannot be converted to a valid
134
+ numeric rating or is not in the expected format.
135
+
136
+ Examples:
137
+ - Non-numeric string values like "invalid" or "abc"
138
+ - Values that cannot be converted to integers
139
+ - None values when a rating is expected
140
+ """
141
+
142
+
143
+ class InvalidMetadataFieldFormatError(ValueError):
144
+ """Raised when a metadata field value has an invalid format.
145
+
146
+ This error indicates that the value has the correct type but does not
147
+ match the required format pattern.
148
+
149
+ Attributes:
150
+ field: str - the unified metadata field name (e.g. 'release_date')
151
+ expected_format: str - human-readable expected format (e.g. 'YYYY or YYYY-MM-DD')
152
+ value: object - the actual value passed
153
+ """
154
+
155
+ def __init__(self, field: str, expected_format: str, actual_value: object):
156
+ """Initialize the exception with field format details.
157
+
158
+ Args:
159
+ field: The unified metadata field name.
160
+ expected_format: Human-readable expected format.
161
+ actual_value: The actual value that was passed.
162
+ """
163
+ message = f"Invalid format for metadata field '{field}': expected {expected_format}, got {actual_value!r}"
164
+ super().__init__(message)
165
+ self.field = field
166
+ self.expected_format = expected_format
167
+ self.value = actual_value