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,506 @@
1
+ """ID3v2 and 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 ID3v2MetadataSetter:
10
+ """Static utility class for ID3v2 metadata setting using external tools."""
11
+
12
+ @staticmethod
13
+ def set_metadata(file_path: Path, metadata: dict[str, Any], version: str = "2.4") -> None:
14
+ """Set MP3 metadata using appropriate ID3v2 tool based on version.
15
+
16
+ Args:
17
+ file_path: Path to the MP3 file
18
+ metadata: Dictionary of metadata to set
19
+ version: Optional ID3v2 version ("2.3" or "2.4"). Defaults to "2.4" if not specified.
20
+ """
21
+ if version == "2.3":
22
+ tool = "id3v2"
23
+ cmd = ["id3v2", "--id3v2-only"]
24
+ # Map common metadata keys to id3v2 tool arguments
25
+ key_mapping = {
26
+ "title": "--song",
27
+ "artist": "--artist",
28
+ "album": "--album",
29
+ "year": "--year",
30
+ "comment": "--comment",
31
+ "track": "--track",
32
+ "track_number": "--TRCK",
33
+ "bpm": "--TBPM",
34
+ "composer": "--TCOM",
35
+ "copyright": "--TCOP",
36
+ "lyrics": "--USLT",
37
+ "language": "--TLAN",
38
+ "rating": "--POPM",
39
+ "album_artist": "--TPE2",
40
+ "encoder": "--TENC",
41
+ "url": "--WOAR",
42
+ "isrc": "--TSRC",
43
+ "mood": "--TMOO",
44
+ "key": "--TKEY",
45
+ "publisher": "--TPUB",
46
+ "disc_number": "--TPOS",
47
+ }
48
+ else:
49
+ tool = "mid3v2"
50
+ cmd = ["mid3v2"]
51
+ # Map common metadata keys to mid3v2 tool arguments
52
+ key_mapping = {
53
+ "title": "--song",
54
+ "artist": "--artist",
55
+ "album": "--album",
56
+ "year": "--year",
57
+ "genre": "--genre",
58
+ "comment": "--comment",
59
+ "track": "--track",
60
+ "track_number": "--track",
61
+ "composer": "--TCOM",
62
+ "copyright": "--TCOP",
63
+ "lyrics": "--USLT",
64
+ "language": "--TLAN",
65
+ "rating": "--POPM",
66
+ "album_artist": "--TPE2",
67
+ "encoder": "--TENC",
68
+ "url": "--WOAR",
69
+ "isrc": "--TSRC",
70
+ "mood": "--TMOO",
71
+ "key": "--TKEY",
72
+ "publisher": "--TPUB",
73
+ "bpm": "--TBPM",
74
+ "disc_number": "--TPOS",
75
+ }
76
+
77
+ # Initialize metadata_added flag
78
+ metadata_added = False
79
+
80
+ # Handle special fields that need combined values
81
+ disc_number = None
82
+ disc_total = None
83
+ for key, value in metadata.items():
84
+ if key.lower() == "disc_number" and not isinstance(value, list):
85
+ disc_number = value
86
+ elif key.lower() == "disc_total" and not isinstance(value, list):
87
+ disc_total = value
88
+
89
+ # Handle disc_number/disc_total combination (TPOS frame)
90
+ if disc_number is not None or disc_total is not None:
91
+ if disc_number is not None and disc_total is not None:
92
+ tpos_value = f"{disc_number}/{disc_total}"
93
+ elif disc_number is not None:
94
+ tpos_value = str(disc_number)
95
+ else:
96
+ tpos_value = f"0/{disc_total}"
97
+ if version == "2.3":
98
+ cmd.extend(["--TPOS", tpos_value])
99
+ else:
100
+ cmd.extend(["--TPOS", tpos_value])
101
+ metadata_added = True
102
+
103
+ # Handle release_date (year) - include in main cmd instead of separate call
104
+ release_date = None
105
+ for key, value in metadata.items():
106
+ if key.lower() in ["release_date", "year"] and not isinstance(value, list):
107
+ release_date = str(value)
108
+ break
109
+
110
+ if release_date:
111
+ if version == "2.3":
112
+ # For ID3v2.3, use --year (TYER frame)
113
+ cmd.extend(["--year", release_date])
114
+ else:
115
+ # For ID3v2.4, use --TDRC
116
+ cmd.extend(["--TDRC", release_date])
117
+ metadata_added = True
118
+
119
+ # Handle non-list values (excluding already handled fields and list fields)
120
+ # Only exclude list fields if they're actually lists (single strings should be processed)
121
+ list_field_keys = set()
122
+ for k, v in metadata.items():
123
+ if isinstance(v, list):
124
+ list_field_keys.add(k.lower())
125
+
126
+ for key, value in metadata.items():
127
+ if (
128
+ key.lower() in key_mapping
129
+ and not isinstance(value, list)
130
+ and key.lower() not in ["disc_number", "disc_total", "release_date", "year"]
131
+ and key.lower() not in list_field_keys # Only exclude if it's actually a list
132
+ ):
133
+ # Special handling for rating (POPM frame requires app name prefix)
134
+ if key.lower() == "rating":
135
+ if version == "2.3":
136
+ cmd.extend(["--POPM", f"Windows Media Player 9 Series:{value}"])
137
+ else:
138
+ cmd.extend(["--POPM", f"Windows Media Player 9 Series:{value}"])
139
+ else:
140
+ cmd.extend([key_mapping[key.lower()], str(value)])
141
+ metadata_added = True
142
+
143
+ # Only run the tool if metadata was actually added
144
+ if metadata_added:
145
+ cmd.append(str(file_path))
146
+ run_external_tool(cmd, tool)
147
+
148
+ # Handle list values AFTER other metadata (to avoid being overwritten)
149
+ # Process in reverse order so the last one (composer) doesn't overwrite others
150
+ list_items = [(k, v) for k, v in metadata.items() if isinstance(v, list) and v]
151
+ # Reverse the list so we process composer last (it seems to work)
152
+ for key, value in reversed(list_items):
153
+ if key.lower() == "artist":
154
+ ID3v2MetadataSetter.set_artists(file_path, value, version=version)
155
+ elif key.lower() == "genre":
156
+ ID3v2MetadataSetter.set_genres(file_path, value, version=version)
157
+ elif key.lower() == "composer":
158
+ ID3v2MetadataSetter.set_composers(file_path, value, version=version)
159
+ elif key.lower() == "album_artist":
160
+ ID3v2MetadataSetter.set_album_artists(file_path, value)
161
+
162
+ @staticmethod
163
+ def set_max_metadata(file_path: Path) -> None:
164
+ from pathlib import Path
165
+
166
+ from ..common.external_tool_runner import run_script
167
+
168
+ scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
169
+ run_script("set-id3v2-max-metadata.sh", file_path, scripts_dir)
170
+
171
+ @staticmethod
172
+ def set_title(file_path: Path, title: str, version: str = "2.4") -> None:
173
+ if version == "2.3":
174
+ command = ["id3v2", "--id3v2-only", "--song", title, str(file_path)]
175
+ run_external_tool(command, "id3v2")
176
+ else:
177
+ command = ["mid3v2", "--song", title, str(file_path)]
178
+ run_external_tool(command, "mid3v2")
179
+
180
+ @staticmethod
181
+ def set_titles(file_path: Path, titles: list[str], in_separate_frames: bool = False, version: str = "2.4"):
182
+ """Set ID3v2 multiple titles using external mid3v2 tool or manual frame creation.
183
+
184
+ Args:
185
+ file_path: Path to the audio file
186
+ titles: List of title strings to set
187
+ in_separate_frames: If True, creates multiple separate TIT2 frames (one per title)
188
+ using manual binary construction. If False (default), creates a single TIT2 frame
189
+ with multiple values using mid3v2.
190
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
191
+ """
192
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TIT2", titles, in_separate_frames, version)
193
+
194
+ @staticmethod
195
+ def set_artists(file_path: Path, artists, in_separate_frames: bool = False, version: str = "2.4") -> None:
196
+ if isinstance(artists, str):
197
+ # For string input, use external tool directly to avoid replacing entire tag
198
+ if version == "2.3":
199
+ command = ["id3v2", "--id3v2-only", "--artist", artists, str(file_path)]
200
+ run_external_tool(command, "id3v2")
201
+ else:
202
+ # Use mid3v2 for ID3v2.4
203
+ command = ["mid3v2", "--artist", artists, str(file_path)]
204
+ run_external_tool(command, "mid3v2")
205
+ else:
206
+ # For list input, use multiple values handling
207
+ ID3v2MetadataSetter._set_multiple_metadata_values(
208
+ file_path, "TPE1", artists, in_separate_frames=in_separate_frames, version=version
209
+ )
210
+
211
+ @staticmethod
212
+ def set_album(file_path: Path, album: str, version: str = "2.4") -> None:
213
+ if version == "2.3":
214
+ command = ["id3v2", "--id3v2-only", "--album", album, str(file_path)]
215
+ run_external_tool(command, "id3v2")
216
+ else:
217
+ command = ["mid3v2", "--album", album, str(file_path)]
218
+ run_external_tool(command, "mid3v2")
219
+
220
+ @staticmethod
221
+ def set_genre(file_path: Path, genre: str, version: str = "2.4") -> None:
222
+ if version == "2.3":
223
+ command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
224
+ run_external_tool(command, "id3v2")
225
+ else:
226
+ command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
227
+ run_external_tool(command, "id3v2")
228
+
229
+ @staticmethod
230
+ def set_lyrics(file_path: Path, lyrics: str, version: str = "2.4") -> None:
231
+ if version == "2.3":
232
+ command = ["id3v2", "--id3v2-only", "--USLT", lyrics, str(file_path)]
233
+ run_external_tool(command, "id3v2")
234
+ else:
235
+ command = ["mid3v2", "--USLT", lyrics, str(file_path)]
236
+ run_external_tool(command, "mid3v2")
237
+
238
+ @staticmethod
239
+ def set_language(file_path: Path, language: str, version: str = "2.4") -> None:
240
+ if version == "2.3":
241
+ command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
242
+ run_external_tool(command, "id3v2")
243
+ else:
244
+ command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
245
+ run_external_tool(command, "id3v2")
246
+
247
+ @staticmethod
248
+ def set_bpm(file_path: Path, bpm: int, version: str = "2.4") -> None:
249
+ if version == "2.3":
250
+ command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
251
+ run_external_tool(command, "id3v2")
252
+ else:
253
+ command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
254
+ run_external_tool(command, "id3v2")
255
+
256
+ @staticmethod
257
+ def set_release_date(file_path: Path, date_str: str, version: str = "2.4") -> None:
258
+ """Set ID3v2 release date using version-specific frames.
259
+
260
+ For ID3v2.3: Uses TYER (year) and TDAT (MMDD) frames
261
+ For ID3v2.4: Uses TDRC frame with full date
262
+
263
+ Args:
264
+ file_path: Path to the audio file
265
+ date_str: Date string in YYYY-MM-DD format
266
+ version: ID3v2 version to use ("2.3" or "2.4")
267
+ """
268
+ if version == "2.3":
269
+ # Parse YYYY-MM-DD format
270
+ if len(date_str) >= 10:
271
+ year = date_str[:4]
272
+ month = date_str[5:7]
273
+ day = date_str[8:10]
274
+ # TDAT format is DDMM
275
+ date_ddmm = f"{day}{month}"
276
+
277
+ # Set TYER frame
278
+ command_year = ["id3v2", "--id3v2-only", "--TYER", year, str(file_path)]
279
+ run_external_tool(command_year, "id3v2")
280
+
281
+ # Set TDAT frame
282
+ command_date = ["id3v2", "--id3v2-only", "--TDAT", date_ddmm, str(file_path)]
283
+ run_external_tool(command_date, "id3v2")
284
+ else:
285
+ # Fallback for year-only dates
286
+ command = ["id3v2", "--id3v2-only", "--TYER", date_str, str(file_path)]
287
+ run_external_tool(command, "id3v2")
288
+ else:
289
+ # ID3v2.4: Use TDRC with full date
290
+ command = ["mid3v2", "--TDRC", date_str, str(file_path)]
291
+ run_external_tool(command, "mid3v2")
292
+
293
+ @staticmethod
294
+ def _set_multiple_values_single_frame(
295
+ file_path: Path, frame_id: str, values: list[str], version: str = "2.4", separator: str | None = None
296
+ ) -> None:
297
+ """Set multiple values in a single ID3v2 frame with version-specific handling.
298
+
299
+ Args:
300
+ file_path: Path to the audio file
301
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
302
+ values: List of values to set in the frame
303
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
304
+ separator: Separator to use between values. If None, uses default behavior
305
+ (semicolon for ID3v2.3, null byte for ID3v2.4)
306
+ """
307
+ # Determine separator if not provided
308
+ if separator is None:
309
+ separator = ";" if version == "2.3" else "\x00"
310
+
311
+ # Use appropriate method for all cases
312
+ ID3v2MetadataSetter._set_single_frame_with_id3v2(file_path, frame_id, values, version, separator)
313
+
314
+ @staticmethod
315
+ def _set_multiple_metadata_values(
316
+ file_path: Path,
317
+ frame_id: str,
318
+ values: list[str],
319
+ in_separate_frames: bool = False,
320
+ version: str = "2.4",
321
+ separator: str | None = None,
322
+ ) -> None:
323
+ """Set multiple metadata values, either in separate frames or a single frame.
324
+
325
+ Args:
326
+ file_path: Path to the audio file
327
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
328
+ values: List of values to set
329
+ in_separate_frames: If True, creates multiple separate frames. If False, creates a
330
+ single frame with multiple values.
331
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
332
+ separator: Separator to use between values when in_separate_frames=False. If None, uses default behavior.
333
+ """
334
+ from .id3v2_metadata_deleter import ID3v2MetadataDeleter
335
+
336
+ # Delete existing frames
337
+ ID3v2MetadataDeleter.delete_frame(file_path, frame_id)
338
+
339
+ if in_separate_frames:
340
+ # Use manual binary construction to create truly separate frames
341
+ ID3v2MetadataSetter._create_multiple_id3v2_frames(file_path, frame_id, values, version)
342
+ else:
343
+ # Create a single frame with multiple values (version-specific handling)
344
+ ID3v2MetadataSetter._set_multiple_values_single_frame(file_path, frame_id, values, version, separator)
345
+
346
+ @staticmethod
347
+ def set_genres(file_path: Path, genres: list[str], in_separate_frames: bool = False, version: str = "2.4"):
348
+ """Set ID3v2 multiple genres using external mid3v2 tool or manual frame creation.
349
+
350
+ Args:
351
+ file_path: Path to the audio file
352
+ genres: List of genre strings to set
353
+ in_separate_frames: If True, creates multiple separate TCON frames (one per genre)
354
+ using manual binary construction. If False (default), creates a single TCON frame
355
+ with multiple values using mid3v2.
356
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
357
+ """
358
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCON", genres, in_separate_frames, version)
359
+
360
+ @staticmethod
361
+ def set_album_artists(file_path: Path, album_artists: list[str], in_separate_frames: bool = False):
362
+ """Set ID3v2 multiple album artists using external mid3v2 tool or manual frame creation.
363
+
364
+ Args:
365
+ file_path: Path to the audio file
366
+ album_artists: List of album artist strings to set
367
+ in_separate_frames: If True, creates multiple separate TPE2 frames (one per album artist)
368
+ using manual binary construction. If False (default), creates a single TPE2 frame
369
+ with multiple values using mid3v2.
370
+ """
371
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TPE2", album_artists, in_separate_frames)
372
+
373
+ @staticmethod
374
+ def set_composers(file_path: Path, composers: list[str], in_separate_frames: bool = False, version: str = "2.4"):
375
+ """Set ID3v2 multiple composers using external mid3v2 tool or manual frame creation.
376
+
377
+ Args:
378
+ file_path: Path to the audio file
379
+ composers: List of composer strings to set
380
+ in_separate_frames: If True, creates multiple separate TCOM frames (one per composer)
381
+ using manual binary construction. If False (default), creates a single TCOM frame
382
+ with multiple values using mid3v2.
383
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
384
+ """
385
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCOM", composers, in_separate_frames, version)
386
+
387
+ @staticmethod
388
+ def set_comments(file_path: Path, comments: list[str], in_separate_frames: bool = False):
389
+ """Set ID3v2 multiple comments using external mid3v2 tool.
390
+
391
+ Args:
392
+ file_path: Path to the audio file
393
+ comments: List of comment strings to set
394
+ in_separate_frames: If True, creates multiple separate COMM frames (one per comment).
395
+ If False (default), creates a single COMM frame with the first comment value.
396
+ """
397
+ from .id3v2_metadata_deleter import ID3v2MetadataDeleter
398
+
399
+ # Delete existing COMM tags
400
+ ID3v2MetadataDeleter.delete_frame(file_path, "COMM")
401
+
402
+ if in_separate_frames:
403
+ # Set each comment in a separate id3v2 call to force multiple frames
404
+ for comment in comments:
405
+ command = ["id3v2", "--id3v2-only", "--comment", comment, str(file_path)]
406
+ run_external_tool(command, "id3v2")
407
+ # Set only the first comment (ID3v2 comment fields are typically single-valued)
408
+ elif comments:
409
+ command = ["id3v2", "--id3v2-only", "--comment", comments[0], str(file_path)]
410
+ run_external_tool(command, "id3v2")
411
+
412
+ @staticmethod
413
+ def _create_multiple_id3v2_frames(file_path: Path, frame_id: str, texts: list[str], version: str = "2.4") -> None:
414
+ """Create multiple separate ID3v2 frames using manual binary construction.
415
+
416
+ This uses the ManualID3v2FrameCreator to bypass standard tools that
417
+ consolidate multiple frames of the same type, allowing creation of
418
+ truly separate frames for testing purposes.
419
+
420
+ Args:
421
+ file_path: Path to the audio file
422
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TPE2', 'TCON', 'TCOM')
423
+ texts: List of text values, one per frame
424
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
425
+ """
426
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
427
+
428
+ # Create frames based on the frame type
429
+ if frame_id == "TPE1":
430
+ ManualID3v2FrameCreator.create_multiple_tpe1_frames(file_path, texts, version)
431
+ elif frame_id == "TPE2":
432
+ ManualID3v2FrameCreator.create_multiple_tpe2_frames(file_path, texts, version)
433
+ elif frame_id == "TCON":
434
+ ManualID3v2FrameCreator.create_multiple_tcon_frames(file_path, texts, version)
435
+ elif frame_id == "TCOM":
436
+ ManualID3v2FrameCreator.create_multiple_tcom_frames(file_path, texts, version)
437
+ else:
438
+ # Generic frame creation for other frame types (including TIT2)
439
+ creator = ManualID3v2FrameCreator()
440
+ frames = []
441
+ for text in texts:
442
+ frame_data = creator._create_text_frame(frame_id, text, version)
443
+ frames.append(frame_data)
444
+ creator._write_id3v2_tag(file_path, frames, version)
445
+
446
+ @staticmethod
447
+ def _set_single_frame_with_id3v2(
448
+ file_path: Path, frame_id: str, alist: list[str], version: str, separator: str | None = None
449
+ ) -> None:
450
+ """Internal helper: Create a single ID3v2 frame using appropriate tool based on version.
451
+
452
+ Args:
453
+ file_path: Path to the audio file
454
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
455
+ alist: List of text values for the frame
456
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
457
+ separator: Separator to use between values. If None, uses default.
458
+ """
459
+ # Determine separator if not provided
460
+ if separator is None:
461
+ separator = ";" if version == "2.3" else "\x00"
462
+
463
+ # Combine values with the appropriate separator
464
+ combined_text = separator.join(alist) if len(alist) > 1 else alist[0] if alist else ""
465
+
466
+ # Check if we have null bytes - if so, use manual frame creator for ID3v2.4
467
+ if version == "2.4" and "\x00" in combined_text:
468
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
469
+
470
+ creator = ManualID3v2FrameCreator()
471
+ frame_data = creator._create_text_frame(frame_id, combined_text, version)
472
+ creator._write_id3v2_tag(file_path, [frame_data], version)
473
+ return
474
+
475
+ # Map frame IDs to tool flags
476
+ flag_mapping = {
477
+ "TCON": "--genre",
478
+ "TIT2": "--song",
479
+ "TPE1": "--artist",
480
+ "TPE2": "--TPE2",
481
+ "TALB": "--album",
482
+ "TDRC": "--year",
483
+ "TRCK": "--track",
484
+ "COMM": "--comment",
485
+ "TCOM": "--TCOM",
486
+ }
487
+
488
+ flag = flag_mapping.get(frame_id, f"--{frame_id}")
489
+
490
+ if version == "2.3":
491
+ # Use id3v2 for ID3v2.3
492
+ command = ["id3v2", "--id3v2-only", flag, combined_text, str(file_path)]
493
+ run_external_tool(command, "id3v2")
494
+ else:
495
+ # Use mid3v2 for ID3v2.4
496
+ command = ["mid3v2", flag, combined_text, str(file_path)]
497
+ run_external_tool(command, "mid3v2")
498
+
499
+ @staticmethod
500
+ def write_tpe1_with_encoding(file_path: Path, text: str, encoding: int) -> None:
501
+ """Write a TPE1 frame with specific encoding for testing purposes."""
502
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
503
+
504
+ creator = ManualID3v2FrameCreator()
505
+ frame_data = creator._create_text_frame("TPE1", text, "2.4", encoding=encoding)
506
+ creator._write_id3v2_tag(file_path, [frame_data], "2.4")
@@ -0,0 +1,8 @@
1
+ """RIFF metadata format helpers."""
2
+
3
+ from .riff_header_verifier import RIFFHeaderVerifier
4
+ from .riff_metadata_deleter import RIFFMetadataDeleter
5
+ from .riff_metadata_getter import RIFFMetadataGetter
6
+ from .riff_metadata_setter import RIFFMetadataSetter
7
+
8
+ __all__ = ["RIFFMetadataGetter", "RIFFHeaderVerifier", "RIFFMetadataDeleter", "RIFFMetadataSetter"]
@@ -0,0 +1,85 @@
1
+ """RIFF metadata header verification utilities."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ..common.external_tool_runner import run_external_tool
6
+
7
+
8
+ class RIFFHeaderVerifier:
9
+ """Utilities for verifying RIFF metadata headers in audio files."""
10
+
11
+ @staticmethod
12
+ def has_riff_info_chunk(file_path: Path) -> bool:
13
+ """Check if file has RIFF INFO chunk by reading file structure."""
14
+ try:
15
+ with file_path.open("rb") as f:
16
+ # Read first few bytes to check for ID3v2 tags
17
+ first_bytes = f.read(10)
18
+ f.seek(0) # Reset to beginning
19
+
20
+ if first_bytes.startswith(b"ID3"):
21
+ # File has ID3v2 tags, find RIFF header after them
22
+ data = f.read()
23
+ pos = 0
24
+ while pos < len(data) - 8:
25
+ if data[pos : pos + 4] == b"RIFF":
26
+ # Found RIFF header, check for LIST chunk containing INFO
27
+ riff_size = int.from_bytes(data[pos + 4 : pos + 8], "little")
28
+ riff_data = data[pos + 8 : pos + 8 + riff_size]
29
+
30
+ # Search for LIST chunk containing INFO in RIFF data
31
+ # Skip the WAVE chunk header (4 bytes)
32
+ info_pos = 4
33
+ while info_pos < len(riff_data) - 8:
34
+ chunk_id = riff_data[info_pos : info_pos + 4]
35
+ chunk_size = int.from_bytes(riff_data[info_pos + 4 : info_pos + 8], "little")
36
+
37
+ if chunk_id == b"LIST":
38
+ # Check if this LIST chunk contains INFO
39
+ list_data = riff_data[info_pos + 8 : info_pos + 8 + chunk_size]
40
+ if len(list_data) >= 4 and list_data[:4] == b"INFO":
41
+ return True
42
+
43
+ # Move to next chunk (chunk size + padding)
44
+ info_pos += 8 + chunk_size
45
+ if chunk_size % 2 == 1: # Odd size needs padding
46
+ info_pos += 1
47
+ return False
48
+ pos += 1
49
+ return False
50
+ # File starts with RIFF header
51
+ riff_header = f.read(12)
52
+ if riff_header[:4] != b"RIFF":
53
+ return False
54
+
55
+ # Look for LIST chunk containing INFO
56
+ chunk_size = int.from_bytes(riff_header[4:8], "little")
57
+ data = f.read(chunk_size)
58
+
59
+ # Search for LIST chunk containing INFO
60
+ pos = 0
61
+ while pos < len(data) - 8:
62
+ chunk_id = data[pos : pos + 4]
63
+ chunk_size = int.from_bytes(data[pos + 4 : pos + 8], "little")
64
+
65
+ if chunk_id == b"LIST":
66
+ # Check if this LIST chunk contains INFO
67
+ list_data = data[pos + 8 : pos + 8 + chunk_size]
68
+ if len(list_data) >= 4 and list_data[:4] == b"INFO":
69
+ return True
70
+
71
+ # Move to next chunk (chunk size + padding)
72
+ pos += 8 + chunk_size
73
+ if chunk_size % 2 == 1: # Odd size needs padding
74
+ pos += 1
75
+
76
+ return False
77
+ except (OSError, ValueError):
78
+ return False
79
+
80
+ @staticmethod
81
+ def get_riff_metadata_info(file_path: Path) -> str:
82
+ """Get RIFF metadata info using exiftool."""
83
+ command = ["exiftool", "-a", "-G", str(file_path)]
84
+ result = run_external_tool(command, "exiftool")
85
+ return result.stdout