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,130 @@
1
+ import stat
2
+ import subprocess
3
+ import sys
4
+
5
+ import pytest
6
+
7
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
8
+
9
+
10
+ @pytest.mark.e2e
11
+ class TestCLIFileAccessErrors:
12
+ def test_cli_read_nonexistent_file(self, tmp_path):
13
+ nonexistent_file = tmp_path / "nonexistent.mp3"
14
+ result = subprocess.run(
15
+ [sys.executable, "-m", "audiometa", "read", str(nonexistent_file)],
16
+ capture_output=True,
17
+ text=True,
18
+ check=False,
19
+ )
20
+ assert result.returncode == 1
21
+ assert "error" in result.stderr.lower()
22
+
23
+ def test_cli_read_with_continue_on_error(self, tmp_path):
24
+ nonexistent_file = tmp_path / "nonexistent.mp3"
25
+ result = subprocess.run(
26
+ [sys.executable, "-m", "audiometa", "read", str(nonexistent_file), "--continue-on-error"],
27
+ capture_output=True,
28
+ text=True,
29
+ check=False,
30
+ )
31
+ assert result.returncode == 0
32
+
33
+ def test_cli_output_file_permission_error(self, sample_mp3_file, tmp_path):
34
+ # Create read-only file (more reliable cross-platform than read-only directory)
35
+ output_file = tmp_path / "output.json"
36
+ output_file.write_text("existing content")
37
+ # Make file read-only
38
+ output_file.chmod(stat.S_IREAD)
39
+
40
+ try:
41
+ # Try to write output to read-only file
42
+ result = subprocess.run(
43
+ [sys.executable, "-m", "audiometa", "read", str(sample_mp3_file), "--output", str(output_file)],
44
+ capture_output=True,
45
+ text=True,
46
+ check=False,
47
+ )
48
+
49
+ # Should fail due to permission error
50
+ assert result.returncode != 0
51
+ # Should contain error message about permission or writing
52
+ stderr_output = result.stderr.lower()
53
+ assert "error" in stderr_output or "permission" in stderr_output or "cannot" in stderr_output
54
+ finally:
55
+ # Restore write permissions for cleanup
56
+ output_file.chmod(stat.S_IWRITE | stat.S_IREAD)
57
+
58
+ def test_cli_output_file_permission_error_with_continue(self, sample_mp3_file, tmp_path):
59
+ # Create read-only file (more reliable cross-platform than read-only directory)
60
+ output_file = tmp_path / "output.json"
61
+ output_file.write_text("existing content")
62
+ # Make file read-only
63
+ output_file.chmod(stat.S_IREAD)
64
+
65
+ try:
66
+ # Try to write output with continue-on-error flag
67
+ result = subprocess.run(
68
+ [
69
+ sys.executable,
70
+ "-m",
71
+ "audiometa",
72
+ "read",
73
+ str(sample_mp3_file),
74
+ "--output",
75
+ str(output_file),
76
+ "--continue-on-error",
77
+ ],
78
+ capture_output=True,
79
+ text=True,
80
+ check=False,
81
+ )
82
+
83
+ # Should succeed overall (exit code 0) because continue-on-error prevents exit
84
+ assert result.returncode == 0
85
+ stderr_output = result.stderr.lower()
86
+ assert "error" in stderr_output or "permission" in stderr_output or "denied" in stderr_output
87
+ finally:
88
+ # Restore write permissions for cleanup
89
+ output_file.chmod(stat.S_IWRITE | stat.S_IREAD)
90
+
91
+ def test_cli_output_file_nonexistent_directory(self, sample_mp3_file, tmp_path):
92
+ # Try to write to a file in a nonexistent directory
93
+ nonexistent_dir = tmp_path / "nonexistent" / "subdir"
94
+ output_file = nonexistent_dir / "output.json"
95
+
96
+ result = subprocess.run(
97
+ [sys.executable, "-m", "audiometa", "read", str(sample_mp3_file), "--output", str(output_file)],
98
+ capture_output=True,
99
+ text=True,
100
+ check=False,
101
+ )
102
+
103
+ # Should fail due to nonexistent directory
104
+ assert result.returncode != 0
105
+ stderr_output = result.stderr.lower()
106
+ assert "error" in stderr_output
107
+
108
+ def test_cli_output_file_unified_command(self, tmp_path):
109
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
110
+ # Create read-only file (more reliable cross-platform than read-only directory)
111
+ output_file = tmp_path / "output.json"
112
+ output_file.write_text("existing content")
113
+ # Make file read-only
114
+ output_file.chmod(stat.S_IREAD)
115
+
116
+ try:
117
+ result = subprocess.run(
118
+ [sys.executable, "-m", "audiometa", "unified", str(temp_file_path), "--output", str(output_file)],
119
+ capture_output=True,
120
+ text=True,
121
+ check=False,
122
+ )
123
+
124
+ # Should fail due to permission error
125
+ assert result.returncode != 0
126
+ stderr_output = result.stderr.lower()
127
+ assert "error" in stderr_output or "permission" in stderr_output or "cannot" in stderr_output
128
+ finally:
129
+ # Restore write permissions for cleanup
130
+ output_file.chmod(stat.S_IWRITE | stat.S_IREAD)
@@ -0,0 +1,118 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
7
+
8
+
9
+ @pytest.mark.e2e
10
+ class TestCLIFormatOutputErrors:
11
+ def test_cli_yaml_format_with_pyyaml(self, capsys):
12
+ # Test the format_output function when PyYAML is installed
13
+ import json
14
+
15
+ from audiometa.cli import format_output
16
+
17
+ # Test that format_output uses YAML when yaml is available
18
+ test_data = {"test": "data", "number": 42}
19
+ result = format_output(test_data, "yaml")
20
+
21
+ # Should NOT be JSON (YAML format is different)
22
+ json_result = json.dumps(test_data, indent=2)
23
+ assert result != json_result
24
+
25
+ # Should NOT print warning to stderr
26
+ captured = capsys.readouterr()
27
+ assert "Warning: PyYAML not installed" not in captured.err
28
+ assert "falling back" not in captured.err
29
+
30
+ # Result should contain YAML formatting (includes colons for key-value pairs)
31
+ assert "test: data" in result or "test: 'data'" in result
32
+ assert "number: 42" in result
33
+
34
+ def test_cli_yaml_format_without_pyyaml(self, monkeypatch, capsys):
35
+ # Test the format_output function directly with mocked yaml import
36
+ import sys
37
+
38
+ from audiometa.cli import format_output
39
+
40
+ # Remove yaml from sys.modules if it exists
41
+ if "yaml" in sys.modules:
42
+ del sys.modules["yaml"]
43
+
44
+ # Mock the import to raise ImportError
45
+ original_import = __import__
46
+
47
+ def mock_import(name, *args, **kwargs):
48
+ if name == "yaml":
49
+ msg = "No module named 'yaml'"
50
+ raise ImportError(msg)
51
+ return original_import(name, *args, **kwargs)
52
+
53
+ monkeypatch.setattr("builtins.__import__", mock_import)
54
+
55
+ # Test that format_output falls back to JSON when yaml is not available
56
+ test_data = {"test": "data"}
57
+ result = format_output(test_data, "yaml")
58
+
59
+ # Should return JSON format
60
+ import json
61
+
62
+ expected_json = json.dumps(test_data, indent=2)
63
+ assert result == expected_json
64
+
65
+ # Should print warning to stderr
66
+ captured = capsys.readouterr()
67
+ assert "Warning: PyYAML not installed, falling back to JSON" in captured.err
68
+
69
+ def test_cli_invalid_format_argument(self):
70
+ result = subprocess.run(
71
+ [sys.executable, "-m", "audiometa", "read", "nonexistent.mp3", "--format", "invalid"],
72
+ capture_output=True,
73
+ text=True,
74
+ check=False,
75
+ )
76
+
77
+ # Should fail due to invalid format choice
78
+ assert result.returncode != 0
79
+ # argparse should show error about invalid choice
80
+ stderr_output = result.stderr.lower()
81
+ assert "invalid choice" in stderr_output or "error" in stderr_output
82
+
83
+ def test_cli_invalid_output_path_empty_string(self):
84
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
85
+ result = subprocess.run(
86
+ [sys.executable, "-m", "audiometa", "read", str(temp_file_path), "--output", ""],
87
+ capture_output=True,
88
+ text=True,
89
+ check=False,
90
+ )
91
+
92
+ # Should succeed - empty output path means stdout
93
+ assert result.returncode == 0
94
+ assert len(result.stdout.strip()) > 0
95
+
96
+ def test_cli_conflicting_format_options_read(self):
97
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
98
+ result = subprocess.run(
99
+ [
100
+ sys.executable,
101
+ "-m",
102
+ "audiometa",
103
+ "read",
104
+ str(temp_file_path),
105
+ "--format",
106
+ "table",
107
+ "--no-headers",
108
+ "--no-technical",
109
+ ],
110
+ capture_output=True,
111
+ text=True,
112
+ check=False,
113
+ )
114
+
115
+ # Should succeed - these options are compatible
116
+ assert result.returncode == 0
117
+ # Table format with no headers/technical should still work
118
+ assert len(result.stdout.strip()) > 0
@@ -0,0 +1,172 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
7
+
8
+
9
+ @pytest.mark.e2e
10
+ class TestCLIInputValidationErrors:
11
+ def test_cli_invalid_rating_value_negative(self):
12
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
13
+ result = subprocess.run(
14
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--rating", "-5"],
15
+ capture_output=True,
16
+ text=True,
17
+ check=False,
18
+ )
19
+
20
+ # Should fail due to negative rating - CLI validates explicitly
21
+ assert result.returncode != 0
22
+ stderr_output = result.stderr.lower()
23
+ assert "error" in stderr_output
24
+ assert "rating" in stderr_output
25
+
26
+ def test_cli_rating_value_allowed_without_normalization(self):
27
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
28
+ # Any integer rating value should be allowed when normalized_rating_max_value is not provided
29
+ # Using a valid ID3v2 profile value (196 = 4 stars in BASE_255_NON_PROPORTIONAL profile)
30
+ result = subprocess.run(
31
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--rating", "196"],
32
+ capture_output=True,
33
+ text=True,
34
+ check=False,
35
+ )
36
+
37
+ # Should succeed - no write profile validation when normalized_rating_max_value is None
38
+ assert result.returncode == 0
39
+
40
+ def test_cli_rating_whole_number_float_allowed(self):
41
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
42
+ # Whole-number floats like 196.0 should be accepted and converted to integers
43
+ result = subprocess.run(
44
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--rating", "196.0"],
45
+ capture_output=True,
46
+ text=True,
47
+ check=False,
48
+ )
49
+
50
+ # Should succeed - whole-number floats are converted to integers in raw mode
51
+ assert result.returncode == 0
52
+
53
+ def test_cli_rating_value_non_multiple_of_10_allowed(self):
54
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
55
+ result = subprocess.run(
56
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--rating", "37"],
57
+ capture_output=True,
58
+ text=True,
59
+ check=False,
60
+ )
61
+
62
+ # Should succeed - no write profile validation when normalized_rating_max_value is None
63
+ assert result.returncode == 0
64
+
65
+ def test_cli_invalid_rating_value_non_numeric(self):
66
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
67
+ result = subprocess.run(
68
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--rating", "not-a-number"],
69
+ capture_output=True,
70
+ text=True,
71
+ check=False,
72
+ )
73
+
74
+ # Should fail due to non-numeric rating
75
+ assert result.returncode != 0
76
+ stderr_output = result.stderr.lower()
77
+ assert "invalid" in stderr_output.lower() or "error" in stderr_output
78
+
79
+ def test_cli_valid_rating_multiple_of_10(self):
80
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
81
+ result = subprocess.run(
82
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--rating", "128"],
83
+ capture_output=True,
84
+ text=True,
85
+ check=False,
86
+ )
87
+
88
+ # Should succeed - any integer rating value is allowed
89
+ assert result.returncode == 0
90
+ assert "updated metadata" in result.stdout.lower()
91
+
92
+ def test_cli_invalid_year_value_non_numeric(self):
93
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
94
+ result = subprocess.run(
95
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--year", "not-a-year"],
96
+ capture_output=True,
97
+ text=True,
98
+ check=False,
99
+ )
100
+
101
+ # Should fail due to non-numeric year
102
+ assert result.returncode != 0
103
+ stderr_output = result.stderr.lower()
104
+ assert "invalid" in stderr_output.lower() or "error" in stderr_output
105
+
106
+ def test_cli_invalid_year_value_negative(self):
107
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
108
+ result = subprocess.run(
109
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--year", "-2023"],
110
+ capture_output=True,
111
+ text=True,
112
+ check=False,
113
+ )
114
+
115
+ # Should fail due to invalid date format (negative year doesn't match YYYY format)
116
+ assert result.returncode != 0
117
+ stderr_output = result.stderr.lower()
118
+ assert "error" in stderr_output or "invalid" in stderr_output
119
+
120
+ def test_cli_valid_year_value_future(self):
121
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
122
+ future_year = str(2030 + 1) # Future year is valid
123
+ result = subprocess.run(
124
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path), "--year", future_year],
125
+ capture_output=True,
126
+ text=True,
127
+ check=False,
128
+ )
129
+
130
+ # Should succeed - future years are allowed
131
+ assert result.returncode == 0
132
+ assert "updated metadata" in result.stdout.lower()
133
+
134
+ def test_cli_write_no_metadata_fields(self):
135
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
136
+ result = subprocess.run(
137
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path)],
138
+ capture_output=True,
139
+ text=True,
140
+ check=False,
141
+ )
142
+
143
+ # Should fail due to no metadata fields
144
+ assert result.returncode != 0
145
+ stderr_output = result.stderr.lower()
146
+ assert "no metadata fields specified" in stderr_output
147
+
148
+ def test_cli_write_empty_title_artist_album(self):
149
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
150
+ result = subprocess.run(
151
+ [
152
+ sys.executable,
153
+ "-m",
154
+ "audiometa",
155
+ "write",
156
+ str(temp_file_path),
157
+ "--title",
158
+ "",
159
+ "--artist",
160
+ "",
161
+ "--album",
162
+ "",
163
+ ],
164
+ capture_output=True,
165
+ text=True,
166
+ check=False,
167
+ )
168
+
169
+ # Should fail - empty strings are not considered valid metadata
170
+ assert result.returncode != 0
171
+ stderr_output = result.stderr.lower()
172
+ assert "no metadata fields specified" in stderr_output
@@ -0,0 +1,49 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
7
+
8
+
9
+ @pytest.mark.e2e
10
+ class TestCLIMissingFieldsValidation:
11
+ def test_cli_write_no_metadata_fields(self):
12
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
13
+ result = subprocess.run(
14
+ [sys.executable, "-m", "audiometa", "write", str(temp_file_path)],
15
+ capture_output=True,
16
+ text=True,
17
+ check=False,
18
+ )
19
+
20
+ # Should fail due to no metadata fields
21
+ assert result.returncode != 0
22
+ stderr_output = result.stderr.lower()
23
+ assert "no metadata fields specified" in stderr_output
24
+
25
+ def test_cli_write_empty_title_artist_album(self):
26
+ with temp_file_with_metadata({}, "mp3") as temp_file_path:
27
+ result = subprocess.run(
28
+ [
29
+ sys.executable,
30
+ "-m",
31
+ "audiometa",
32
+ "write",
33
+ str(temp_file_path),
34
+ "--title",
35
+ "",
36
+ "--artist",
37
+ "",
38
+ "--album",
39
+ "",
40
+ ],
41
+ capture_output=True,
42
+ text=True,
43
+ check=False,
44
+ )
45
+
46
+ # Should fail - empty strings are not considered valid metadata
47
+ assert result.returncode != 0
48
+ stderr_output = result.stderr.lower()
49
+ assert "no metadata fields specified" in stderr_output
@@ -0,0 +1,160 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from audiometa.test.helpers.temp_file_with_metadata import temp_file_with_metadata
7
+
8
+
9
+ @pytest.mark.e2e
10
+ class TestCLIMultipleFilesErrors:
11
+ def test_cli_multiple_files_mixed_success_failure_continue_on_error(
12
+ self, sample_mp3_file, sample_wav_file, tmp_path
13
+ ):
14
+ # Create unsupported file type
15
+ unsupported_file = tmp_path / "unsupported.txt"
16
+ unsupported_file.write_text("not audio")
17
+
18
+ # Run CLI with mixed files and continue_on_error=True
19
+ result = subprocess.run(
20
+ [
21
+ sys.executable,
22
+ "-m",
23
+ "audiometa",
24
+ "read",
25
+ str(sample_mp3_file),
26
+ str(sample_wav_file),
27
+ str(unsupported_file),
28
+ "--continue-on-error",
29
+ ],
30
+ capture_output=True,
31
+ text=True,
32
+ check=False,
33
+ )
34
+
35
+ # Should succeed overall (exit code 0)
36
+ assert result.returncode == 0
37
+
38
+ # Should contain error messages for failed files
39
+ stderr_output = result.stderr.lower()
40
+ assert "error processing" in stderr_output or "error" in stderr_output
41
+
42
+ # Should contain output for successful files (at least some JSON output)
43
+ assert "{" in result.stdout or "}" in result.stdout
44
+
45
+ def test_cli_multiple_files_mixed_success_failure_no_continue(self, sample_mp3_file, tmp_path):
46
+ # Create unsupported file type
47
+ unsupported_file = tmp_path / "unsupported.txt"
48
+ unsupported_file.write_text("not audio")
49
+
50
+ # Run CLI with mixed files and continue_on_error=False (default)
51
+ result = subprocess.run(
52
+ [sys.executable, "-m", "audiometa", "read", str(sample_mp3_file), str(unsupported_file)],
53
+ capture_output=True,
54
+ text=True,
55
+ check=False,
56
+ )
57
+
58
+ # Should fail overall (exit code 1) due to the unsupported file
59
+ assert result.returncode == 1
60
+
61
+ # Should contain error message
62
+ stderr_output = result.stderr.lower()
63
+ assert "error" in stderr_output
64
+
65
+ def test_cli_multiple_files_all_fail_continue_on_error(self, tmp_path):
66
+ # Create unsupported files
67
+ unsupported1 = tmp_path / "unsupported1.txt"
68
+ unsupported1.write_text("not audio")
69
+
70
+ unsupported2 = tmp_path / "unsupported2.jpg"
71
+ unsupported2.write_text("not audio")
72
+
73
+ # Run CLI with all failing files and continue_on_error=True
74
+ result = subprocess.run(
75
+ [sys.executable, "-m", "audiometa", "read", str(unsupported1), str(unsupported2), "--continue-on-error"],
76
+ capture_output=True,
77
+ text=True,
78
+ check=False,
79
+ )
80
+
81
+ # Should succeed overall (exit code 0) despite all files failing
82
+ assert result.returncode == 0
83
+
84
+ # Should contain error messages for all failed files
85
+ stderr_output = result.stderr.lower()
86
+ assert "error" in stderr_output
87
+
88
+ def test_cli_multiple_files_write_mixed_success_failure(self, tmp_path):
89
+ with temp_file_with_metadata({}, "mp3") as temp_mp3_path, temp_file_with_metadata({}, "flac") as temp_flac_path:
90
+ # Create unsupported file type
91
+ unsupported_file = tmp_path / "unsupported.txt"
92
+ unsupported_file.write_text("not audio")
93
+
94
+ # Run CLI write command with mixed files
95
+ result = subprocess.run(
96
+ [
97
+ sys.executable,
98
+ "-m",
99
+ "audiometa",
100
+ "write",
101
+ str(temp_mp3_path),
102
+ str(temp_flac_path),
103
+ str(unsupported_file),
104
+ "--title",
105
+ "Test Title",
106
+ "--continue-on-error",
107
+ ],
108
+ capture_output=True,
109
+ text=True,
110
+ check=False,
111
+ )
112
+
113
+ # Should succeed overall (exit code 0)
114
+ assert result.returncode == 0
115
+
116
+ # Should contain success messages for valid files
117
+ stdout_output = result.stdout.lower()
118
+ assert (
119
+ "updated metadata for" in stdout_output
120
+ ), f"Expected success message in stdout but got:\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
121
+
122
+ # Should contain error message for unsupported file
123
+ stderr_output = result.stderr.lower()
124
+ assert "error" in stderr_output
125
+
126
+ def test_cli_multiple_files_delete_mixed_success_failure(self, tmp_path):
127
+ with temp_file_with_metadata({}, "mp3") as temp_mp3_path, temp_file_with_metadata({}, "wav") as temp_wav_path:
128
+ # Create unsupported file type
129
+ unsupported_file = tmp_path / "unsupported.txt"
130
+ unsupported_file.write_text("not audio")
131
+
132
+ # Run CLI delete command with mixed files
133
+ result = subprocess.run(
134
+ [
135
+ sys.executable,
136
+ "-m",
137
+ "audiometa",
138
+ "delete",
139
+ str(temp_mp3_path),
140
+ str(temp_wav_path),
141
+ str(unsupported_file),
142
+ "--continue-on-error",
143
+ ],
144
+ capture_output=True,
145
+ text=True,
146
+ check=False,
147
+ )
148
+
149
+ # Should succeed overall (exit code 0)
150
+ assert result.returncode == 0
151
+
152
+ # Should contain messages for processed files
153
+ stdout_output = result.stdout.lower()
154
+ assert (
155
+ "deleted" in stdout_output or "found" in stdout_output
156
+ ), f"Expected success message in stdout but got:\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
157
+
158
+ # Should contain error message for unsupported file
159
+ stderr_output = result.stderr.lower()
160
+ assert "error" in stderr_output