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,242 @@
1
+ """ID3v1 raw metadata handling."""
2
+
3
+ import struct
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from mutagen._file import FileType
9
+
10
+ from ._constants import (
11
+ ID3V1_MIN_COMMENT_LENGTH_FOR_TRACK_NUMBER,
12
+ ID3V1_TAG_SIZE,
13
+ ID3V1_TRACK_NUMBER_POSITION,
14
+ ID3V1_TRACK_NUMBER_VALUE_POSITION,
15
+ )
16
+ from .id3v1_raw_metadata_key import Id3v1RawMetadataKey
17
+
18
+
19
+ class Id3v1RawMetadata(FileType):
20
+ """A custom file-like object for ID3v1 tags, providing a consistent interface similar to mutagen.
21
+
22
+ This class encapsulates the ID3v1 128-byte structure and provides a clean interface for accessing and modifying tag
23
+ data. It supports both reading and writing using direct file manipulation.
24
+ """
25
+
26
+ @dataclass
27
+ class Id3v1Tag:
28
+ title: str = ""
29
+ artists_names_str: str = ""
30
+ album_name: str = ""
31
+ year: str = ""
32
+ comment: str = ""
33
+ track_number: int | None = None
34
+ genre_code: int = 255 # 255 is undefined genre
35
+
36
+ def __init__(self, fileobj: Any):
37
+ self.fileobj = fileobj
38
+ object.__setattr__(self, "tags", None)
39
+ self._load_tags()
40
+
41
+ def _load_tags(self) -> None:
42
+ # Handle both file objects and file paths
43
+ if isinstance(self.fileobj, str | Path):
44
+ with Path(self.fileobj).open("rb") as f:
45
+ f.seek(-ID3V1_TAG_SIZE, 2) # Seek from end
46
+ data = f.read(ID3V1_TAG_SIZE)
47
+ else:
48
+ self.fileobj.seek(-ID3V1_TAG_SIZE, 2) # Seek from end
49
+ data = self.fileobj.read(ID3V1_TAG_SIZE)
50
+
51
+ if not data.startswith(b"TAG"):
52
+ self.tags = None
53
+ return
54
+
55
+ # Parse the fixed structure into our tag object
56
+ tag = self.Id3v1Tag(
57
+ title=data[3:33].strip(b"\0").decode("latin1", "replace"),
58
+ artists_names_str=data[33:63].strip(b"\0").decode("latin1", "replace"),
59
+ album_name=data[63:93].strip(b"\0").decode("latin1", "replace"),
60
+ year=data[93:97].strip(b"\0").decode("latin1", "replace"),
61
+ genre_code=struct.unpack("B", data[127:128])[0],
62
+ )
63
+
64
+ # Handle ID3v1.1 track number in comment field
65
+ try:
66
+ comment = data[97:127]
67
+
68
+ # Check for ID3v1.1 track number format (bytes 125-126)
69
+ if (
70
+ len(comment) >= ID3V1_MIN_COMMENT_LENGTH_FOR_TRACK_NUMBER
71
+ and comment[ID3V1_TRACK_NUMBER_POSITION] == 0
72
+ and comment[ID3V1_TRACK_NUMBER_VALUE_POSITION] != 0
73
+ ):
74
+ # ID3v1.1 format: track number in last two bytes
75
+ tag.track_number = comment[ID3V1_TRACK_NUMBER_VALUE_POSITION]
76
+ tag.comment = comment[:ID3V1_TRACK_NUMBER_POSITION].strip(b"\0").decode("latin1", "replace")
77
+ else:
78
+ # Regular ID3v1 format: no track number
79
+ tag.track_number = None
80
+ tag.comment = comment.strip(b"\0").decode("latin1", "replace")
81
+ except Exception:
82
+ pass
83
+
84
+ # Convert to dictionary format similar to other metadata formats
85
+ tags_dict: dict[Id3v1RawMetadataKey, list[str]] = {}
86
+ if tag.title:
87
+ tags_dict[Id3v1RawMetadataKey.TITLE] = [tag.title]
88
+ if tag.artists_names_str:
89
+ tags_dict[Id3v1RawMetadataKey.ARTISTS_NAMES_STR] = [tag.artists_names_str]
90
+ if tag.album_name:
91
+ tags_dict[Id3v1RawMetadataKey.ALBUM] = [tag.album_name]
92
+ if tag.year:
93
+ tags_dict[Id3v1RawMetadataKey.YEAR] = [tag.year]
94
+ if tag.genre_code is not None:
95
+ tags_dict[Id3v1RawMetadataKey.GENRE_CODE_OR_NAME] = [str(tag.genre_code)]
96
+ if tag.track_number and tag.track_number != 0:
97
+ tags_dict[Id3v1RawMetadataKey.TRACK_NUMBER] = [str(tag.track_number)]
98
+ if tag.comment:
99
+ tags_dict[Id3v1RawMetadataKey.COMMENT] = [tag.comment]
100
+ object.__setattr__(self, "tags", tags_dict)
101
+
102
+ def save(self) -> None:
103
+ """Save ID3v1 metadata to file using direct file manipulation."""
104
+ if not self.tags:
105
+ return
106
+
107
+ # Read the entire file
108
+ if isinstance(self.fileobj, str | Path): # type: ignore[unreachable]
109
+ # File path
110
+ with Path(self.fileobj).open("rb") as f:
111
+ file_data = bytearray(f.read())
112
+ else:
113
+ # File object - use the same pattern as _load_tags
114
+ self.fileobj.seek(0)
115
+ file_data = bytearray(self.fileobj.read())
116
+
117
+ # Create ID3v1 tag data
118
+ tag_data = self._create_id3v1_tag_data()
119
+
120
+ # Remove existing ID3v1 tag if present
121
+ self._remove_existing_id3v1_tag(file_data)
122
+
123
+ # Append new ID3v1 tag
124
+ file_data.extend(tag_data)
125
+
126
+ # Write back to file
127
+ if isinstance(self.fileobj, str | Path):
128
+ # File path
129
+ with Path(self.fileobj).open("wb") as f:
130
+ f.write(file_data)
131
+ else:
132
+ # File object
133
+ self.fileobj.seek(0)
134
+ self.fileobj.write(file_data)
135
+ self.fileobj.truncate()
136
+
137
+ def _create_id3v1_tag_data(self) -> bytes:
138
+ """Create 128-byte ID3v1 tag data from current tags."""
139
+ from typing import cast as type_cast
140
+
141
+ tags: dict[Id3v1RawMetadataKey, list[str]] = type_cast(dict[Id3v1RawMetadataKey, list[str]], self.tags)
142
+ if not tags:
143
+ msg = "Tags must be loaded before creating tag data"
144
+ raise ValueError(msg)
145
+ # Initialize with null bytes
146
+ tag_data = bytearray(ID3V1_TAG_SIZE)
147
+
148
+ # TAG identifier (bytes 0-2)
149
+ tag_data[0:3] = b"TAG"
150
+
151
+ # Title (bytes 3-32, 30 chars max)
152
+ title = tags.get(Id3v1RawMetadataKey.TITLE, [""])[0]
153
+ title_bytes = self._truncate_string(title, 30).encode("latin-1", errors="ignore")
154
+ tag_data[3 : 3 + len(title_bytes)] = title_bytes
155
+
156
+ # Artist (bytes 33-62, 30 chars max)
157
+ artist = tags.get(Id3v1RawMetadataKey.ARTISTS_NAMES_STR, [""])[0]
158
+ artist_bytes = self._truncate_string(artist, 30).encode("latin-1", errors="ignore")
159
+ tag_data[33 : 33 + len(artist_bytes)] = artist_bytes
160
+
161
+ # Album (bytes 63-92, 30 chars max)
162
+ album = tags.get(Id3v1RawMetadataKey.ALBUM, [""])[0]
163
+ album_bytes = self._truncate_string(album, 30).encode("latin-1", errors="ignore")
164
+ tag_data[63 : 63 + len(album_bytes)] = album_bytes
165
+
166
+ # Year (bytes 93-96, 4 chars max)
167
+ year = tags.get(Id3v1RawMetadataKey.YEAR, [""])[0]
168
+ year_bytes = self._truncate_string(year, 4).encode("latin-1", errors="ignore")
169
+ tag_data[93 : 93 + len(year_bytes)] = year_bytes
170
+
171
+ # Comment and track number (bytes 97-126, 28 chars for comment + 2 for track)
172
+ comment = tags.get(Id3v1RawMetadataKey.COMMENT, [""])[0]
173
+ comment_bytes = self._truncate_string(comment, 28).encode("latin-1", errors="ignore")
174
+ tag_data[97 : 97 + len(comment_bytes)] = comment_bytes
175
+
176
+ # Track number (bytes 125-126 for ID3v1.1)
177
+ track_number = tags.get(Id3v1RawMetadataKey.TRACK_NUMBER, ["0"])[0]
178
+ if track_number and track_number != "0":
179
+ track_num = max(0, min(255, int(track_number)))
180
+ if track_num > 0:
181
+ tag_data[125] = 0 # Null byte to indicate track number presence
182
+ tag_data[126] = track_num
183
+
184
+ # Genre (byte 127)
185
+ genre_code = tags.get(Id3v1RawMetadataKey.GENRE_CODE_OR_NAME, ["255"])[0]
186
+ try:
187
+ tag_data[127] = int(genre_code)
188
+ except ValueError:
189
+ tag_data[127] = 255 # Unknown genre
190
+
191
+ return bytes(tag_data)
192
+
193
+ def _remove_existing_id3v1_tag(self, file_data: bytearray) -> bool:
194
+ """Remove existing ID3v1 tag from file data if present.
195
+
196
+ Returns:
197
+ bool: True if a tag was removed, False otherwise
198
+ """
199
+ if len(file_data) >= ID3V1_TAG_SIZE:
200
+ # Check if last 128 bytes contain ID3v1 tag
201
+ last_128 = file_data[-ID3V1_TAG_SIZE:]
202
+ if last_128[:3] == b"TAG":
203
+ # Remove the last 128 bytes
204
+ del file_data[-ID3V1_TAG_SIZE:]
205
+ return True
206
+ return False
207
+
208
+ def _truncate_string(self, text: str, max_length: int) -> str:
209
+ """Truncate string to maximum length, handling encoding properly."""
210
+ if len(text) <= max_length:
211
+ return text
212
+ return text[:max_length]
213
+
214
+ @property
215
+ def mime(self) -> list[str]:
216
+ """Return a list of MIME types this file type could be."""
217
+ return ["audio/mpeg"] # ID3v1 is typically used with MP3 files
218
+
219
+ def add_tags(self) -> None:
220
+ """Add a new ID3v1 tag to the file."""
221
+ if self.tags is None:
222
+ object.__setattr__(self, "tags", {})
223
+
224
+ def delete(self, filename: str) -> None:
225
+ """Remove tags from a file."""
226
+ try:
227
+ # Read the entire file
228
+ with Path(filename).open("rb") as f:
229
+ file_data = bytearray(f.read())
230
+
231
+ # Remove existing ID3v1 tag if present
232
+ if self._remove_existing_id3v1_tag(file_data):
233
+ # Write back to file
234
+ with Path(filename).open("wb") as f:
235
+ f.write(file_data)
236
+ except Exception:
237
+ pass # Ignore errors during deletion
238
+
239
+ @staticmethod
240
+ def score(_filename: str, _fileobj: Any, _header: Any) -> int:
241
+ """Return a score indicating how likely this class can handle the file."""
242
+ return 0 # We don't want this to be auto-detected by mutagen
@@ -0,0 +1,13 @@
1
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
2
+
3
+ from ...utils.types import RawMetadataKey
4
+
5
+
6
+ class Id3v1RawMetadataKey(RawMetadataKey):
7
+ TITLE = UnifiedMetadataKey.TITLE
8
+ ARTISTS_NAMES_STR = UnifiedMetadataKey.ARTISTS
9
+ ALBUM = UnifiedMetadataKey.ALBUM
10
+ GENRE_CODE_OR_NAME = "GENRE_CODE_OR_NAME"
11
+ YEAR = "YEAR"
12
+ TRACK_NUMBER = "TRACK_NUMBER"
13
+ COMMENT = "COMMENT"
@@ -0,0 +1 @@
1
+ """Test package for audiometa-python."""
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """Script to create test audio files for testing."""
3
+
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+
8
+ def create_silent_audio_file(output_path: Path, duration: float = 1.0, sample_rate: int = 44100):
9
+ """Create a silent audio file using ffmpeg."""
10
+ try:
11
+ # Determine codec based on file extension
12
+ ext = output_path.suffix.lower()
13
+ if ext == ".mp3":
14
+ codec = "libmp3lame"
15
+ bitrate = "128k"
16
+ elif ext == ".flac":
17
+ codec = "flac"
18
+ bitrate = None
19
+ elif ext == ".wav":
20
+ codec = "pcm_s16le"
21
+ bitrate = None
22
+ else:
23
+ codec = "libmp3lame"
24
+ bitrate = "128k"
25
+
26
+ cmd = [
27
+ "ffmpeg",
28
+ "-f",
29
+ "lavfi",
30
+ "-i",
31
+ f"anullsrc=duration={duration}:sample_rate={sample_rate}",
32
+ "-c:a",
33
+ codec,
34
+ "-y", # Overwrite output file
35
+ str(output_path),
36
+ ]
37
+
38
+ if bitrate:
39
+ cmd.insert(-1, "-b:a")
40
+ cmd.insert(-1, bitrate)
41
+
42
+ subprocess.run(cmd, check=True, capture_output=True)
43
+ except (subprocess.CalledProcessError, FileNotFoundError):
44
+ return False
45
+ else:
46
+ return True
47
+
48
+
49
+ def create_test_files():
50
+ """Create test audio files in different formats."""
51
+ test_dir = Path(__file__).parent
52
+ test_dir.mkdir(exist_ok=True)
53
+
54
+ # Create different format test files
55
+ formats = [
56
+ ("sample.mp3", 1.0),
57
+ ("sample.flac", 1.0),
58
+ ("sample.wav", 1.0),
59
+ ]
60
+
61
+ for filename, duration in formats:
62
+ output_path = test_dir / filename
63
+ if not output_path.exists():
64
+ success = create_silent_audio_file(output_path, duration)
65
+ if not success:
66
+ pass
67
+ else:
68
+ pass
69
+
70
+
71
+ if __name__ == "__main__":
72
+ create_test_files()
@@ -0,0 +1,51 @@
1
+ """Test helpers package for audiometa.
2
+
3
+ This package provides utilities for creating and managing test files with metadata.
4
+ Classes are organized by metadata format in subdirectories following clean architecture principles.
5
+
6
+ Main Classes:
7
+ - TempFileWithMetadata: Context manager for test files with comprehensive metadata operations
8
+ Located in: temp_file_with_metadata.py
9
+
10
+ Organized by Format:
11
+
12
+ ID3v1 Format (id3v1/):
13
+ - Id3v1Tool: Wrapper for id3v1 operations using id3v2 tool
14
+ - ID3v1MetadataDeleter: Deleting ID3v1 metadata
15
+
16
+ ID3v2 Format (id3v2/):
17
+ - ID3v2MetadataVerifier: Verifying ID3v2 metadata
18
+ - ID3v2MetadataSetter: Setting ID3v2 metadata including multiple frame values
19
+
20
+ - ManualID3v2FrameCreator: Manual binary construction of ID3v2 frames for testing edge cases
21
+ - ID3HeaderVerifier: Verifying ID3v1/ID3v2 headers
22
+
23
+ Vorbis Format (vorbis/):
24
+ - VorbisMetadataSetter: Setting Vorbis metadata and managing multiple Vorbis comment values
25
+ - VorbisMetadataDeleter: Deleting Vorbis metadata
26
+ - VorbisHeaderVerifier: Verifying Vorbis comment headers and retrieving metadata information
27
+ - VorbisMetadataVerifier: Verifying Vorbis comments
28
+
29
+ RIFF Format (riff/):
30
+ - RIFFMetadataVerifier: Verifying RIFF metadata
31
+ - RIFFMetadataSetter: Setting RIFF metadata, managing separator-based metadata, and managing multiple RIFF chunk values
32
+ - RIFFMetadataDeleter: Deleting RIFF metadata
33
+ - RIFFHeaderVerifier: Verifying RIFF INFO chunk headers and retrieving metadata information
34
+
35
+ Common Utilities (common/):
36
+ - AudioFileCreator: Utilities for creating minimal audio files
37
+ - ComprehensiveMetadataVerifier: Cross-format comprehensive verification and header detection
38
+ - run_script: Unified function for running external scripts with proper error handling
39
+
40
+ Usage:
41
+ from audiometa.test.helpers.temp_file_with_metadata import TempFileWithMetadata
42
+ from audiometa.test.helpers.id3v1 import Id3v1Tool, ID3v1MetadataDeleter
43
+ from audiometa.test.helpers.id3v2 import (
44
+ ID3v2MetadataVerifier, ID3v2MetadataSetter, ManualID3v2FrameCreator, ID3HeaderVerifier
45
+ )
46
+ from audiometa.test.helpers.vorbis import (
47
+ VorbisMetadataSetter, VorbisMetadataDeleter, VorbisHeaderVerifier, VorbisMetadataVerifier
48
+ )
49
+ from audiometa.test.helpers.riff import RIFFMetadataVerifier, RIFFHeaderVerifier
50
+ from audiometa.test.helpers.common import AudioFileCreator, ComprehensiveMetadataVerifier, run_script
51
+ """
@@ -0,0 +1,6 @@
1
+ """Common utilities for metadata testing."""
2
+
3
+ from .audio_file_creator import AudioFileCreator
4
+ from .external_tool_runner import run_external_tool, run_script
5
+
6
+ __all__ = ["AudioFileCreator", "run_external_tool", "run_script"]
@@ -0,0 +1,68 @@
1
+ """Audio file creator utilities for testing."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+
8
+ class AudioFileCreator:
9
+ """Utilities for creating minimal audio files for testing."""
10
+
11
+ @staticmethod
12
+ def create_minimal_audio_file(file_path: Path, format_type: str, assets_dir: Path) -> None:
13
+ """Create a minimal audio file for testing.
14
+
15
+ Args:
16
+ file_path: Path where to create the file
17
+ format_type: Audio format ('mp3', 'flac', 'wav')
18
+ assets_dir: Directory containing template files
19
+ """
20
+ if format_type.lower() in ["mp3", "id3v1", "id3v2.3", "id3v2.4"]:
21
+ template_file = assets_dir / "metadata=none.mp3"
22
+ elif format_type.lower() == "flac":
23
+ template_file = assets_dir / "metadata=none.flac"
24
+ elif format_type.lower() == "wav":
25
+ template_file = assets_dir / "metadata=none.wav"
26
+ else:
27
+ msg = f"Unsupported format type: {format_type}"
28
+ raise ValueError(msg)
29
+
30
+ if template_file.exists():
31
+ # Copy from template
32
+ shutil.copy2(template_file, file_path)
33
+ else:
34
+ # Fallback: create a minimal file using ffmpeg if available
35
+ try:
36
+ # For id3v1, id3v2.3, id3v2.4, use mp3 format for ffmpeg
37
+ actual_format = "mp3" if format_type.lower() in ["id3v1", "id3v2.3", "id3v2.4"] else format_type.lower()
38
+ AudioFileCreator._create_minimal_audio_with_ffmpeg(file_path, actual_format)
39
+ except (subprocess.CalledProcessError, FileNotFoundError):
40
+ # Last resort: copy from any available sample file
41
+ search_format = "mp3" if format_type.lower() in ["id3v1", "id3v2.3", "id3v2.4"] else format_type.lower()
42
+ sample_files = list(assets_dir.glob(f"*.{search_format}"))
43
+ if sample_files:
44
+ shutil.copy2(sample_files[0], file_path)
45
+ else:
46
+ msg = f"No template file found for {format_type}"
47
+ raise RuntimeError(msg) from None
48
+
49
+ @staticmethod
50
+ def _create_minimal_audio_with_ffmpeg(file_path: Path, format_type: str) -> None:
51
+ """Create a minimal audio file using ffmpeg."""
52
+ # Create 1 second of silence
53
+ cmd = [
54
+ "ffmpeg",
55
+ "-f",
56
+ "lavfi",
57
+ "-i",
58
+ "anullsrc=duration=1",
59
+ ]
60
+
61
+ # For WAV, don't specify -acodec as ffmpeg will use the default PCM codec
62
+ # For other formats, specify the codec
63
+ if format_type.lower() != "wav":
64
+ cmd.extend(["-acodec", format_type.lower()])
65
+
66
+ cmd.extend(["-y", str(file_path)])
67
+
68
+ subprocess.run(cmd, check=True, capture_output=True)
@@ -0,0 +1,74 @@
1
+ """Unified utility for running external tools and scripts with consistent error handling."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+
7
+ class ExternalMetadataToolError(Exception):
8
+ """Exception raised when external metadata tools fail."""
9
+
10
+
11
+ def run_external_tool(
12
+ command: list[str], tool_name: str = "external tool", check: bool = True, input_data: str | bytes | None = None
13
+ ) -> subprocess.CompletedProcess:
14
+ """Run an external tool command with proper error handling.
15
+
16
+ Args:
17
+ command: List of command and arguments to execute
18
+ tool_name: Name of the tool for error messages (e.g., "metaflac", "mid3v2")
19
+ check: Whether to raise exception on non-zero exit code
20
+ input_data: Optional input to pass to stdin
21
+
22
+ Returns:
23
+ subprocess.CompletedProcess: The result of the command execution
24
+
25
+ Raises:
26
+ ExternalMetadataToolError: If the command fails or tool is not found
27
+ """
28
+ try:
29
+ text = not isinstance(input_data, bytes) if input_data is not None else True
30
+ return subprocess.run(command, capture_output=True, text=text, check=check, input=input_data)
31
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
32
+ msg = f"{tool_name} failed: {e}"
33
+ raise ExternalMetadataToolError(msg) from e
34
+
35
+
36
+ def run_script(
37
+ script_path: str | Path, target_file: str | Path, scripts_dir: str | Path | None = None
38
+ ) -> subprocess.CompletedProcess:
39
+ """Run a shell script with proper error handling and permissions.
40
+
41
+ Convenience wrapper around run_external_tool for script execution.
42
+
43
+ Args:
44
+ script_path: Path to the script file, or script name if scripts_dir is provided
45
+ target_file: File to pass as argument to the script
46
+ scripts_dir: Directory containing scripts (optional, if script_path is relative)
47
+
48
+ Returns:
49
+ subprocess.CompletedProcess: The result of the script execution
50
+
51
+ Raises:
52
+ FileNotFoundError: If the script file doesn't exist
53
+ ExternalMetadataToolError: If the script execution fails
54
+ """
55
+ # Resolve script path
56
+ full_script_path = Path(scripts_dir) / script_path if scripts_dir is not None else Path(script_path)
57
+
58
+ # Validate script exists
59
+ if not full_script_path.exists():
60
+ msg = f"Script not found: {full_script_path}"
61
+ raise FileNotFoundError(msg)
62
+
63
+ if not full_script_path.is_file():
64
+ msg = f"Script is not a file: {full_script_path}"
65
+ raise FileNotFoundError(msg)
66
+
67
+ # Make script executable
68
+ full_script_path.chmod(0o755)
69
+
70
+ # Run script using the unified external tool runner
71
+ script_name = full_script_path.name
72
+ command = [str(full_script_path), str(target_file)]
73
+
74
+ return run_external_tool(command, f"script {script_name}")
@@ -0,0 +1,8 @@
1
+ """ID3v1 metadata format helpers."""
2
+
3
+ from .id3v1_header_verifier import ID3v1HeaderVerifier
4
+ from .id3v1_metadata_deleter import ID3v1MetadataDeleter
5
+ from .id3v1_metadata_getter import ID3v1MetadataGetter
6
+ from .id3v1_metadata_setter import ID3v1MetadataSetter
7
+
8
+ __all__ = ["ID3v1MetadataDeleter", "ID3v1MetadataSetter", "ID3v1HeaderVerifier", "ID3v1MetadataGetter"]
@@ -0,0 +1,18 @@
1
+ """ID3v1 metadata header verification utilities."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class ID3v1HeaderVerifier:
7
+ """Utilities for verifying ID3v1 metadata headers in audio files."""
8
+
9
+ @staticmethod
10
+ def has_id3v1_header(file_path: Path) -> bool:
11
+ """Check if file has ID3v1 header by reading the last 128 bytes."""
12
+ try:
13
+ with file_path.open("rb") as f:
14
+ f.seek(-128, 2) # Seek to last 128 bytes
15
+ header = f.read(128)
16
+ return header[:3] == b"TAG"
17
+ except OSError:
18
+ return False
@@ -0,0 +1,37 @@
1
+ """ID3v1 metadata deletion operations."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ..common.external_tool_runner import ExternalMetadataToolError, run_external_tool
6
+
7
+
8
+ class ID3v1MetadataDeleter:
9
+ """Static utility class for ID3v1 metadata deletion using external tools."""
10
+
11
+ @staticmethod
12
+ def delete_tag(file_path: Path, tag_name: str) -> None:
13
+ try:
14
+ command = ["id3v2", "--id3v1-only", "--delete", tag_name, str(file_path)]
15
+ run_external_tool(command, "id3v2")
16
+ except ExternalMetadataToolError:
17
+ pass
18
+
19
+ @staticmethod
20
+ def delete_comment(file_path: Path) -> None:
21
+ ID3v1MetadataDeleter.delete_tag(file_path, "COMM")
22
+
23
+ @staticmethod
24
+ def delete_title(file_path: Path) -> None:
25
+ ID3v1MetadataDeleter.delete_tag(file_path, "TIT2")
26
+
27
+ @staticmethod
28
+ def delete_artist(file_path: Path) -> None:
29
+ ID3v1MetadataDeleter.delete_tag(file_path, "TPE1")
30
+
31
+ @staticmethod
32
+ def delete_album(file_path: Path) -> None:
33
+ ID3v1MetadataDeleter.delete_tag(file_path, "TALB")
34
+
35
+ @staticmethod
36
+ def delete_genre(file_path: Path) -> None:
37
+ ID3v1MetadataDeleter.delete_tag(file_path, "TCON")
@@ -0,0 +1,61 @@
1
+ """ID3v1 metadata inspection utilities for testing audio file metadata."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+
7
+ class ID3v1MetadataGetter:
8
+ """Utilities for inspecting ID3v1 metadata in audio files."""
9
+
10
+ @staticmethod
11
+ def get_raw_metadata(file_path: Path) -> dict[str, Any]:
12
+ """Return the raw metadata for a specific ID3v1 field."""
13
+ with file_path.open("rb") as f:
14
+ f.seek(-128, 2) # Seek to last 128 bytes (ID3v1 tag location)
15
+ data = f.read(128)
16
+
17
+ # Check for ID3v1 tag header
18
+ if not data.startswith(b"TAG"):
19
+ return {}
20
+
21
+ # Determine if it's ID3v1.1 (has track number)
22
+ is_id3v1_1 = data[125] != 0
23
+
24
+ # Parse fields based on ID3v1 specification
25
+ field_info = {
26
+ "title": (3, 33, 30), # bytes 3-32, 30 chars max
27
+ "artist": (33, 63, 30), # bytes 33-62, 30 chars max
28
+ "album": (63, 93, 30), # bytes 63-92, 30 chars max
29
+ "year": (93, 97, 4), # bytes 93-96, 4 chars max
30
+ "genre": (127, 128, 1), # byte 127
31
+ }
32
+
33
+ if is_id3v1_1:
34
+ field_info["comment"] = (97, 125, 28) # bytes 97-124, 28 chars max (ID3v1.1)
35
+ field_info["track"] = (125, 126, 1) # byte 125 (ID3v1.1)
36
+ else:
37
+ field_info["comment"] = (97, 127, 30) # bytes 97-126, 30 chars max (ID3v1)
38
+
39
+ metadata: dict[str, str | int | None] = {}
40
+ for field, (start, end, _max_chars) in field_info.items():
41
+ raw_bytes = data[start:end]
42
+ if field in ["title", "artist", "album", "year", "comment"]:
43
+ metadata[field] = raw_bytes.decode("latin-1").rstrip("\x00")
44
+ elif field == "track":
45
+ metadata[field] = raw_bytes[0] if raw_bytes and raw_bytes[0] != 0 else None
46
+ elif field == "genre":
47
+ metadata[field] = raw_bytes[0] if raw_bytes else 0
48
+
49
+ # Handle ID3v1 track number stored in comment field (non-standard but common)
50
+ if "track" not in metadata or metadata["track"] is None:
51
+ comment = metadata.get("comment", "")
52
+ if isinstance(comment, str) and len(comment) == 30 and comment[-1] != "\x00":
53
+ metadata["track"] = ord(comment[-1])
54
+ metadata["comment"] = comment[:-1].rstrip("\x00")
55
+
56
+ return metadata
57
+
58
+ @staticmethod
59
+ def get_title(file_path):
60
+ metadata = ID3v1MetadataGetter.get_raw_metadata(file_path)
61
+ return metadata.get("title", "")