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,512 @@
1
+ from typing import Any, cast
2
+
3
+ from mutagen._file import FileType as MutagenMetadata
4
+
5
+ from audiometa.utils.unified_metadata_key import UnifiedMetadataKey
6
+
7
+ from ..._audio_file import _AudioFile
8
+ from ...exceptions import FileCorruptedError, MetadataFieldNotSupportedByMetadataFormatError
9
+ from ...utils.types import RawMetadataDict, RawMetadataKey, UnifiedMetadata, UnifiedMetadataValue
10
+ from .._MetadataManager import _MetadataManager
11
+ from ._constants import ID3V1_MIN_COMMENT_LENGTH_TO_CHECK_TRACK_NUMBER, ID3V1_TAG_SIZE
12
+ from .id3v1_raw_metadata import Id3v1RawMetadata
13
+ from .id3v1_raw_metadata_key import Id3v1RawMetadataKey
14
+
15
+
16
+ class _Id3v1Manager(_MetadataManager):
17
+ """Manages ID3v1 metadata for audio files.
18
+
19
+ ID3v1 is a simple, legacy metadata format with significant limitations:
20
+ - Fixed 128-byte block at end of file
21
+ - No Unicode support (Latin-1 only)
22
+ - Limited field lengths (30 chars)
23
+ - No support for:
24
+ - Album artist
25
+ - BPM
26
+ - Ratings
27
+ - Language
28
+ - Custom genres
29
+ - Multiple genres
30
+ - Multiple artists
31
+ ...
32
+ """
33
+
34
+ def _get_formatted_metadata_format_name(self) -> str:
35
+ """Get the formatted metadata format name.
36
+
37
+ Returns:
38
+ The formatted format name 'ID3v1'
39
+ """
40
+ return "ID3v1"
41
+
42
+ """
43
+ - Supports both reading and writing metadata using direct file manipulation.
44
+
45
+ Format Structure:
46
+ - Bytes 0-2: "TAG" identifier
47
+ - Bytes 3-32: Title (30 chars)
48
+ - Bytes 33-62: Artist (30 chars)
49
+ - Bytes 63-92: Album (30 chars)
50
+ - Bytes 93-96: Release year (4 chars)
51
+ - Bytes 97-126: Comment (28 chars in ID3v1.1, 30 chars in ID3v1)
52
+ - Byte 125: Always 0 in ID3v1.1 to indicate track number presence
53
+ - Byte 126: Track number in ID3v1.1 (1-255, 0 = not set)
54
+ - Byte 127: Genre code (0-255)
55
+
56
+ Note: ID3v1.1 extends ID3v1 by using the last two bytes of the comment
57
+ field to store the track number. If byte 125 is 0 and byte 126 is not 0,
58
+ then byte 126 contains the track number (1-255).
59
+
60
+ Note 2: The genre code is an index into a predefined list of genres.
61
+
62
+ Supported File Formats:
63
+ - MP3: Native ID3v1 format, optimal support
64
+ - FLAC: Some FLAC files may contain ID3v1 tags (not optimal but supported)
65
+ - WAV: Some WAV files may contain ID3v1 tags (not optimal but supported)
66
+
67
+ While ID3v1 is natively designed for MP3 files, this manager supports reading
68
+ ID3v1 tags from FLAC and WAV files when present, even though it's not the
69
+ optimal metadata format for these file types.
70
+ """
71
+
72
+ def __init__(self, audio_file: _AudioFile):
73
+ metadata_keys_direct_map_read: dict = {
74
+ UnifiedMetadataKey.TITLE: Id3v1RawMetadataKey.TITLE,
75
+ UnifiedMetadataKey.ARTISTS: Id3v1RawMetadataKey.ARTISTS_NAMES_STR,
76
+ UnifiedMetadataKey.ALBUM: Id3v1RawMetadataKey.ALBUM,
77
+ UnifiedMetadataKey.RELEASE_DATE: Id3v1RawMetadataKey.YEAR,
78
+ UnifiedMetadataKey.TRACK_NUMBER: Id3v1RawMetadataKey.TRACK_NUMBER,
79
+ UnifiedMetadataKey.COMMENT: Id3v1RawMetadataKey.COMMENT,
80
+ UnifiedMetadataKey.GENRES_NAMES: None,
81
+ }
82
+ metadata_keys_direct_map_write: dict = {
83
+ UnifiedMetadataKey.TITLE: Id3v1RawMetadataKey.TITLE,
84
+ UnifiedMetadataKey.ARTISTS: Id3v1RawMetadataKey.ARTISTS_NAMES_STR,
85
+ UnifiedMetadataKey.ALBUM: Id3v1RawMetadataKey.ALBUM,
86
+ UnifiedMetadataKey.RELEASE_DATE: Id3v1RawMetadataKey.YEAR,
87
+ UnifiedMetadataKey.TRACK_NUMBER: Id3v1RawMetadataKey.TRACK_NUMBER,
88
+ UnifiedMetadataKey.COMMENT: Id3v1RawMetadataKey.COMMENT,
89
+ UnifiedMetadataKey.GENRES_NAMES: None, # Handled indirectly
90
+ }
91
+ super().__init__(
92
+ audio_file=audio_file,
93
+ metadata_keys_direct_map_read=metadata_keys_direct_map_read,
94
+ metadata_keys_direct_map_write=metadata_keys_direct_map_write,
95
+ update_using_mutagen_metadata=False, # Use direct file manipulation for ID3v1
96
+ )
97
+
98
+ @staticmethod
99
+ def find_safe_separator(values: list[str]) -> str:
100
+ """ID3v1-specific separator logic optimized for legacy format constraints.
101
+
102
+ Only single-character separators are used to maximize space efficiency in 30-char fields.
103
+ Tries to use the safest available single-character separator that does not appear in any value.
104
+
105
+ Separator priority for ID3v1 (all single-character, Latin-1 compatible):
106
+ 1. ',' (comma) - Standard, readable
107
+ 2. ';' (semicolon) - Common alternative
108
+ 3. '|' (pipe) - Less common
109
+ 4. '·' (middle dot) - Unicode but Latin-1 safe
110
+ 5. '/' (slash) - Last resort, may be confusing
111
+
112
+ Args:
113
+ values: List of string values to check for separator conflicts
114
+
115
+ Returns:
116
+ The safest available single-character separator, prioritizing readability and space efficiency
117
+ """
118
+ # ID3v1 compatible single-character separators in order of preference
119
+ id3v1_separators = [",", ";", "|", "·", "/"]
120
+
121
+ # Find the first separator that doesn't appear in any value
122
+ for sep in id3v1_separators:
123
+ if not any(sep in value for value in values):
124
+ return sep
125
+
126
+ # If all separators conflict, use comma as fallback (established convention)
127
+ return ","
128
+
129
+ def _extract_mutagen_metadata(self) -> Id3v1RawMetadata:
130
+ try:
131
+ return Id3v1RawMetadata(fileobj=self.audio_file.file_path)
132
+ except Exception as exc:
133
+ msg = f"Failed to extract ID3v1 metadata: {exc}"
134
+ raise FileCorruptedError(msg) from exc
135
+
136
+ def _convert_raw_mutagen_metadata_to_dict_with_potential_duplicate_keys(
137
+ self, raw_mutagen_metadata: MutagenMetadata
138
+ ) -> RawMetadataDict:
139
+ raw_metadata_id3v1: Id3v1RawMetadata = cast(Id3v1RawMetadata, raw_mutagen_metadata)
140
+ if not raw_metadata_id3v1.tags:
141
+ return {}
142
+
143
+ # Create a mapping of string values to enum members with proper value types
144
+ result: RawMetadataDict = {} # type: ignore[unreachable]
145
+ for key, value in raw_metadata_id3v1.tags.items():
146
+ # Skip empty values
147
+ if not value:
148
+ continue
149
+
150
+ # Find the matching enum member by its value
151
+ for enum_member in Id3v1RawMetadataKey:
152
+ if enum_member == key:
153
+ result[enum_member] = value
154
+ return result
155
+
156
+ def _get_undirectly_mapped_metadata_value_from_raw_clean_metadata(
157
+ self, raw_clean_metadata_uppercase_keys: RawMetadataDict, unified_metadata_key: UnifiedMetadataKey
158
+ ) -> UnifiedMetadataValue:
159
+ if unified_metadata_key == UnifiedMetadataKey.GENRES_NAMES:
160
+ return self._get_genre_name_from_raw_clean_metadata_id3v1(
161
+ raw_clean_metadata=raw_clean_metadata_uppercase_keys,
162
+ raw_metadata_ket=Id3v1RawMetadataKey.GENRE_CODE_OR_NAME,
163
+ )
164
+ msg = f"{unified_metadata_key} metadata is not undirectly handled"
165
+ raise MetadataFieldNotSupportedByMetadataFormatError(msg)
166
+
167
+ def _update_undirectly_mapped_metadata(
168
+ self,
169
+ raw_mutagen_metadata: MutagenMetadata,
170
+ app_metadata_value: UnifiedMetadataValue,
171
+ unified_metadata_key: UnifiedMetadataKey,
172
+ ) -> None:
173
+ # Ensure tags exist
174
+ if not hasattr(raw_mutagen_metadata, "tags") or raw_mutagen_metadata.tags is None:
175
+ raw_mutagen_metadata.tags = {}
176
+ # Type narrowing: mypy now knows tags is not None after the assignment above
177
+ tags: dict[Any, Any] = cast(dict[Any, Any], raw_mutagen_metadata.tags)
178
+
179
+ if unified_metadata_key == UnifiedMetadataKey.GENRES_NAMES:
180
+ # Handle both single string and list values gracefully
181
+ if isinstance(app_metadata_value, list):
182
+ app_metadata_value = app_metadata_value[0] if app_metadata_value else None
183
+
184
+ if app_metadata_value:
185
+ # Convert genre name to genre code
186
+ genre_code = self._convert_genre_name_to_code(str(app_metadata_value))
187
+ if genre_code is not None:
188
+ tags[Id3v1RawMetadataKey.GENRE_CODE_OR_NAME] = [str(genre_code)]
189
+ else:
190
+ msg = f"{unified_metadata_key} metadata is not undirectly handled"
191
+ raise MetadataFieldNotSupportedByMetadataFormatError(msg)
192
+
193
+ def _update_formatted_value_in_raw_mutagen_metadata(
194
+ self,
195
+ raw_mutagen_metadata: MutagenMetadata,
196
+ raw_metadata_key: RawMetadataKey,
197
+ app_metadata_value: UnifiedMetadataValue,
198
+ ) -> None:
199
+ # Ensure tags exist
200
+ if not hasattr(raw_mutagen_metadata, "tags") or raw_mutagen_metadata.tags is None:
201
+ raw_mutagen_metadata.tags = {}
202
+ # Type narrowing: mypy now knows tags is not None after the assignment above
203
+ tags: dict[Any, Any] = cast(dict[Any, Any], raw_mutagen_metadata.tags)
204
+
205
+ # If value is None, remove the field (delete from tags)
206
+ if app_metadata_value is None:
207
+ if raw_metadata_key in tags:
208
+ del tags[raw_metadata_key]
209
+ return
210
+
211
+ # Convert and truncate the value according to ID3v1 constraints
212
+ if raw_metadata_key == Id3v1RawMetadataKey.TITLE:
213
+ value = self._truncate_string(str(app_metadata_value), 30)
214
+ elif raw_metadata_key == Id3v1RawMetadataKey.ARTISTS_NAMES_STR:
215
+ # Convert list to string using smart separator logic and truncate
216
+ if isinstance(app_metadata_value, list):
217
+ if app_metadata_value:
218
+ separator = self.find_safe_separator(app_metadata_value)
219
+ artists_str = separator.join(app_metadata_value)
220
+ else:
221
+ # If no valid values, set to empty string
222
+ artists_str = ""
223
+ else:
224
+ artists_str = str(app_metadata_value)
225
+ value = self._truncate_string(artists_str, 30)
226
+ elif raw_metadata_key == Id3v1RawMetadataKey.ALBUM:
227
+ value = self._truncate_string(str(app_metadata_value), 30)
228
+ elif raw_metadata_key == Id3v1RawMetadataKey.YEAR:
229
+ value = self._truncate_string(str(app_metadata_value), 4)
230
+ elif raw_metadata_key == Id3v1RawMetadataKey.TRACK_NUMBER:
231
+ # Convert to int and validate range, parsing strings like "5/12"
232
+ if isinstance(app_metadata_value, str):
233
+ import re
234
+
235
+ if re.match(r"^\d+([-/]\d*)?$", app_metadata_value):
236
+ track_match = re.match(r"(\d+)", app_metadata_value)
237
+ track_num = 0 if track_match is None else int(track_match.group(1))
238
+ else:
239
+ track_num = 0
240
+ elif isinstance(app_metadata_value, int | float):
241
+ track_num = int(float(app_metadata_value))
242
+ else:
243
+ track_num = 0
244
+ value = str(max(0, min(255, track_num)))
245
+ elif raw_metadata_key == Id3v1RawMetadataKey.COMMENT:
246
+ value = self._truncate_string(str(app_metadata_value), 28) # 28 for ID3v1.1 with track number
247
+ else:
248
+ value = str(app_metadata_value)
249
+
250
+ tags[raw_metadata_key] = [value]
251
+
252
+ def _update_not_using_mutagen_metadata(self, unified_metadata: UnifiedMetadata) -> None:
253
+ """Update ID3v1 metadata using direct file manipulation."""
254
+ # Validate that all fields are supported by ID3v1
255
+ if self.metadata_keys_direct_map_write is None:
256
+ msg = "metadata_keys_direct_map_write is None"
257
+ raise MetadataFieldNotSupportedByMetadataFormatError(msg)
258
+ for unified_metadata_key in unified_metadata:
259
+ if unified_metadata_key not in self.metadata_keys_direct_map_write:
260
+ metadata_format_name = self._get_formatted_metadata_format_name()
261
+ msg = f"{unified_metadata_key} metadata not supported by {metadata_format_name} format"
262
+ raise MetadataFieldNotSupportedByMetadataFormatError(msg)
263
+
264
+ # Read the entire file
265
+ self.audio_file.seek(0)
266
+ file_data = bytearray(self.audio_file.read())
267
+
268
+ # Create ID3v1 tag data
269
+ tag_data = self._create_id3v1_tag_data(unified_metadata)
270
+
271
+ # Find and remove existing ID3v1 tag if present
272
+ self._remove_existing_id3v1_tag(file_data)
273
+
274
+ # Append new ID3v1 tag
275
+ file_data.extend(tag_data)
276
+
277
+ # Write back to file
278
+ self.audio_file.write(file_data)
279
+
280
+ def _create_id3v1_tag_data(self, unified_metadata: UnifiedMetadata) -> bytes:
281
+ """Create 128-byte ID3v1 tag data from app metadata."""
282
+ # Initialize with null bytes
283
+ tag_data = bytearray(128)
284
+
285
+ # TAG identifier (bytes 0-2)
286
+ tag_data[0:3] = b"TAG"
287
+
288
+ # Title (bytes 3-32, 30 chars max)
289
+ title = unified_metadata.get(UnifiedMetadataKey.TITLE)
290
+ if title is not None:
291
+ title_bytes = self._truncate_string(str(title), 30).encode("latin-1", errors="ignore")
292
+ tag_data[3 : 3 + len(title_bytes)] = title_bytes
293
+
294
+ # Artist (bytes 33-62, 30 chars max)
295
+ artists = unified_metadata.get(UnifiedMetadataKey.ARTISTS)
296
+ if artists is not None:
297
+ if isinstance(artists, list):
298
+ separator = self.find_safe_separator(artists)
299
+ artist_str = separator.join(artists)
300
+ else:
301
+ artist_str = str(artists)
302
+ artist_bytes = self._truncate_string(artist_str, 30).encode("latin-1", errors="ignore")
303
+ tag_data[33 : 33 + len(artist_bytes)] = artist_bytes
304
+
305
+ # Album (bytes 63-92, 30 chars max)
306
+ album = unified_metadata.get(UnifiedMetadataKey.ALBUM)
307
+ if album is not None:
308
+ album_bytes = self._truncate_string(str(album), 30).encode("latin-1", errors="ignore")
309
+ tag_data[63 : 63 + len(album_bytes)] = album_bytes
310
+
311
+ # Year (bytes 93-96, 4 chars max)
312
+ year = unified_metadata.get(UnifiedMetadataKey.RELEASE_DATE)
313
+ if year is not None:
314
+ year_bytes = self._truncate_string(str(year), 4).encode("latin-1", errors="ignore")
315
+ tag_data[93 : 93 + len(year_bytes)] = year_bytes
316
+
317
+ # Comment and track number (bytes 97-126, 28 chars for comment + 2 for track)
318
+ comment = unified_metadata.get(UnifiedMetadataKey.COMMENT)
319
+ if comment is not None:
320
+ comment_bytes = self._truncate_string(str(comment), 28).encode("latin-1", errors="ignore")
321
+ tag_data[97 : 97 + len(comment_bytes)] = comment_bytes
322
+
323
+ # Track number (bytes 125-126 for ID3v1.1)
324
+ track_number = unified_metadata.get(UnifiedMetadataKey.TRACK_NUMBER)
325
+ if track_number is not None:
326
+ if isinstance(track_number, str):
327
+ import re
328
+
329
+ if re.match(r"^\d+([-/]\d*)?$", track_number):
330
+ track_match = re.match(r"(\d+)", track_number)
331
+ track_num = 0 if track_match is None else int(track_match.group(1))
332
+ else:
333
+ track_num = 0
334
+ elif isinstance(track_number, int | float):
335
+ track_num = int(float(track_number))
336
+ else:
337
+ track_num = 0
338
+ track_num = max(0, min(255, track_num))
339
+ if track_num > 0:
340
+ tag_data[125] = 0 # Null byte to indicate track number presence
341
+ tag_data[126] = track_num
342
+
343
+ # Genre (byte 127)
344
+ genre_name = unified_metadata.get(UnifiedMetadataKey.GENRES_NAMES)
345
+ if genre_name is not None:
346
+ # Handle both single string and list values gracefully
347
+ if isinstance(genre_name, list):
348
+ genre_name = genre_name[0] if genre_name else None
349
+
350
+ if genre_name:
351
+ genre_code = self._convert_genre_name_to_code(str(genre_name))
352
+ if genre_code is not None:
353
+ tag_data[127] = genre_code
354
+ else:
355
+ tag_data[127] = 255 # Unknown genre
356
+ else:
357
+ # Genre is explicitly set to None/empty - mark as undefined for deletion
358
+ tag_data[127] = 255 # Undefined genre
359
+ else:
360
+ # Genre is not specified - mark as undefined for deletion
361
+ tag_data[127] = 255 # Undefined genre
362
+
363
+ return bytes(tag_data)
364
+
365
+ def _remove_existing_id3v1_tag(self, file_data: bytearray) -> bool:
366
+ """Remove existing ID3v1 tag from file data if present.
367
+
368
+ Returns:
369
+ bool: True if a tag was removed, False otherwise
370
+ """
371
+ if len(file_data) >= ID3V1_TAG_SIZE:
372
+ # Check if last 128 bytes contain ID3v1 tag
373
+ last_128 = file_data[-ID3V1_TAG_SIZE:]
374
+ if last_128[:3] == b"TAG":
375
+ # Remove the last 128 bytes
376
+ del file_data[-ID3V1_TAG_SIZE:]
377
+ return True
378
+ return False
379
+
380
+ def _truncate_string(self, text: str, max_length: int) -> str:
381
+ """Truncate string to maximum length, handling encoding properly."""
382
+ if len(text) <= max_length:
383
+ return text
384
+ return text[:max_length]
385
+
386
+ def _convert_genre_name_to_code(self, genre_name: str) -> int | None:
387
+ """Convert genre name to ID3v1 genre code."""
388
+ from .._MetadataManager import ID3V1_GENRE_CODE_MAP
389
+
390
+ # First try exact match
391
+ for code, name in ID3V1_GENRE_CODE_MAP.items():
392
+ if name and name.lower() == genre_name.lower():
393
+ return code
394
+
395
+ # Try partial match
396
+ for code, name in ID3V1_GENRE_CODE_MAP.items():
397
+ if name and genre_name.lower() in name.lower():
398
+ return code
399
+
400
+ return None
401
+
402
+ def delete_metadata(self) -> bool:
403
+ """Delete ID3v1 metadata from the audio file.
404
+
405
+ Returns:
406
+ bool: True if metadata was successfully deleted, False otherwise
407
+ """
408
+ try:
409
+ # Read the entire file
410
+ self.audio_file.seek(0)
411
+ file_data = bytearray(self.audio_file.read())
412
+
413
+ # Remove existing ID3v1 tag if present
414
+ if self._remove_existing_id3v1_tag(file_data):
415
+ # Write back to file
416
+ self.audio_file.write(file_data)
417
+ return True
418
+ except Exception:
419
+ return False
420
+ else:
421
+ return False
422
+
423
+ def get_header_info(self) -> dict:
424
+ try:
425
+ if self.raw_mutagen_metadata is None:
426
+ self.raw_mutagen_metadata = self._extract_mutagen_metadata()
427
+
428
+ if not self.raw_mutagen_metadata:
429
+ return {
430
+ "present": False,
431
+ "position": "end_of_file",
432
+ "size_bytes": 0,
433
+ "version": None,
434
+ "has_track_number": False,
435
+ }
436
+
437
+ # Check if ID3v1 tag is present
438
+ present = hasattr(self.raw_mutagen_metadata, "tags") and self.raw_mutagen_metadata.tags is not None
439
+
440
+ if not present:
441
+ return {
442
+ "present": False,
443
+ "position": "end_of_file",
444
+ "size_bytes": 0,
445
+ "version": None,
446
+ "has_track_number": False,
447
+ }
448
+
449
+ # Determine version (ID3v1 or ID3v1.1)
450
+ version = "1.0"
451
+ has_track_number = False
452
+
453
+ # Check if track number is present (ID3v1.1 feature)
454
+ # We already know tags is not None from the present check above
455
+ tags = cast(dict[Any, Any], self.raw_mutagen_metadata.tags)
456
+ comment = tags.get("COMMENT", [""])[0]
457
+ if (
458
+ len(comment) >= ID3V1_MIN_COMMENT_LENGTH_TO_CHECK_TRACK_NUMBER
459
+ and comment[-2] == "\x00"
460
+ and comment[-1] != "\x00"
461
+ ):
462
+ version = "1.1"
463
+ has_track_number = True
464
+ except Exception:
465
+ return {
466
+ "present": False,
467
+ "position": "end_of_file",
468
+ "size_bytes": 0,
469
+ "version": None,
470
+ "has_track_number": False,
471
+ }
472
+ else:
473
+ return {
474
+ "present": True,
475
+ "position": "end_of_file",
476
+ "size_bytes": 128,
477
+ "version": version,
478
+ "has_track_number": has_track_number,
479
+ }
480
+
481
+ def get_raw_metadata_info(self) -> dict:
482
+ """Get raw ID3v1 metadata information."""
483
+ try:
484
+ if self.raw_mutagen_metadata is None:
485
+ self.raw_mutagen_metadata = self._extract_mutagen_metadata()
486
+
487
+ if (
488
+ not self.raw_mutagen_metadata
489
+ or not hasattr(self.raw_mutagen_metadata, "tags")
490
+ or self.raw_mutagen_metadata.tags is None
491
+ ):
492
+ return {"raw_data": None, "parsed_fields": {}, "frames": {}, "comments": {}, "chunk_structure": {}}
493
+
494
+ # Get parsed fields using unified metadata keys
495
+ parsed_fields: dict[str, Any] = {}
496
+ # We already checked tags exist and is not None above
497
+ tags = cast(dict[Any, Any], self.raw_mutagen_metadata.tags)
498
+ # Map raw mutagen keys to unified metadata keys
499
+ for unified_key, raw_key in self.metadata_keys_direct_map_read.items():
500
+ if raw_key and raw_key in tags:
501
+ value = tags[raw_key]
502
+ parsed_fields[unified_key] = value[0] if value else ""
503
+ except Exception:
504
+ return {"raw_data": None, "parsed_fields": {}, "frames": {}, "comments": {}, "chunk_structure": {}}
505
+ else:
506
+ return {
507
+ "raw_data": None, # ID3v1 is 128 bytes at end of file
508
+ "parsed_fields": parsed_fields,
509
+ "frames": {},
510
+ "comments": {},
511
+ "chunk_structure": {},
512
+ }
@@ -0,0 +1 @@
1
+ """ID3v1 metadata management."""
@@ -0,0 +1,8 @@
1
+ """Constants for ID3v1 format."""
2
+
3
+ ID3V1_TAG_SIZE = 128
4
+ ID3V1_COMMENT_FIELD_SIZE = 30
5
+ ID3V1_TRACK_NUMBER_POSITION = 28
6
+ ID3V1_TRACK_NUMBER_VALUE_POSITION = 29
7
+ ID3V1_MIN_COMMENT_LENGTH_FOR_TRACK_NUMBER = 30
8
+ ID3V1_MIN_COMMENT_LENGTH_TO_CHECK_TRACK_NUMBER = 2