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,82 @@
1
+ """ID3v1 metadata setting operations."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from ..common.external_tool_runner import run_external_tool
7
+
8
+
9
+ class ID3v1MetadataSetter:
10
+ """Static utility class for ID3v1 metadata setting using external tools."""
11
+
12
+ @staticmethod
13
+ def set_genre(file_path: Path, genre_code: str) -> None:
14
+ """Set ID3v1 genre using external id3v2 tool."""
15
+ command = ["id3v2", "--id3v1-only", "--genre", genre_code, str(file_path)]
16
+ run_external_tool(command, "id3v2")
17
+
18
+ @staticmethod
19
+ def set_comment(file_path: Path, comment: str) -> None:
20
+ """Set ID3v1 comment using external id3v2 tool."""
21
+ command = ["id3v2", "--id3v1-only", "--comment", comment, str(file_path)]
22
+ run_external_tool(command, "id3v2")
23
+
24
+ @staticmethod
25
+ def set_title(file_path: Path, title: str) -> None:
26
+ """Set ID3v1 title using external id3v2 tool."""
27
+ command = ["id3v2", "--id3v1-only", "--song", title, str(file_path)]
28
+ run_external_tool(command, "id3v2")
29
+
30
+ @staticmethod
31
+ def set_artist(file_path: Path, artist: str) -> None:
32
+ """Set ID3v1 artist using external id3v2 tool."""
33
+ command = ["id3v2", "--id3v1-only", "--artist", artist, str(file_path)]
34
+ run_external_tool(command, "id3v2")
35
+
36
+ @staticmethod
37
+ def set_album(file_path: Path, album: str) -> None:
38
+ """Set ID3v1 album using external id3v2 tool."""
39
+ command = ["id3v2", "--id3v1-only", "--album", album, str(file_path)]
40
+ run_external_tool(command, "id3v2")
41
+
42
+ @staticmethod
43
+ def set_max_metadata(file_path: Path) -> None:
44
+ """Set maximum ID3v1 metadata using external script."""
45
+ from pathlib import Path
46
+
47
+ from ..common.external_tool_runner import run_script
48
+
49
+ scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
50
+ run_script("set-id3v1-max-metadata.sh", file_path, scripts_dir)
51
+
52
+ @staticmethod
53
+ def set_metadata(file_path: Path, metadata: dict[str, Any]) -> None:
54
+ """Set ID3v1 metadata using id3v2 tool (id3v2 can also set ID3v1 tags)."""
55
+ # Ensure ID3v1.1 format when track is set
56
+ metadata = metadata.copy()
57
+ if "track" in metadata and "comment" not in metadata:
58
+ metadata["comment"] = " " * 28 # Set comment to 28 spaces to enable ID3v1.1
59
+
60
+ cmd = ["id3v2", "--id3v1-only"]
61
+
62
+ # Map common metadata keys to id3v2 arguments for ID3v1
63
+ key_mapping = {
64
+ "title": "--song",
65
+ "artist": "--artist",
66
+ "album": "--album",
67
+ "year": "--year",
68
+ "genre": "--genre",
69
+ "comment": "--comment",
70
+ "track": "--track",
71
+ }
72
+
73
+ metadata_added = False
74
+ for key, value in metadata.items():
75
+ if key.lower() in key_mapping:
76
+ cmd.extend([key_mapping[key.lower()], str(value)])
77
+ metadata_added = True
78
+
79
+ # Only run id3v2 if metadata was actually added
80
+ if metadata_added:
81
+ cmd.append(str(file_path))
82
+ run_external_tool(cmd, "id3v2")
@@ -0,0 +1,28 @@
1
+ """ID3v2 metadata format helpers."""
2
+
3
+ # Core operations (following RIFF pattern)
4
+ # External tool wrappers
5
+ from ..common.external_tool_runner import ExternalMetadataToolError
6
+
7
+ # Advanced tools
8
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
9
+ from .id3v2_header_verifier import ID3v2HeaderVerifier
10
+ from .id3v2_metadata_deleter import ID3v2MetadataDeleter
11
+ from .id3v2_metadata_getter import ID3v2MetadataGetter
12
+ from .id3v2_metadata_setter import ID3v2MetadataSetter
13
+
14
+ # Specialized managers (moved to ID3v2MetadataSetter)
15
+
16
+
17
+ __all__ = [
18
+ # Core operations
19
+ "ID3v2HeaderVerifier",
20
+ "ID3v2MetadataDeleter",
21
+ "ID3v2MetadataSetter",
22
+ "ID3v2MetadataGetter",
23
+ # Specialized managers (moved to ID3v2MetadataSetter)
24
+ # Advanced tools
25
+ "ManualID3v2FrameCreator",
26
+ # External tool wrappers
27
+ "ExternalMetadataToolError",
28
+ ]
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python3
2
+ """Manual implementation to create multiple separate ID3v2 frames for testing.
3
+
4
+ This bypasses standard tools and libraries that automatically consolidate frames, allowing creation of test files with
5
+ truly separate TPE1, TPE2, TCON etc. frames.
6
+ """
7
+
8
+ import struct
9
+ import subprocess
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ from audiometa.utils.tool_path_resolver import get_tool_path
14
+
15
+
16
+ class ManualID3v2FrameCreator:
17
+ """Creates ID3v2 tags with multiple separate frames by manual binary construction."""
18
+
19
+ @staticmethod
20
+ def create_multiple_tpe1_frames(file_path: Path, artists: list[str], version: str = "2.4") -> None:
21
+ if version not in ["2.3", "2.4"]:
22
+ msg = "Version must be '2.3' or '2.4'"
23
+ raise ValueError(msg)
24
+ frames = []
25
+ for artist in artists:
26
+ frame_data = ManualID3v2FrameCreator._create_text_frame("TPE1", artist, version)
27
+ frames.append(frame_data)
28
+
29
+ ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
30
+
31
+ @staticmethod
32
+ def create_multiple_tpe2_frames(file_path: Path, album_artists: list[str], version: str = "2.4") -> None:
33
+ if version not in ["2.3", "2.4"]:
34
+ msg = "Version must be '2.3' or '2.4'"
35
+ raise ValueError(msg)
36
+ frames = []
37
+ for album_artist in album_artists:
38
+ frame_data = ManualID3v2FrameCreator._create_text_frame("TPE2", album_artist, version)
39
+ frames.append(frame_data)
40
+
41
+ ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
42
+
43
+ @staticmethod
44
+ def create_multiple_tcon_frames(file_path: Path, genres: list[str], version: str = "2.4") -> None:
45
+ if version not in ["2.3", "2.4"]:
46
+ msg = "Version must be '2.3' or '2.4'"
47
+ raise ValueError(msg)
48
+ frames = []
49
+ for genre in genres:
50
+ frame_data = ManualID3v2FrameCreator._create_text_frame("TCON", genre, version)
51
+ frames.append(frame_data)
52
+
53
+ ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
54
+
55
+ @staticmethod
56
+ def create_multiple_tcom_frames(file_path: Path, composers: list[str], version: str = "2.4") -> None:
57
+ if version not in ["2.3", "2.4"]:
58
+ msg = "Version must be '2.3' or '2.4'"
59
+ raise ValueError(msg)
60
+ frames = []
61
+ for composer in composers:
62
+ frame_data = ManualID3v2FrameCreator._create_text_frame("TCOM", composer, version)
63
+ frames.append(frame_data)
64
+
65
+ ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
66
+
67
+ @staticmethod
68
+ def create_mixed_multiple_frames(
69
+ file_path: Path, artists: list[str], genres: list[str], version: str = "2.4"
70
+ ) -> None:
71
+ if version not in ["2.3", "2.4"]:
72
+ msg = "Version must be '2.3' or '2.4'"
73
+ raise ValueError(msg)
74
+ frames = []
75
+
76
+ # Add multiple TPE1 frames
77
+ for artist in artists:
78
+ frame_data = ManualID3v2FrameCreator._create_text_frame("TPE1", artist, version)
79
+ frames.append(frame_data)
80
+
81
+ # Add multiple TCON frames
82
+ for genre in genres:
83
+ frame_data = ManualID3v2FrameCreator._create_text_frame("TCON", genre, version)
84
+ frames.append(frame_data)
85
+
86
+ ManualID3v2FrameCreator._write_id3v2_tag(file_path, frames, version)
87
+
88
+ @staticmethod
89
+ def _create_text_frame(frame_id: str, text: str, version: str, encoding: int | None = None) -> bytes:
90
+ """Create a single ID3v2 text frame with the given ID and text."""
91
+ # Choose encoding based on version or provided encoding
92
+ if encoding is not None:
93
+ enc = encoding
94
+ elif version == "2.3":
95
+ # ID3v2.3: Use ISO-8859-1
96
+ enc = 0
97
+ else: # ID3v2.4
98
+ # ID3v2.4: Use UTF-8
99
+ enc = 3
100
+
101
+ # Determine null terminator based on encoding
102
+ null_terminator = b"\x00\x00" if enc in (1, 2) else b"\x00"
103
+
104
+ # Encode text
105
+ if enc == 0: # ISO-8859-1
106
+ text_bytes = text.encode("latin1", errors="ignore")
107
+ elif enc == 1: # UTF-16 with BOM
108
+ text_bytes = text.encode("utf-16")
109
+ elif enc == 2: # UTF-16BE without BOM
110
+ text_bytes = text.encode("utf-16be")
111
+ elif enc == 3: # UTF-8
112
+ text_bytes = text.encode("utf-8")
113
+ else:
114
+ text_bytes = text.encode("latin1", errors="ignore")
115
+
116
+ # Frame data: encoding byte + text + null terminator
117
+ frame_data = struct.pack("B", enc) + text_bytes + null_terminator
118
+
119
+ # Frame header: ID (4 bytes) + size (4 bytes) + flags (2 bytes)
120
+ frame_id_bytes = frame_id.encode("ascii")
121
+ frame_size = len(frame_data)
122
+ frame_flags = 0x0000 # No flags
123
+
124
+ if version == "2.3":
125
+ frame_header = (
126
+ frame_id_bytes
127
+ + struct.pack(">I", frame_size) # Big-endian 32-bit size
128
+ + struct.pack(">H", frame_flags) # Big-endian 16-bit flags
129
+ )
130
+ else: # ID3v2.4
131
+ frame_header = (
132
+ frame_id_bytes
133
+ + ManualID3v2FrameCreator._synchsafe_int(frame_size) # Synchsafe size
134
+ + struct.pack(">H", frame_flags) # Big-endian 16-bit flags
135
+ )
136
+
137
+ return frame_header + frame_data
138
+
139
+ @staticmethod
140
+ def _synchsafe_int(value: int) -> bytes:
141
+ """Convert integer to ID3v2 synchsafe integer (7 bits per byte)."""
142
+ # Split into 7-bit chunks, most significant first
143
+ result: list[int] = []
144
+ for _i in range(4):
145
+ result.insert(0, value & 0x7F)
146
+ value >>= 7
147
+ return struct.pack("4B", *result)
148
+
149
+ @staticmethod
150
+ def _syncsafe_decode(data: bytes) -> int:
151
+ """Decode a 4-byte syncsafe integer."""
152
+ return ((data[0] & 0x7F) << 21) | ((data[1] & 0x7F) << 14) | ((data[2] & 0x7F) << 7) | (data[3] & 0x7F)
153
+
154
+ @staticmethod
155
+ def _write_id3v2_tag(file_path: Path, frames: list[bytes], version: str) -> None:
156
+ """Write ID3v2 tag with the given frames to the file, preserving existing frames."""
157
+
158
+ # Read existing file content
159
+ with file_path.open("rb") as f:
160
+ original_data = f.read()
161
+
162
+ # Extract existing frames and audio data
163
+ existing_frames: list[bytes] = []
164
+ audio_data = original_data
165
+ frame_ids_to_replace: set[str] = set()
166
+
167
+ # Extract frame IDs from new frames to know which ones to replace
168
+ for frame_bytes in frames:
169
+ if len(frame_bytes) >= 4:
170
+ frame_id = frame_bytes[:4].decode("ascii", errors="ignore")
171
+ frame_ids_to_replace.add(frame_id)
172
+
173
+ if original_data.startswith(b"ID3") and len(original_data) >= 10:
174
+ # Parse existing ID3v2 tag
175
+ existing_version = original_data[3]
176
+ size_bytes = original_data[6:10]
177
+
178
+ if existing_version == 4:
179
+ # ID3v2.4 uses synchsafe integers
180
+ existing_tag_size = 0
181
+ for byte in size_bytes:
182
+ existing_tag_size = (existing_tag_size << 7) | (byte & 0x7F)
183
+ else:
184
+ # ID3v2.3 and earlier use regular integers
185
+ existing_tag_size = struct.unpack(">I", size_bytes)[0]
186
+
187
+ # Read existing tag data
188
+ tag_data = original_data[10 : 10 + existing_tag_size]
189
+ audio_data = original_data[10 + existing_tag_size :]
190
+
191
+ # Parse existing frames, preserving those not being replaced
192
+ pos = 0
193
+ while pos < len(tag_data) - 10:
194
+ frame_id_bytes = tag_data[pos : pos + 4]
195
+ if frame_id_bytes == b"\x00\x00\x00\x00":
196
+ break
197
+
198
+ try:
199
+ frame_id_str = frame_id_bytes.decode("ascii")
200
+ except UnicodeDecodeError:
201
+ break
202
+
203
+ # Determine frame size based on version
204
+ if existing_version == 4:
205
+ frame_size = ManualID3v2FrameCreator._syncsafe_decode(tag_data[pos + 4 : pos + 8])
206
+ else:
207
+ frame_size = int.from_bytes(tag_data[pos + 4 : pos + 8], "big")
208
+
209
+ if pos + 10 + frame_size > len(tag_data):
210
+ break
211
+
212
+ # Only preserve frames that aren't being replaced
213
+ if frame_id_str not in frame_ids_to_replace:
214
+ frame_data = tag_data[pos : pos + 10 + frame_size]
215
+ existing_frames.append(frame_data)
216
+
217
+ pos += 10 + frame_size
218
+
219
+ # Combine existing frames (that aren't being replaced) with new frames
220
+ all_frames = existing_frames + frames
221
+ frames_data = b"".join(all_frames)
222
+ tag_size = len(frames_data)
223
+
224
+ # Create header based on version
225
+ if version == "2.3":
226
+ # ID3v2.3 header: "ID3" + version + flags + size (regular integer)
227
+ header = (
228
+ b"ID3" # ID3 identifier
229
+ + struct.pack("BB", 3, 0) # Version 2.3.0
230
+ + struct.pack("B", 0) # Flags (no unsynchronisation, etc.)
231
+ + struct.pack(">I", tag_size) # Size as regular 32-bit integer
232
+ )
233
+ else: # ID3v2.4
234
+ # ID3v2.4 header: "ID3" + version + flags + size (synchsafe integer)
235
+ header = (
236
+ b"ID3" # ID3 identifier
237
+ + struct.pack("BB", 4, 0) # Version 2.4.0
238
+ + struct.pack("B", 0) # Flags (no unsynchronisation, etc.)
239
+ + ManualID3v2FrameCreator._synchsafe_int(tag_size) # Size as synchsafe integer
240
+ )
241
+
242
+ # Write new file with merged ID3v2 tag
243
+ with file_path.open("wb") as f:
244
+ f.write(header)
245
+ f.write(frames_data)
246
+ f.write(audio_data)
247
+
248
+
249
+ def manual_multiple_frames_test():
250
+ """Test the manual frame creation for both ID3v2.3 and ID3v2.4."""
251
+
252
+ def run_test_for_version(version: str):
253
+ """Test a specific ID3v2 version."""
254
+
255
+ # Create a temporary MP3 file
256
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp:
257
+ tmp_path = Path(tmp.name)
258
+
259
+ try:
260
+ # Create minimal MP3 file
261
+ subprocess.run(
262
+ ["ffmpeg", "-f", "lavfi", "-i", "anullsrc=duration=1", "-acodec", "mp3", "-y", str(tmp_path)],
263
+ check=True,
264
+ capture_output=True,
265
+ )
266
+
267
+ # Test 1: Multiple TPE1 frames
268
+ artists = ["Artist One", "Artist Two", "Artist Three"]
269
+ ManualID3v2FrameCreator.create_multiple_tpe1_frames(tmp_path, artists, version)
270
+
271
+ # Check result with mid3v2
272
+ result = subprocess.run(
273
+ [get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
274
+ )
275
+
276
+ # Count TPE1 occurrences
277
+ tpe1_count = result.stdout.count("TPE1=")
278
+
279
+ if tpe1_count > 1:
280
+ pass
281
+ else:
282
+ pass
283
+
284
+ # Test 2: Multiple TCON frames
285
+ genres = ["Rock", "Pop", "Alternative"]
286
+ ManualID3v2FrameCreator.create_multiple_tcon_frames(tmp_path, genres, version)
287
+
288
+ result = subprocess.run(
289
+ [get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
290
+ )
291
+
292
+ tcon_count = result.stdout.count("TCON=")
293
+
294
+ # Test 3: Mixed multiple frames
295
+ ManualID3v2FrameCreator.create_mixed_multiple_frames(
296
+ tmp_path, artists=["Artist A", "Artist B"], genres=["Genre X", "Genre Y"], version=version
297
+ )
298
+
299
+ result = subprocess.run(
300
+ [get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
301
+ )
302
+
303
+ # Check version in the file and verify multiple frames exist in binary
304
+ result = subprocess.run(
305
+ [get_tool_path("mid3v2"), "-l", str(tmp_path)], capture_output=True, text=True, check=False
306
+ )
307
+ if result.stdout:
308
+ # Verify multiple frames exist by checking raw binary
309
+ with Path(tmp_path).open("rb") as f:
310
+ data = f.read(1000) # Read first 1KB to check for multiple frame IDs
311
+ tpe1_count = data.count(b"TPE1")
312
+ tcon_count = data.count(b"TCON")
313
+
314
+ if tpe1_count > 1 or tcon_count > 1:
315
+ pass
316
+ else:
317
+ pass
318
+
319
+ finally:
320
+ if tmp_path.exists():
321
+ tmp_path.unlink()
322
+
323
+ # Test both versions
324
+ run_test_for_version("2.3")
325
+ run_test_for_version("2.4")
326
+
327
+
328
+ def create_test_file_with_version(
329
+ output_path: Path, version: str = "2.4", artists: list[str] | None = None, genres: list[str] | None = None
330
+ ) -> None:
331
+ """Create a test MP3 file with multiple frames in the specified ID3v2 version."""
332
+ if artists is None:
333
+ artists = ["Artist One", "Artist Two", "Artist Three"]
334
+ if genres is None:
335
+ genres = ["Rock", "Pop", "Alternative"]
336
+
337
+ # Create minimal MP3 file
338
+ subprocess.run(
339
+ ["ffmpeg", "-f", "lavfi", "-i", "anullsrc=duration=1", "-acodec", "mp3", "-y", str(output_path)],
340
+ check=True,
341
+ capture_output=True,
342
+ )
343
+
344
+ # Add multiple frames
345
+ ManualID3v2FrameCreator.create_mixed_multiple_frames(output_path, artists, genres, version)
346
+
347
+
348
+ if __name__ == "__main__":
349
+ manual_multiple_frames_test()
@@ -0,0 +1,38 @@
1
+ """ID3 metadata header verification utilities."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class ID3v2HeaderVerifier:
7
+ """Utilities for verifying ID3 metadata headers in audio files."""
8
+
9
+ @staticmethod
10
+ def has_id3v2_header(file_path: Path) -> bool:
11
+ """Check if file has ID3v2 header by reading the first 10 bytes."""
12
+ try:
13
+ with file_path.open("rb") as f:
14
+ header = f.read(10)
15
+ return header[:3] == b"ID3"
16
+ except OSError:
17
+ return False
18
+
19
+ @staticmethod
20
+ def get_id3v2_version(file_path: Path) -> tuple[int, int, int] | None:
21
+ """Get the ID3v2 version of the file.
22
+
23
+ Args:
24
+ file_path: Path to the audio file
25
+
26
+ Returns:
27
+ Version tuple (major, minor, revision) or None if no ID3v2 header found
28
+ """
29
+ try:
30
+ from mutagen.id3 import ID3, ID3NoHeaderError
31
+
32
+ id3_tags = ID3(file_path)
33
+ except ID3NoHeaderError:
34
+ return None
35
+ except Exception:
36
+ return None
37
+ else:
38
+ return id3_tags.version
@@ -0,0 +1,56 @@
1
+ """ID3v2 metadata deletion operations."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ..common.external_tool_runner import run_external_tool
6
+
7
+
8
+ class ID3v2MetadataDeleter:
9
+ """Static utility class for ID3v2 metadata deletion using external tools."""
10
+
11
+ @staticmethod
12
+ def delete_frame(file_path: Path, frame_id: str) -> None:
13
+ """Delete a specific ID3v2 frame."""
14
+ try:
15
+ command = ["mid3v2", "--delete-frames", frame_id, str(file_path)]
16
+ run_external_tool(command, "mid3v2")
17
+ except Exception:
18
+ # Ignore if frame doesn't exist
19
+ pass
20
+
21
+ @staticmethod
22
+ def delete_comment(file_path: Path) -> None:
23
+ ID3v2MetadataDeleter.delete_frame(file_path, "COMM")
24
+
25
+ @staticmethod
26
+ def delete_title(file_path: Path) -> None:
27
+ ID3v2MetadataDeleter.delete_frame(file_path, "TIT2")
28
+
29
+ @staticmethod
30
+ def delete_artist(file_path: Path) -> None:
31
+ ID3v2MetadataDeleter.delete_frame(file_path, "TPE1")
32
+
33
+ @staticmethod
34
+ def delete_album(file_path: Path) -> None:
35
+ ID3v2MetadataDeleter.delete_frame(file_path, "TALB")
36
+
37
+ @staticmethod
38
+ def delete_genre(file_path: Path) -> None:
39
+ ID3v2MetadataDeleter.delete_frame(file_path, "TCON")
40
+
41
+ @staticmethod
42
+ def delete_lyrics(file_path: Path) -> None:
43
+ ID3v2MetadataDeleter.delete_frame(file_path, "USLT")
44
+
45
+ @staticmethod
46
+ def delete_language(file_path: Path) -> None:
47
+ ID3v2MetadataDeleter.delete_frame(file_path, "TLAN")
48
+
49
+ @staticmethod
50
+ def delete_bpm(file_path: Path) -> None:
51
+ ID3v2MetadataDeleter.delete_frame(file_path, "TBPM")
52
+
53
+ @staticmethod
54
+ def delete_tag(file_path: Path, tag_name: str) -> None:
55
+ command = ["id3v2", "--id3v2-only", "--delete", tag_name, str(file_path)]
56
+ run_external_tool(command, "id3v2")