audiometa-python 0.2.2__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 (318) hide show
  1. audiometa/__init__.py +1240 -0
  2. audiometa/__main__.py +6 -0
  3. audiometa/_audio_file.py +602 -0
  4. audiometa/cli.py +347 -0
  5. audiometa/exceptions.py +167 -0
  6. audiometa/manager/_MetadataManager.py +665 -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 +945 -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 +778 -0
  14. audiometa/manager/_rating_supporting/riff/__init__.py +25 -0
  15. audiometa/manager/_rating_supporting/riff/_riff_constants.py +10 -0
  16. audiometa/manager/_rating_supporting/vorbis/_VorbisManager.py +525 -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 +305 -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 +188 -0
  40. audiometa/test/helpers/id3v2/id3v2_metadata_setter.py +428 -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 +216 -0
  44. audiometa/test/helpers/riff/riff_metadata_deleter.py +56 -0
  45. audiometa/test/helpers/riff/riff_metadata_getter.py +118 -0
  46. audiometa/test/helpers/riff/riff_metadata_setter.py +196 -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 +200 -0
  55. audiometa/test/tests/__init__.py +0 -0
  56. audiometa/test/tests/conftest.py +261 -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/test_cli_delete.py +20 -0
  69. audiometa/test/tests/e2e/cli/test_cli_formatting.py +31 -0
  70. audiometa/test/tests/e2e/cli/test_cli_help.py +40 -0
  71. audiometa/test/tests/e2e/cli/test_cli_read.py +79 -0
  72. audiometa/test/tests/e2e/cli/test_cli_unified.py +33 -0
  73. audiometa/test/tests/e2e/cli/test_cli_write.py +116 -0
  74. audiometa/test/tests/e2e/scenarios/__init__.py +0 -0
  75. audiometa/test/tests/e2e/scenarios/test_user_scenarios.py +166 -0
  76. audiometa/test/tests/e2e/workflows/__init__.py +0 -0
  77. audiometa/test/tests/e2e/workflows/test_core_workflows.py +166 -0
  78. audiometa/test/tests/e2e/workflows/test_deletion_workflows.py +318 -0
  79. audiometa/test/tests/e2e/workflows/test_error_handling_workflows.py +165 -0
  80. audiometa/test/tests/e2e/workflows/test_format_specific_workflows.py +129 -0
  81. audiometa/test/tests/e2e/workflows/test_rating_workflows.py +124 -0
  82. audiometa/test/tests/integration/__init__.py +0 -0
  83. audiometa/test/tests/integration/audio_format/__init__.py +0 -0
  84. audiometa/test/tests/integration/audio_format/flac/__init__.py +0 -0
  85. audiometa/test/tests/integration/audio_format/flac/test_flac_delete_all.py +108 -0
  86. audiometa/test/tests/integration/audio_format/flac/test_flac_reading_all.py +61 -0
  87. audiometa/test/tests/integration/audio_format/flac/test_flac_reading_field.py +65 -0
  88. audiometa/test/tests/integration/audio_format/flac/test_flac_writing.py +69 -0
  89. audiometa/test/tests/integration/audio_format/mp3/__init__.py +0 -0
  90. audiometa/test/tests/integration/audio_format/mp3/test_mp3_delete_all.py +79 -0
  91. audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_all.py +61 -0
  92. audiometa/test/tests/integration/audio_format/mp3/test_mp3_reading_field.py +67 -0
  93. audiometa/test/tests/integration/audio_format/mp3/test_mp3_writing.py +60 -0
  94. audiometa/test/tests/integration/audio_format/wav/__init__.py +0 -0
  95. audiometa/test/tests/integration/audio_format/wav/test_wav_delete_all.py +87 -0
  96. audiometa/test/tests/integration/audio_format/wav/test_wav_reading_all.py +62 -0
  97. audiometa/test/tests/integration/audio_format/wav/test_wav_reading_field.py +57 -0
  98. audiometa/test/tests/integration/audio_format/wav/test_wav_with_id3v2_tags.py +83 -0
  99. audiometa/test/tests/integration/audio_format/wav/test_wav_writing.py +62 -0
  100. audiometa/test/tests/integration/conftest.py +29 -0
  101. audiometa/test/tests/integration/delete_all_metadata/__init__.py +1 -0
  102. audiometa/test/tests/integration/delete_all_metadata/test_audio_format_all.py +102 -0
  103. audiometa/test/tests/integration/delete_all_metadata/test_audio_format_header_deletion.py +77 -0
  104. audiometa/test/tests/integration/delete_all_metadata/test_basic_functionality.py +47 -0
  105. audiometa/test/tests/integration/delete_all_metadata/test_error_handling.py +24 -0
  106. audiometa/test/tests/integration/encoding/__init__.py +1 -0
  107. audiometa/test/tests/integration/encoding/test_encoding.py +88 -0
  108. audiometa/test/tests/integration/encoding/test_special_characters_edge_cases.py +223 -0
  109. audiometa/test/tests/integration/get_full_metadata/__init__.py +0 -0
  110. audiometa/test/tests/integration/get_full_metadata/test_binary_data_filtering.py +250 -0
  111. audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata.py +297 -0
  112. audiometa/test/tests/integration/get_full_metadata/test_get_full_metadata_edge_cases.py +231 -0
  113. audiometa/test/tests/integration/get_full_metadata/test_options.py +207 -0
  114. audiometa/test/tests/integration/get_full_metadata/test_parsed_fields_keys.py +85 -0
  115. audiometa/test/tests/integration/metadata_field/__init__.py +0 -0
  116. audiometa/test/tests/integration/metadata_field/album/__init__.py +0 -0
  117. audiometa/test/tests/integration/metadata_field/album/test_deleting.py +73 -0
  118. audiometa/test/tests/integration/metadata_field/album/test_reading.py +36 -0
  119. audiometa/test/tests/integration/metadata_field/album/test_writing.py +50 -0
  120. audiometa/test/tests/integration/metadata_field/album_artists/__init__.py +0 -0
  121. audiometa/test/tests/integration/metadata_field/album_artists/test_deleting.py +83 -0
  122. audiometa/test/tests/integration/metadata_field/album_artists/test_reading.py +38 -0
  123. audiometa/test/tests/integration/metadata_field/album_artists/test_writing.py +52 -0
  124. audiometa/test/tests/integration/metadata_field/artists/__init__.py +0 -0
  125. audiometa/test/tests/integration/metadata_field/artists/test_deleting.py +68 -0
  126. audiometa/test/tests/integration/metadata_field/artists/test_reading.py +36 -0
  127. audiometa/test/tests/integration/metadata_field/artists/test_writing.py +46 -0
  128. audiometa/test/tests/integration/metadata_field/bpm/__init__.py +0 -0
  129. audiometa/test/tests/integration/metadata_field/bpm/test_deleting.py +75 -0
  130. audiometa/test/tests/integration/metadata_field/bpm/test_reading.py +32 -0
  131. audiometa/test/tests/integration/metadata_field/bpm/test_writing.py +56 -0
  132. audiometa/test/tests/integration/metadata_field/comment/__init__.py +0 -0
  133. audiometa/test/tests/integration/metadata_field/comment/test_deleting.py +68 -0
  134. audiometa/test/tests/integration/metadata_field/comment/test_reading.py +36 -0
  135. audiometa/test/tests/integration/metadata_field/comment/test_writing.py +49 -0
  136. audiometa/test/tests/integration/metadata_field/composer/__init__.py +0 -0
  137. audiometa/test/tests/integration/metadata_field/composer/test_deleting.py +75 -0
  138. audiometa/test/tests/integration/metadata_field/composer/test_reading.py +34 -0
  139. audiometa/test/tests/integration/metadata_field/composer/test_writing.py +41 -0
  140. audiometa/test/tests/integration/metadata_field/copyright/__init__.py +0 -0
  141. audiometa/test/tests/integration/metadata_field/copyright/test_deleting.py +81 -0
  142. audiometa/test/tests/integration/metadata_field/copyright/test_reading.py +35 -0
  143. audiometa/test/tests/integration/metadata_field/copyright/test_writing.py +41 -0
  144. audiometa/test/tests/integration/metadata_field/field_not_supported/__init__.py +0 -0
  145. audiometa/test/tests/integration/metadata_field/field_not_supported/test_deleting.py +56 -0
  146. audiometa/test/tests/integration/metadata_field/field_not_supported/test_reading.py +54 -0
  147. audiometa/test/tests/integration/metadata_field/field_not_supported/test_writing.py +61 -0
  148. audiometa/test/tests/integration/metadata_field/genre/__init__.py +0 -0
  149. audiometa/test/tests/integration/metadata_field/genre/reading/__init__.py +0 -0
  150. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/__init__.py +0 -0
  151. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v1_reading.py +65 -0
  152. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_id3v2_reading.py +25 -0
  153. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_riff_reading.py +58 -0
  154. audiometa/test/tests/integration/metadata_field/genre/reading/metadata_format/test_vorbis_reading.py +61 -0
  155. audiometa/test/tests/integration/metadata_field/genre/reading/test_smart_reading.py +191 -0
  156. audiometa/test/tests/integration/metadata_field/genre/test_deleting.py +62 -0
  157. audiometa/test/tests/integration/metadata_field/genre/test_writing.py +64 -0
  158. audiometa/test/tests/integration/metadata_field/language/__init__.py +0 -0
  159. audiometa/test/tests/integration/metadata_field/language/test_deleting.py +75 -0
  160. audiometa/test/tests/integration/metadata_field/language/test_reading.py +39 -0
  161. audiometa/test/tests/integration/metadata_field/language/test_writing.py +43 -0
  162. audiometa/test/tests/integration/metadata_field/lyrics/__init__.py +0 -0
  163. audiometa/test/tests/integration/metadata_field/lyrics/test_deleting.py +129 -0
  164. audiometa/test/tests/integration/metadata_field/lyrics/test_reading.py +57 -0
  165. audiometa/test/tests/integration/metadata_field/lyrics/test_writing.py +59 -0
  166. audiometa/test/tests/integration/metadata_field/publisher/__init__.py +0 -0
  167. audiometa/test/tests/integration/metadata_field/publisher/test_deleting.py +88 -0
  168. audiometa/test/tests/integration/metadata_field/publisher/test_reading.py +32 -0
  169. audiometa/test/tests/integration/metadata_field/publisher/test_writing.py +47 -0
  170. audiometa/test/tests/integration/metadata_field/rating/__init__.py +0 -0
  171. audiometa/test/tests/integration/metadata_field/rating/reading/__init__.py +0 -0
  172. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_100_proportional.py +81 -0
  173. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_non_proportional.py +33 -0
  174. audiometa/test/tests/integration/metadata_field/rating/reading/test_base_255_proportional.py +58 -0
  175. audiometa/test/tests/integration/metadata_field/rating/test_deleting.py +117 -0
  176. audiometa/test/tests/integration/metadata_field/rating/test_error_handling.py +137 -0
  177. audiometa/test/tests/integration/metadata_field/rating/writing/__init__.py +0 -0
  178. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/__init__.py +0 -0
  179. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_id3v2.py +77 -0
  180. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_riff.py +55 -0
  181. audiometa/test/tests/integration/metadata_field/rating/writing/metadata_format/test_vorbis.py +57 -0
  182. audiometa/test/tests/integration/metadata_field/rating/writing/test_comprehensive.py +192 -0
  183. audiometa/test/tests/integration/metadata_field/release_date/__init__.py +0 -0
  184. audiometa/test/tests/integration/metadata_field/release_date/test_deleting.py +74 -0
  185. audiometa/test/tests/integration/metadata_field/release_date/test_error_handling.py +82 -0
  186. audiometa/test/tests/integration/metadata_field/release_date/test_reading.py +59 -0
  187. audiometa/test/tests/integration/metadata_field/release_date/test_writing.py +49 -0
  188. audiometa/test/tests/integration/metadata_field/test_metadata_field_validation.py +134 -0
  189. audiometa/test/tests/integration/metadata_field/title/__init__.py +0 -0
  190. audiometa/test/tests/integration/metadata_field/title/test_deleting.py +73 -0
  191. audiometa/test/tests/integration/metadata_field/title/test_error_handling.py +47 -0
  192. audiometa/test/tests/integration/metadata_field/title/test_reading.py +36 -0
  193. audiometa/test/tests/integration/metadata_field/title/test_writing.py +64 -0
  194. audiometa/test/tests/integration/metadata_field/track_number/__init__.py +0 -0
  195. audiometa/test/tests/integration/metadata_field/track_number/reading/__init__.py +0 -0
  196. audiometa/test/tests/integration/metadata_field/track_number/reading/test_edge_cases.py +43 -0
  197. audiometa/test/tests/integration/metadata_field/track_number/reading/test_metadata_format.py +32 -0
  198. audiometa/test/tests/integration/metadata_field/track_number/test_deleting.py +59 -0
  199. audiometa/test/tests/integration/metadata_field/track_number/test_writing.py +73 -0
  200. audiometa/test/tests/integration/multiple_values/__init__.py +1 -0
  201. audiometa/test/tests/integration/multiple_values/reading/__init__.py +1 -0
  202. audiometa/test/tests/integration/multiple_values/reading/metadata_format/__init__.py +1 -0
  203. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v1.py +23 -0
  204. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_3.py +92 -0
  205. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_id3v2_4.py +216 -0
  206. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_riff.py +84 -0
  207. audiometa/test/tests/integration/multiple_values/reading/metadata_format/test_vorbis.py +169 -0
  208. audiometa/test/tests/integration/multiple_values/reading/test_performance_large_data.py +209 -0
  209. audiometa/test/tests/integration/multiple_values/reading/test_smart_parsing_scenarios.py +198 -0
  210. audiometa/test/tests/integration/multiple_values/reading/test_unicode_handling.py +24 -0
  211. audiometa/test/tests/integration/multiple_values/writing/__init__.py +1 -0
  212. audiometa/test/tests/integration/multiple_values/writing/metadata_format/__init__.py +1 -0
  213. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v1.py +62 -0
  214. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_3.py +36 -0
  215. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_id3v2_4.py +34 -0
  216. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_riff.py +32 -0
  217. audiometa/test/tests/integration/multiple_values/writing/metadata_format/test_vorbis.py +54 -0
  218. audiometa/test/tests/integration/multiple_values/writing/test_error_handling.py +42 -0
  219. audiometa/test/tests/integration/multiple_values/writing/test_large_values.py +98 -0
  220. audiometa/test/tests/integration/reading/__init__.py +1 -0
  221. audiometa/test/tests/integration/reading/test_read_multiple_metadata.py +80 -0
  222. audiometa/test/tests/integration/reading/test_reading_error_handling.py +36 -0
  223. audiometa/test/tests/integration/real_audio_files/__init__.py +0 -0
  224. audiometa/test/tests/integration/real_audio_files/test_reading.py +146 -0
  225. audiometa/test/tests/integration/real_audio_files/test_writing.py +198 -0
  226. audiometa/test/tests/integration/technical_info/__init__.py +0 -0
  227. audiometa/test/tests/integration/technical_info/flac_md5/__init__.py +0 -0
  228. audiometa/test/tests/integration/technical_info/flac_md5/conftest.py +103 -0
  229. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/__init__.py +0 -0
  230. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_audio_data_corruption.py +21 -0
  231. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_flipped_md5.py +29 -0
  232. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_non_flac_error.py +13 -0
  233. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_partial_md5.py +29 -0
  234. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_random_md5.py +29 -0
  235. audiometa/test/tests/integration/technical_info/flac_md5/test_invalid_md5/test_unset_md5.py +56 -0
  236. audiometa/test/tests/integration/technical_info/flac_md5/test_valid_md5.py +21 -0
  237. audiometa/test/tests/integration/technical_info/test_bitrate.py +38 -0
  238. audiometa/test/tests/integration/technical_info/test_channels.py +38 -0
  239. audiometa/test/tests/integration/technical_info/test_duration_in_sec.py +38 -0
  240. audiometa/test/tests/integration/technical_info/test_sample_rate.py +40 -0
  241. audiometa/test/tests/integration/test_audio_file.py +23 -0
  242. audiometa/test/tests/integration/test_audio_format_readable_after_update_all_metadata_formats.py +95 -0
  243. audiometa/test/tests/integration/writing/__init__.py +0 -0
  244. audiometa/test/tests/integration/writing/test_error_handling.py +44 -0
  245. audiometa/test/tests/integration/writing/test_forced_format.py +224 -0
  246. audiometa/test/tests/integration/writing/test_multiple_format_preservation.py +223 -0
  247. audiometa/test/tests/integration/writing/test_partial_update.py +36 -0
  248. audiometa/test/tests/integration/writing/writing_strategies/__init__.py +0 -0
  249. audiometa/test/tests/integration/writing/writing_strategies/test_cleanup_strategy.py +79 -0
  250. audiometa/test/tests/integration/writing/writing_strategies/test_preserve_strategy.py +76 -0
  251. audiometa/test/tests/integration/writing/writing_strategies/test_sync_strategy.py +215 -0
  252. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/__init__.py +0 -0
  253. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_fail_behavior.py +42 -0
  254. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_no_writing_on_failure.py +93 -0
  255. audiometa/test/tests/integration/writing/writing_strategies/unsupported_fields/test_strategy_specific.py +99 -0
  256. audiometa/test/tests/unit/__init__.py +0 -0
  257. audiometa/test/tests/unit/audio_file/__init__.py +0 -0
  258. audiometa/test/tests/unit/audio_file/technical_info/__init__.py +0 -0
  259. audiometa/test/tests/unit/audio_file/technical_info/test_bitrate.py +26 -0
  260. audiometa/test/tests/unit/audio_file/technical_info/test_channels.py +31 -0
  261. audiometa/test/tests/unit/audio_file/technical_info/test_duration_in_sec.py +38 -0
  262. audiometa/test/tests/unit/audio_file/technical_info/test_error_handling.py +130 -0
  263. audiometa/test/tests/unit/audio_file/technical_info/test_file_size.py +51 -0
  264. audiometa/test/tests/unit/audio_file/technical_info/test_format_name.py +28 -0
  265. audiometa/test/tests/unit/audio_file/technical_info/test_sample_rate.py +31 -0
  266. audiometa/test/tests/unit/audio_file/test_context_manager.py +30 -0
  267. audiometa/test/tests/unit/audio_file/test_file_validation.py +40 -0
  268. audiometa/test/tests/unit/audio_file/test_operations.py +20 -0
  269. audiometa/test/tests/unit/audio_file/test_path_handling.py +23 -0
  270. audiometa/test/tests/unit/cli/__init__.py +0 -0
  271. audiometa/test/tests/unit/cli/test_expand_file_patterns.py +234 -0
  272. audiometa/test/tests/unit/metadata_managers/__init__.py +0 -0
  273. audiometa/test/tests/unit/metadata_managers/conftest.py +142 -0
  274. audiometa/test/tests/unit/metadata_managers/header_info/__init__.py +0 -0
  275. audiometa/test/tests/unit/metadata_managers/header_info/test_id3v1.py +49 -0
  276. audiometa/test/tests/unit/metadata_managers/header_info/test_id3v2.py +66 -0
  277. audiometa/test/tests/unit/metadata_managers/header_info/test_riff.py +57 -0
  278. audiometa/test/tests/unit/metadata_managers/header_info/test_vorbis.py +53 -0
  279. audiometa/test/tests/unit/metadata_managers/metadata_field/__init__.py +0 -0
  280. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/__init__.py +0 -0
  281. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/__init__.py +0 -0
  282. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/reading/test_smart_parsing.py +186 -0
  283. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/__init__.py +0 -0
  284. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_separator_selection.py +142 -0
  285. audiometa/test/tests/unit/metadata_managers/metadata_field/multiple_values/writing/test_value_filtering.py +76 -0
  286. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/__init__.py +0 -0
  287. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/__init__.py +0 -0
  288. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_normalization.py +152 -0
  289. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/reading/test_profiles_values.py +23 -0
  290. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/test_rating_validation.py +77 -0
  291. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/__init__.py +0 -0
  292. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_validation.py +151 -0
  293. audiometa/test/tests/unit/metadata_managers/metadata_field/rating/writing/test_writing_profiles.py +61 -0
  294. audiometa/test/tests/unit/metadata_managers/metadata_field/test_date_format_validation.py +135 -0
  295. audiometa/test/tests/unit/metadata_managers/metadata_field/test_track_number_validation.py +46 -0
  296. audiometa/test/tests/unit/metadata_managers/metadata_field/test_type_validation_exception.py +22 -0
  297. audiometa/test/tests/unit/metadata_managers/metadata_field/test_validation.py +83 -0
  298. audiometa/test/tests/unit/metadata_managers/test_metadata_format_managers_write_and_read.py +74 -0
  299. audiometa/utils/__init__.py +1 -0
  300. audiometa/utils/id3v1_genre_code_map.py +205 -0
  301. audiometa/utils/metadata_format.py +31 -0
  302. audiometa/utils/metadata_writing_strategy.py +16 -0
  303. audiometa/utils/os_dependencies_checker/__init__.py +24 -0
  304. audiometa/utils/os_dependencies_checker/base.py +62 -0
  305. audiometa/utils/os_dependencies_checker/config.py +51 -0
  306. audiometa/utils/os_dependencies_checker/macos.py +236 -0
  307. audiometa/utils/os_dependencies_checker/ubuntu.py +95 -0
  308. audiometa/utils/os_dependencies_checker/windows.py +227 -0
  309. audiometa/utils/rating_profiles.py +110 -0
  310. audiometa/utils/tool_path_resolver.py +135 -0
  311. audiometa/utils/types.py +82 -0
  312. audiometa/utils/unified_metadata_key.py +81 -0
  313. audiometa_python-0.2.2.dist-info/METADATA +1464 -0
  314. audiometa_python-0.2.2.dist-info/RECORD +318 -0
  315. audiometa_python-0.2.2.dist-info/WHEEL +5 -0
  316. audiometa_python-0.2.2.dist-info/entry_points.txt +2 -0
  317. audiometa_python-0.2.2.dist-info/licenses/LICENSE +202 -0
  318. audiometa_python-0.2.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,188 @@
1
+ """id3v2_metadata_getter.py Helper for extracting ID3v2 metadata from audio files."""
2
+
3
+
4
+ class ID3v2MetadataGetter:
5
+ """Helper class to get ID3v2 metadata from audio files using manual parsing."""
6
+
7
+ @staticmethod
8
+ def _syncsafe_decode(data: bytes) -> int:
9
+ """Decode a 4-byte syncsafe integer."""
10
+ return ((data[0] & 0x7F) << 21) | ((data[1] & 0x7F) << 14) | ((data[2] & 0x7F) << 7) | (data[3] & 0x7F)
11
+
12
+ @staticmethod
13
+ def _decode_text(encoding: int, data: bytes) -> str:
14
+ """Decode text based on encoding."""
15
+ if encoding == 0: # ISO-8859-1
16
+ return data.decode("latin1", errors="ignore")
17
+ if encoding == 1: # UTF-16 with BOM
18
+ return data.decode("utf-16", errors="ignore")
19
+ if encoding == 2: # UTF-16BE without BOM
20
+ return data.decode("utf-16be", errors="ignore")
21
+ if encoding == 3: # UTF-8
22
+ return data.decode("utf-8", errors="ignore")
23
+ return data.decode("latin1", errors="ignore") # fallback
24
+
25
+ @staticmethod
26
+ def get_raw_metadata(file_path, version=None):
27
+ """Get the raw metadata from the audio file using manual ID3v2 parsing, returning a string with frame IDs and
28
+ values.
29
+
30
+ Args:
31
+ file_path: Path to the audio file.
32
+ version: The ID3v2 version to parse ("2.3" for ID3v2.3, "2.4" for ID3v2.4). Defaults to "2.4".
33
+
34
+ Returns:
35
+ String with metadata in 'frame_id=value' format, one per line, or error string if parsing fails.
36
+ """
37
+ try:
38
+ with file_path.open("rb") as f:
39
+ # Read ID3v2 header (10 bytes)
40
+ header = f.read(10)
41
+ if len(header) < 10 or header[:3] != b"ID3":
42
+ return "No ID3v2 tag found"
43
+
44
+ # Parse header
45
+ file_version = (header[3], header[4])
46
+ if version is None:
47
+ version = "2.4" # Default to 2.4
48
+ if version == "2.3":
49
+ expected_major = 3
50
+ expected_minor = 0
51
+ elif version == "2.4":
52
+ expected_major = 4
53
+ expected_minor = 0
54
+ else:
55
+ msg = "Version must be '2.3' or '2.4'"
56
+ raise ValueError(msg)
57
+ if file_version[0] != expected_major or file_version[1] != expected_minor:
58
+ return None
59
+ header[5]
60
+ tag_size = ID3v2MetadataGetter._syncsafe_decode(header[6:10])
61
+
62
+ # Read tag data
63
+ tag_data = f.read(tag_size)
64
+ if len(tag_data) != tag_size:
65
+ return "Incomplete ID3v2 tag"
66
+
67
+ metadata: dict[str, list[str]] = {}
68
+ pos = 0
69
+ while pos < len(tag_data) - 10:
70
+ # Parse frame header (10 bytes)
71
+ frame_id = tag_data[pos : pos + 4]
72
+ if frame_id == b"\x00\x00\x00\x00":
73
+ break # Padding or end
74
+ try:
75
+ frame_id_str = frame_id.decode("ascii")
76
+ except UnicodeDecodeError:
77
+ break
78
+
79
+ if expected_minor == 4:
80
+ frame_size = ID3v2MetadataGetter._syncsafe_decode(tag_data[pos + 4 : pos + 8])
81
+ else:
82
+ frame_size = int.from_bytes(tag_data[pos + 4 : pos + 8], "big")
83
+
84
+ if pos + 10 + frame_size > len(tag_data):
85
+ break
86
+
87
+ frame_data = tag_data[pos + 10 : pos + 10 + frame_size]
88
+
89
+ # Parse text frames (those starting with encoding byte)
90
+ if frame_data and len(frame_data) > 1:
91
+ encoding = frame_data[0]
92
+ text_data = frame_data[1:]
93
+ # Decode the entire text_data first
94
+ decoded_text = ID3v2MetadataGetter._decode_text(encoding, text_data).rstrip("\x00")
95
+ if frame_id_str == "USLT":
96
+ # Parse USLT: language (3 bytes) + descriptor (null-terminated) + lyrics (null-terminated)
97
+ text_data_bytes = text_data
98
+ if len(text_data_bytes) > 3:
99
+ language = text_data_bytes[:3].decode("ascii", errors="ignore")
100
+ uslt_pos = 3 # after language
101
+ while uslt_pos < len(text_data_bytes) and text_data_bytes[uslt_pos] != 0:
102
+ uslt_pos += 1
103
+ uslt_pos += 1 # skip null
104
+ lyrics_bytes = text_data_bytes[uslt_pos:].rstrip(b"\x00")
105
+ lyrics = ID3v2MetadataGetter._decode_text(encoding, lyrics_bytes)
106
+ text = f"{language}\x00{lyrics}"
107
+ else:
108
+ text = decoded_text
109
+ else:
110
+ text = decoded_text
111
+ if frame_id_str not in metadata:
112
+ metadata[frame_id_str] = []
113
+ metadata[frame_id_str].append(text)
114
+ else:
115
+ # Non-text frame, just show size
116
+ if frame_id_str not in metadata:
117
+ metadata[frame_id_str] = []
118
+ metadata[frame_id_str].append(f"<{frame_size} bytes>")
119
+
120
+ pos += 10 + frame_size
121
+
122
+ return metadata
123
+ except Exception as e:
124
+ return f"Error parsing ID3v2: {e!s}"
125
+
126
+ @staticmethod
127
+ def get_artists(file_path, version=None):
128
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
129
+ if not isinstance(metadata, dict) or not metadata:
130
+ return None
131
+ tpe1_values: list[str] = metadata.get("TPE1", [])
132
+ return tpe1_values[0] if tpe1_values else None
133
+
134
+ @staticmethod
135
+ def get_title(file_path, version=None):
136
+ try:
137
+ versions_to_try = [version] if version else ["2.3", "2.4"]
138
+ for v in versions_to_try:
139
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, v)
140
+ if isinstance(metadata, dict) and metadata:
141
+ tit2_values = metadata.get("TIT2", [])
142
+ if tit2_values:
143
+ return tit2_values[0]
144
+ except Exception:
145
+ return None
146
+ else:
147
+ return None
148
+
149
+ @staticmethod
150
+ def get_album(file_path, version=None):
151
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
152
+ if not isinstance(metadata, dict) or not metadata:
153
+ return None
154
+ talb_values = metadata.get("TALB", [])
155
+ return talb_values[0] if talb_values else None
156
+
157
+ @staticmethod
158
+ def get_year(file_path, version=None):
159
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
160
+ if not isinstance(metadata, dict) or not metadata:
161
+ return None
162
+ tyer_values = metadata.get("TYER", [])
163
+ tdrc_values = metadata.get("TDRC", [])
164
+ return (tyer_values + tdrc_values)[0] if tyer_values or tdrc_values else None
165
+
166
+ @staticmethod
167
+ def get_genres(file_path, version=None):
168
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
169
+ if not isinstance(metadata, dict) or not metadata:
170
+ return None
171
+ tcon_values = metadata.get("TCON", [])
172
+ return tcon_values[0] if tcon_values else None
173
+
174
+ @staticmethod
175
+ def get_comment(file_path, version=None):
176
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
177
+ if not isinstance(metadata, dict) or not metadata:
178
+ return None
179
+ comm_values = metadata.get("COMM", [])
180
+ return comm_values[0] if comm_values else None
181
+
182
+ @staticmethod
183
+ def get_track(file_path, version=None):
184
+ metadata = ID3v2MetadataGetter.get_raw_metadata(file_path, version)
185
+ if not isinstance(metadata, dict) or not metadata:
186
+ return None
187
+ trck_values = metadata.get("TRCK", [])
188
+ return trck_values[0] if trck_values else None
@@ -0,0 +1,428 @@
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
+ }
47
+ else:
48
+ tool = "mid3v2"
49
+ cmd = ["mid3v2"]
50
+ # Map common metadata keys to mid3v2 tool arguments
51
+ key_mapping = {
52
+ "title": "--song",
53
+ "artist": "--artist",
54
+ "album": "--album",
55
+ "year": "--year",
56
+ "genre": "--genre",
57
+ "comment": "--comment",
58
+ "track": "--track",
59
+ "track_number": "--track",
60
+ "composer": "--TCOM",
61
+ "copyright": "--TCOP",
62
+ "lyrics": "--USLT",
63
+ "language": "--TLAN",
64
+ "rating": "--POPM",
65
+ "album_artist": "--TPE2",
66
+ "encoder": "--TENC",
67
+ "url": "--WOAR",
68
+ "isrc": "--TSRC",
69
+ "mood": "--TMOO",
70
+ "key": "--TKEY",
71
+ "publisher": "--TPUB",
72
+ "bpm": "--TBPM",
73
+ }
74
+
75
+ metadata_added = False
76
+ for key, value in metadata.items():
77
+ if key.lower() in key_mapping:
78
+ cmd.extend([key_mapping[key.lower()], str(value)])
79
+ metadata_added = True
80
+
81
+ # Only run the tool if metadata was actually added
82
+ if metadata_added:
83
+ cmd.append(str(file_path))
84
+ run_external_tool(cmd, tool)
85
+
86
+ @staticmethod
87
+ def set_max_metadata(file_path: Path) -> None:
88
+ from pathlib import Path
89
+
90
+ from ..common.external_tool_runner import run_script
91
+
92
+ scripts_dir = Path(__file__).parent.parent.parent.parent / "test" / "helpers" / "scripts"
93
+ run_script("set-id3v2-max-metadata.sh", file_path, scripts_dir)
94
+
95
+ @staticmethod
96
+ def set_title(file_path: Path, title: str, version: str = "2.4") -> None:
97
+ if version == "2.3":
98
+ command = ["id3v2", "--id3v2-only", "--song", title, str(file_path)]
99
+ run_external_tool(command, "id3v2")
100
+ else:
101
+ command = ["mid3v2", "--song", title, str(file_path)]
102
+ run_external_tool(command, "mid3v2")
103
+
104
+ @staticmethod
105
+ def set_titles(file_path: Path, titles: list[str], in_separate_frames: bool = False, version: str = "2.4"):
106
+ """Set ID3v2 multiple titles using external mid3v2 tool or manual frame creation.
107
+
108
+ Args:
109
+ file_path: Path to the audio file
110
+ titles: List of title strings to set
111
+ in_separate_frames: If True, creates multiple separate TIT2 frames (one per title)
112
+ using manual binary construction. If False (default), creates a single TIT2 frame
113
+ with multiple values using mid3v2.
114
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
115
+ """
116
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TIT2", titles, in_separate_frames, version)
117
+
118
+ @staticmethod
119
+ def set_artists(file_path: Path, artists, in_separate_frames: bool = False, version: str = "2.4") -> None:
120
+ if isinstance(artists, str):
121
+ # For string input, use external tool directly to avoid replacing entire tag
122
+ if version == "2.3":
123
+ command = ["id3v2", "--id3v2-only", "--artist", artists, str(file_path)]
124
+ run_external_tool(command, "id3v2")
125
+ else:
126
+ # Use mid3v2 for ID3v2.4
127
+ command = ["mid3v2", "--artist", artists, str(file_path)]
128
+ run_external_tool(command, "mid3v2")
129
+ else:
130
+ # For list input, use multiple values handling
131
+ ID3v2MetadataSetter._set_multiple_metadata_values(
132
+ file_path, "TPE1", artists, in_separate_frames=in_separate_frames, version=version
133
+ )
134
+
135
+ @staticmethod
136
+ def set_album(file_path: Path, album: str, version: str = "2.4") -> None:
137
+ if version == "2.3":
138
+ command = ["id3v2", "--id3v2-only", "--album", album, str(file_path)]
139
+ run_external_tool(command, "id3v2")
140
+ else:
141
+ command = ["mid3v2", "--album", album, str(file_path)]
142
+ run_external_tool(command, "mid3v2")
143
+
144
+ @staticmethod
145
+ def set_genre(file_path: Path, genre: str, version: str = "2.4") -> None:
146
+ if version == "2.3":
147
+ command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
148
+ run_external_tool(command, "id3v2")
149
+ else:
150
+ command = ["id3v2", "--id3v2-only", "--genre", genre, str(file_path)]
151
+ run_external_tool(command, "id3v2")
152
+
153
+ @staticmethod
154
+ def set_lyrics(file_path: Path, lyrics: str, version: str = "2.4") -> None:
155
+ if version == "2.3":
156
+ command = ["id3v2", "--id3v2-only", "--USLT", lyrics, str(file_path)]
157
+ run_external_tool(command, "id3v2")
158
+ else:
159
+ command = ["mid3v2", "--USLT", lyrics, str(file_path)]
160
+ run_external_tool(command, "mid3v2")
161
+
162
+ @staticmethod
163
+ def set_language(file_path: Path, language: str, version: str = "2.4") -> None:
164
+ if version == "2.3":
165
+ command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
166
+ run_external_tool(command, "id3v2")
167
+ else:
168
+ command = ["id3v2", "--id3v2-only", "--TLAN", language, str(file_path)]
169
+ run_external_tool(command, "id3v2")
170
+
171
+ @staticmethod
172
+ def set_bpm(file_path: Path, bpm: int, version: str = "2.4") -> None:
173
+ if version == "2.3":
174
+ command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
175
+ run_external_tool(command, "id3v2")
176
+ else:
177
+ command = ["id3v2", "--id3v2-only", "--TBPM", str(bpm), str(file_path)]
178
+ run_external_tool(command, "id3v2")
179
+
180
+ @staticmethod
181
+ def set_release_date(file_path: Path, date_str: str, version: str = "2.4") -> None:
182
+ """Set ID3v2 release date using version-specific frames.
183
+
184
+ For ID3v2.3: Uses TYER (year) and TDAT (MMDD) frames
185
+ For ID3v2.4: Uses TDRC frame with full date
186
+
187
+ Args:
188
+ file_path: Path to the audio file
189
+ date_str: Date string in YYYY-MM-DD format
190
+ version: ID3v2 version to use ("2.3" or "2.4")
191
+ """
192
+ if version == "2.3":
193
+ # Parse YYYY-MM-DD format
194
+ if len(date_str) >= 10:
195
+ year = date_str[:4]
196
+ month = date_str[5:7]
197
+ day = date_str[8:10]
198
+ # TDAT format is DDMM
199
+ date_ddmm = f"{day}{month}"
200
+
201
+ # Set TYER frame
202
+ command_year = ["id3v2", "--id3v2-only", "--TYER", year, str(file_path)]
203
+ run_external_tool(command_year, "id3v2")
204
+
205
+ # Set TDAT frame
206
+ command_date = ["id3v2", "--id3v2-only", "--TDAT", date_ddmm, str(file_path)]
207
+ run_external_tool(command_date, "id3v2")
208
+ else:
209
+ # Fallback for year-only dates
210
+ command = ["id3v2", "--id3v2-only", "--TYER", date_str, str(file_path)]
211
+ run_external_tool(command, "id3v2")
212
+ else:
213
+ # ID3v2.4: Use TDRC with full date
214
+ command = ["mid3v2", "--TDRC", date_str, str(file_path)]
215
+ run_external_tool(command, "mid3v2")
216
+
217
+ @staticmethod
218
+ def _set_multiple_values_single_frame(
219
+ file_path: Path, frame_id: str, values: list[str], version: str = "2.4", separator: str | None = None
220
+ ) -> None:
221
+ """Set multiple values in a single ID3v2 frame with version-specific handling.
222
+
223
+ Args:
224
+ file_path: Path to the audio file
225
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
226
+ values: List of values to set in the frame
227
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
228
+ separator: Separator to use between values. If None, uses default behavior
229
+ (semicolon for ID3v2.3, null byte for ID3v2.4)
230
+ """
231
+ # Determine separator if not provided
232
+ if separator is None:
233
+ separator = ";" if version == "2.3" else "\x00"
234
+
235
+ # Use appropriate method for all cases
236
+ ID3v2MetadataSetter._set_single_frame_with_id3v2(file_path, frame_id, values, version, separator)
237
+
238
+ @staticmethod
239
+ def _set_multiple_metadata_values(
240
+ file_path: Path,
241
+ frame_id: str,
242
+ values: list[str],
243
+ in_separate_frames: bool = False,
244
+ version: str = "2.4",
245
+ separator: str | None = None,
246
+ ) -> None:
247
+ """Set multiple metadata values, either in separate frames or a single frame.
248
+
249
+ Args:
250
+ file_path: Path to the audio file
251
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
252
+ values: List of values to set
253
+ in_separate_frames: If True, creates multiple separate frames. If False, creates a
254
+ single frame with multiple values.
255
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
256
+ separator: Separator to use between values when in_separate_frames=False. If None, uses default behavior.
257
+ """
258
+ from .id3v2_metadata_deleter import ID3v2MetadataDeleter
259
+
260
+ # Delete existing frames
261
+ ID3v2MetadataDeleter.delete_frame(file_path, frame_id)
262
+
263
+ if in_separate_frames:
264
+ # Use manual binary construction to create truly separate frames
265
+ ID3v2MetadataSetter._create_multiple_id3v2_frames(file_path, frame_id, values, version)
266
+ else:
267
+ # Create a single frame with multiple values (version-specific handling)
268
+ ID3v2MetadataSetter._set_multiple_values_single_frame(file_path, frame_id, values, version, separator)
269
+
270
+ @staticmethod
271
+ def set_genres(file_path: Path, genres: list[str], in_separate_frames: bool = False, version: str = "2.4"):
272
+ """Set ID3v2 multiple genres using external mid3v2 tool or manual frame creation.
273
+
274
+ Args:
275
+ file_path: Path to the audio file
276
+ genres: List of genre strings to set
277
+ in_separate_frames: If True, creates multiple separate TCON frames (one per genre)
278
+ using manual binary construction. If False (default), creates a single TCON frame
279
+ with multiple values using mid3v2.
280
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
281
+ """
282
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCON", genres, in_separate_frames, version)
283
+
284
+ @staticmethod
285
+ def set_album_artists(file_path: Path, album_artists: list[str], in_separate_frames: bool = False):
286
+ """Set ID3v2 multiple album artists using external mid3v2 tool or manual frame creation.
287
+
288
+ Args:
289
+ file_path: Path to the audio file
290
+ album_artists: List of album artist strings to set
291
+ in_separate_frames: If True, creates multiple separate TPE2 frames (one per album artist)
292
+ using manual binary construction. If False (default), creates a single TPE2 frame
293
+ with multiple values using mid3v2.
294
+ """
295
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TPE2", album_artists, in_separate_frames)
296
+
297
+ @staticmethod
298
+ def set_composers(file_path: Path, composers: list[str], in_separate_frames: bool = False, version: str = "2.4"):
299
+ """Set ID3v2 multiple composers using external mid3v2 tool or manual frame creation.
300
+
301
+ Args:
302
+ file_path: Path to the audio file
303
+ composers: List of composer strings to set
304
+ in_separate_frames: If True, creates multiple separate TCOM frames (one per composer)
305
+ using manual binary construction. If False (default), creates a single TCOM frame
306
+ with multiple values using mid3v2.
307
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
308
+ """
309
+ ID3v2MetadataSetter._set_multiple_metadata_values(file_path, "TCOM", composers, in_separate_frames, version)
310
+
311
+ @staticmethod
312
+ def set_comments(file_path: Path, comments: list[str], in_separate_frames: bool = False):
313
+ """Set ID3v2 multiple comments using external mid3v2 tool.
314
+
315
+ Args:
316
+ file_path: Path to the audio file
317
+ comments: List of comment strings to set
318
+ in_separate_frames: If True, creates multiple separate COMM frames (one per comment).
319
+ If False (default), creates a single COMM frame with the first comment value.
320
+ """
321
+ from .id3v2_metadata_deleter import ID3v2MetadataDeleter
322
+
323
+ # Delete existing COMM tags
324
+ ID3v2MetadataDeleter.delete_frame(file_path, "COMM")
325
+
326
+ if in_separate_frames:
327
+ # Set each comment in a separate id3v2 call to force multiple frames
328
+ for comment in comments:
329
+ command = ["id3v2", "--id3v2-only", "--comment", comment, str(file_path)]
330
+ run_external_tool(command, "id3v2")
331
+ # Set only the first comment (ID3v2 comment fields are typically single-valued)
332
+ elif comments:
333
+ command = ["id3v2", "--id3v2-only", "--comment", comments[0], str(file_path)]
334
+ run_external_tool(command, "id3v2")
335
+
336
+ @staticmethod
337
+ def _create_multiple_id3v2_frames(file_path: Path, frame_id: str, texts: list[str], version: str = "2.4") -> None:
338
+ """Create multiple separate ID3v2 frames using manual binary construction.
339
+
340
+ This uses the ManualID3v2FrameCreator to bypass standard tools that
341
+ consolidate multiple frames of the same type, allowing creation of
342
+ truly separate frames for testing purposes.
343
+
344
+ Args:
345
+ file_path: Path to the audio file
346
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TPE2', 'TCON', 'TCOM')
347
+ texts: List of text values, one per frame
348
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
349
+ """
350
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
351
+
352
+ # Create frames based on the frame type
353
+ if frame_id == "TPE1":
354
+ ManualID3v2FrameCreator.create_multiple_tpe1_frames(file_path, texts, version)
355
+ elif frame_id == "TPE2":
356
+ ManualID3v2FrameCreator.create_multiple_tpe2_frames(file_path, texts, version)
357
+ elif frame_id == "TCON":
358
+ ManualID3v2FrameCreator.create_multiple_tcon_frames(file_path, texts, version)
359
+ elif frame_id == "TCOM":
360
+ ManualID3v2FrameCreator.create_multiple_tcom_frames(file_path, texts, version)
361
+ else:
362
+ # Generic frame creation for other frame types (including TIT2)
363
+ creator = ManualID3v2FrameCreator()
364
+ frames = []
365
+ for text in texts:
366
+ frame_data = creator._create_text_frame(frame_id, text, version)
367
+ frames.append(frame_data)
368
+ creator._write_id3v2_tag(file_path, frames, version)
369
+
370
+ @staticmethod
371
+ def _set_single_frame_with_id3v2(
372
+ file_path: Path, frame_id: str, alist: list[str], version: str, separator: str | None = None
373
+ ) -> None:
374
+ """Internal helper: Create a single ID3v2 frame using appropriate tool based on version.
375
+
376
+ Args:
377
+ file_path: Path to the audio file
378
+ frame_id: The ID3v2 frame identifier (e.g., 'TPE1', 'TCON', 'TCOM', 'TIT2')
379
+ alist: List of text values for the frame
380
+ version: ID3v2 version to use (e.g., "2.3", "2.4")
381
+ separator: Separator to use between values. If None, uses default.
382
+ """
383
+ # Determine separator if not provided
384
+ if separator is None:
385
+ separator = ";" if version == "2.3" else "\x00"
386
+
387
+ # Combine values with the appropriate separator
388
+ combined_text = separator.join(alist) if len(alist) > 1 else alist[0] if alist else ""
389
+
390
+ # Check if we have null bytes - if so, use manual frame creator for ID3v2.4
391
+ if version == "2.4" and "\x00" in combined_text:
392
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
393
+
394
+ creator = ManualID3v2FrameCreator()
395
+ frame_data = creator._create_text_frame(frame_id, combined_text, version)
396
+ creator._write_id3v2_tag(file_path, [frame_data], version)
397
+ return
398
+
399
+ # Map frame IDs to tool flags
400
+ flag_mapping = {
401
+ "TCON": "--genre",
402
+ "TIT2": "--song",
403
+ "TPE1": "--artist",
404
+ "TALB": "--album",
405
+ "TDRC": "--year",
406
+ "TRCK": "--track",
407
+ "COMM": "--comment",
408
+ }
409
+
410
+ flag = flag_mapping.get(frame_id, f"--{frame_id.lower()}")
411
+
412
+ if version == "2.3":
413
+ # Use id3v2 for ID3v2.3
414
+ command = ["id3v2", "--id3v2-only", flag, combined_text, str(file_path)]
415
+ run_external_tool(command, "id3v2")
416
+ else:
417
+ # Use mid3v2 for ID3v2.4
418
+ command = ["mid3v2", flag, combined_text, str(file_path)]
419
+ run_external_tool(command, "mid3v2")
420
+
421
+ @staticmethod
422
+ def write_tpe1_with_encoding(file_path: Path, text: str, encoding: int) -> None:
423
+ """Write a TPE1 frame with specific encoding for testing purposes."""
424
+ from .id3v2_frame_manual_creator import ManualID3v2FrameCreator
425
+
426
+ creator = ManualID3v2FrameCreator()
427
+ frame_data = creator._create_text_frame("TPE1", text, "2.4", encoding=encoding)
428
+ 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"]