iOpenPod 2.0.4__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 (681) hide show
  1. device_registry/__init__.py +68 -0
  2. device_registry/authority.py +671 -0
  3. device_registry/data/__init__.py +1 -0
  4. device_registry/data/catalog.py +1115 -0
  5. device_registry/identifiers.py +44 -0
  6. device_registry/metadata.py +352 -0
  7. device_registry/models.py +403 -0
  8. device_registry/py.typed +1 -0
  9. device_registry/registry.py +389 -0
  10. iOpenPod/GUI/PROTOTYPE.md +218 -0
  11. iOpenPod/GUI/delegates/album_card_delegate.py +155 -0
  12. iOpenPod/GUI/delegates/collection_card_delegate.py +124 -0
  13. iOpenPod/GUI/delegates/collection_list_delegate.py +172 -0
  14. iOpenPod/GUI/delegates/photo_album_membership_delegate.py +168 -0
  15. iOpenPod/GUI/delegates/photo_card_delegate.py +142 -0
  16. iOpenPod/GUI/delegates/podcast_delegates.py +726 -0
  17. iOpenPod/GUI/delegates/selection_group_delegate.py +326 -0
  18. iOpenPod/GUI/delegates/track_artwork_delegate.py +92 -0
  19. iOpenPod/GUI/dialogs/artwork_editor.py +816 -0
  20. iOpenPod/GUI/dialogs/device_picker.py +396 -0
  21. iOpenPod/GUI/dialogs/external_playlist_files.py +286 -0
  22. iOpenPod/GUI/dialogs/library_review.py +379 -0
  23. iOpenPod/GUI/dialogs/linux_identity_setup.py +182 -0
  24. iOpenPod/GUI/dialogs/media_folders.py +440 -0
  25. iOpenPod/GUI/dialogs/media_tools_setup.py +175 -0
  26. iOpenPod/GUI/dialogs/metadata_editor.py +819 -0
  27. iOpenPod/GUI/dialogs/photo_album_editor.py +122 -0
  28. iOpenPod/GUI/dialogs/photo_album_manager.py +143 -0
  29. iOpenPod/GUI/dialogs/photo_metadata_editor.py +365 -0
  30. iOpenPod/GUI/dialogs/playlist_editor.py +1064 -0
  31. iOpenPod/GUI/dialogs/playlist_export.py +148 -0
  32. iOpenPod/GUI/dialogs/podcast_search.py +326 -0
  33. iOpenPod/GUI/dialogs/podcast_sync_settings.py +224 -0
  34. iOpenPod/GUI/dialogs/scrobble_report.py +68 -0
  35. iOpenPod/GUI/dialogs/tag_normalizer.py +489 -0
  36. iOpenPod/GUI/host_library_browser.py +312 -0
  37. iOpenPod/GUI/main_window.py +2069 -0
  38. iOpenPod/GUI/navigation.py +113 -0
  39. iOpenPod/GUI/pages/backup_page.py +1254 -0
  40. iOpenPod/GUI/pages/collection_page.py +504 -0
  41. iOpenPod/GUI/pages/library_page.py +227 -0
  42. iOpenPod/GUI/pages/photo_page.py +753 -0
  43. iOpenPod/GUI/pages/placeholder_page.py +75 -0
  44. iOpenPod/GUI/pages/playlist_page.py +486 -0
  45. iOpenPod/GUI/pages/podcast_page.py +1049 -0
  46. iOpenPod/GUI/pages/settings_page.py +950 -0
  47. iOpenPod/GUI/pages/sync_execution_page.py +589 -0
  48. iOpenPod/GUI/pages/sync_plan_page.py +401 -0
  49. iOpenPod/GUI/pages/synesthesia_page.py +175 -0
  50. iOpenPod/GUI/pages/track_page.py +90 -0
  51. iOpenPod/GUI/presentation/application_icon.py +13 -0
  52. iOpenPod/GUI/presentation/artwork.py +128 -0
  53. iOpenPod/GUI/presentation/artwork_provider.py +317 -0
  54. iOpenPod/GUI/presentation/backup_messages.py +480 -0
  55. iOpenPod/GUI/presentation/collection_text.py +27 -0
  56. iOpenPod/GUI/presentation/device_images.py +81 -0
  57. iOpenPod/GUI/presentation/display.py +20 -0
  58. iOpenPod/GUI/presentation/eta.py +61 -0
  59. iOpenPod/GUI/presentation/i18n/manager.py +146 -0
  60. iOpenPod/GUI/presentation/i18n/text.py +155 -0
  61. iOpenPod/GUI/presentation/i18n/translations/iopenpod_de.qm +0 -0
  62. iOpenPod/GUI/presentation/i18n/translations/iopenpod_de.ts +10060 -0
  63. iOpenPod/GUI/presentation/i18n/translations/iopenpod_es.qm +0 -0
  64. iOpenPod/GUI/presentation/i18n/translations/iopenpod_es.ts +10060 -0
  65. iOpenPod/GUI/presentation/i18n/workflow.py +24 -0
  66. iOpenPod/GUI/presentation/icons.py +51 -0
  67. iOpenPod/GUI/presentation/image_color.py +136 -0
  68. iOpenPod/GUI/presentation/library_card.py +252 -0
  69. iOpenPod/GUI/presentation/middle_mouse_scrolling.py +208 -0
  70. iOpenPod/GUI/presentation/photo.py +47 -0
  71. iOpenPod/GUI/presentation/photo_provider.py +182 -0
  72. iOpenPod/GUI/presentation/podcast_artwork_provider.py +83 -0
  73. iOpenPod/GUI/presentation/podcast_collection_artwork.py +91 -0
  74. iOpenPod/GUI/presentation/selection_checkbox.py +148 -0
  75. iOpenPod/GUI/presentation/theme/backup_styles.py +84 -0
  76. iOpenPod/GUI/presentation/theme/checkbox_style.py +173 -0
  77. iOpenPod/GUI/presentation/theme/dynamic.py +187 -0
  78. iOpenPod/GUI/presentation/theme/manager.py +235 -0
  79. iOpenPod/GUI/presentation/theme/podcast_styles.py +163 -0
  80. iOpenPod/GUI/presentation/theme/stylesheet.py +2322 -0
  81. iOpenPod/GUI/presentation/theme/tokens.py +631 -0
  82. iOpenPod/GUI/presentation/track_drag_preview.py +229 -0
  83. iOpenPod/GUI/sync_workspace.py +795 -0
  84. iOpenPod/GUI/synesthesia/__init__.py +36 -0
  85. iOpenPod/GUI/synesthesia/conductor.py +677 -0
  86. iOpenPod/GUI/synesthesia/motion_phases.py +81 -0
  87. iOpenPod/GUI/synesthesia/persistent_world.py +751 -0
  88. iOpenPod/GUI/synesthesia/preview.py +76 -0
  89. iOpenPod/GUI/synesthesia/render_resolution.py +104 -0
  90. iOpenPod/GUI/synesthesia/renderer.py +1653 -0
  91. iOpenPod/GUI/synesthesia/scene_director.py +1632 -0
  92. iOpenPod/GUI/synesthesia/shader_sources/comets.frag +21 -0
  93. iOpenPod/GUI/synesthesia/shader_sources/comets.frag.qsb +0 -0
  94. iOpenPod/GUI/synesthesia/shader_sources/comets.vert +98 -0
  95. iOpenPod/GUI/synesthesia/shader_sources/comets.vert.qsb +0 -0
  96. iOpenPod/GUI/synesthesia/shader_sources/events.frag +166 -0
  97. iOpenPod/GUI/synesthesia/shader_sources/events.frag.qsb +0 -0
  98. iOpenPod/GUI/synesthesia/shader_sources/feedback.frag +144 -0
  99. iOpenPod/GUI/synesthesia/shader_sources/feedback.frag.qsb +0 -0
  100. iOpenPod/GUI/synesthesia/shader_sources/field_sim.comp +200 -0
  101. iOpenPod/GUI/synesthesia/shader_sources/field_sim.comp.qsb +0 -0
  102. iOpenPod/GUI/synesthesia/shader_sources/filaments.frag +21 -0
  103. iOpenPod/GUI/synesthesia/shader_sources/filaments.frag.qsb +0 -0
  104. iOpenPod/GUI/synesthesia/shader_sources/filaments.vert +95 -0
  105. iOpenPod/GUI/synesthesia/shader_sources/filaments.vert.qsb +0 -0
  106. iOpenPod/GUI/synesthesia/shader_sources/fullscreen.vert +13 -0
  107. iOpenPod/GUI/synesthesia/shader_sources/fullscreen.vert.qsb +0 -0
  108. iOpenPod/GUI/synesthesia/shader_sources/particles.frag +25 -0
  109. iOpenPod/GUI/synesthesia/shader_sources/particles.frag.qsb +0 -0
  110. iOpenPod/GUI/synesthesia/shader_sources/particles.vert +113 -0
  111. iOpenPod/GUI/synesthesia/shader_sources/particles.vert.qsb +0 -0
  112. iOpenPod/GUI/synesthesia/shader_sources/present.frag +95 -0
  113. iOpenPod/GUI/synesthesia/shader_sources/present.frag.qsb +0 -0
  114. iOpenPod/GUI/synesthesia/shader_sources/scenes.frag +758 -0
  115. iOpenPod/GUI/synesthesia/shader_sources/scenes.frag.qsb +0 -0
  116. iOpenPod/GUI/synesthesia/visual_style.py +129 -0
  117. iOpenPod/GUI/widgets/album_grid.py +86 -0
  118. iOpenPod/GUI/widgets/app_combo_box.py +158 -0
  119. iOpenPod/GUI/widgets/artwork_view.py +124 -0
  120. iOpenPod/GUI/widgets/browser_chrome.py +147 -0
  121. iOpenPod/GUI/widgets/collection_detail.py +321 -0
  122. iOpenPod/GUI/widgets/collection_grid.py +87 -0
  123. iOpenPod/GUI/widgets/elided_label.py +171 -0
  124. iOpenPod/GUI/widgets/equalized_grid.py +160 -0
  125. iOpenPod/GUI/widgets/eta_label.py +61 -0
  126. iOpenPod/GUI/widgets/host_media_drop.py +144 -0
  127. iOpenPod/GUI/widgets/ipod_preferences.py +96 -0
  128. iOpenPod/GUI/widgets/library_content.py +193 -0
  129. iOpenPod/GUI/widgets/library_drag_grid.py +102 -0
  130. iOpenPod/GUI/widgets/library_toolbar.py +257 -0
  131. iOpenPod/GUI/widgets/media_tools_settings.py +145 -0
  132. iOpenPod/GUI/widgets/metadata_field_editor.py +416 -0
  133. iOpenPod/GUI/widgets/photo_album_membership_grid.py +70 -0
  134. iOpenPod/GUI/widgets/photo_grid.py +131 -0
  135. iOpenPod/GUI/widgets/photo_inspector.py +612 -0
  136. iOpenPod/GUI/widgets/playback_pane.py +881 -0
  137. iOpenPod/GUI/widgets/player_bar.py +1087 -0
  138. iOpenPod/GUI/widgets/playlist_artwork_banner.py +203 -0
  139. iOpenPod/GUI/widgets/playlist_tree.py +790 -0
  140. iOpenPod/GUI/widgets/podcast_artwork.py +153 -0
  141. iOpenPod/GUI/widgets/scrobbling_settings.py +210 -0
  142. iOpenPod/GUI/widgets/search_field.py +99 -0
  143. iOpenPod/GUI/widgets/selection_grid.py +55 -0
  144. iOpenPod/GUI/widgets/selection_group_transition.py +298 -0
  145. iOpenPod/GUI/widgets/setting_group.py +75 -0
  146. iOpenPod/GUI/widgets/sidebar.py +695 -0
  147. iOpenPod/GUI/widgets/status_controls.py +94 -0
  148. iOpenPod/GUI/widgets/status_list.py +386 -0
  149. iOpenPod/GUI/widgets/sync_preparation_progress.py +130 -0
  150. iOpenPod/GUI/widgets/sync_review_group.py +285 -0
  151. iOpenPod/GUI/widgets/sync_selection_actions.py +101 -0
  152. iOpenPod/GUI/widgets/sync_settings.py +510 -0
  153. iOpenPod/GUI/widgets/sync_storage_bar.py +241 -0
  154. iOpenPod/GUI/widgets/themed_buttons.py +351 -0
  155. iOpenPod/GUI/widgets/track_actions.py +861 -0
  156. iOpenPod/GUI/widgets/track_list_header.py +214 -0
  157. iOpenPod/GUI/widgets/track_table.py +483 -0
  158. iOpenPod/__main__.py +6 -0
  159. iOpenPod/app/app.py +195 -0
  160. iOpenPod/app/artwork_controller.py +254 -0
  161. iOpenPod/app/artwork_import.py +195 -0
  162. iOpenPod/app/backup_controller.py +591 -0
  163. iOpenPod/app/backups/__init__.py +77 -0
  164. iOpenPod/app/backups/_json.py +60 -0
  165. iOpenPod/app/backups/archive.py +500 -0
  166. iOpenPod/app/backups/models.py +628 -0
  167. iOpenPod/app/backups/original_archive.py +249 -0
  168. iOpenPod/app/backups/outcomes.py +419 -0
  169. iOpenPod/app/backups/recovery.py +183 -0
  170. iOpenPod/app/backups/repository.py +2139 -0
  171. iOpenPod/app/backups/scope.py +57 -0
  172. iOpenPod/app/backups/service.py +1281 -0
  173. iOpenPod/app/chaptered_conversion.py +258 -0
  174. iOpenPod/app/chaptered_conversion_controller.py +228 -0
  175. iOpenPod/app/context.py +236 -0
  176. iOpenPod/app/core/certifi_ssl.py +9 -0
  177. iOpenPod/app/core/eta.py +340 -0
  178. iOpenPod/app/core/logging.py +63 -0
  179. iOpenPod/app/core/runtime.py +52 -0
  180. iOpenPod/app/core/settings/definitions.py +299 -0
  181. iOpenPod/app/core/settings/paths.py +43 -0
  182. iOpenPod/app/core/settings/service.py +134 -0
  183. iOpenPod/app/core/settings/stores.py +250 -0
  184. iOpenPod/app/core/settings/transcoding.py +152 -0
  185. iOpenPod/app/core/status.py +205 -0
  186. iOpenPod/app/core/version.py +14 -0
  187. iOpenPod/app/device_controller.py +562 -0
  188. iOpenPod/app/display_text.py +58 -0
  189. iOpenPod/app/export_tagging.py +301 -0
  190. iOpenPod/app/host_media_controller.py +250 -0
  191. iOpenPod/app/host_media_fingerprint.py +206 -0
  192. iOpenPod/app/host_media_folders.py +162 -0
  193. iOpenPod/app/host_media_library.py +2396 -0
  194. iOpenPod/app/host_playlists.py +232 -0
  195. iOpenPod/app/ipod_media_controller.py +146 -0
  196. iOpenPod/app/library_export.py +714 -0
  197. iOpenPod/app/library_export_controller.py +271 -0
  198. iOpenPod/app/library_sync_helper.py +1108 -0
  199. iOpenPod/app/library_workspace.py +1294 -0
  200. iOpenPod/app/library_write.py +130 -0
  201. iOpenPod/app/library_write_controller.py +608 -0
  202. iOpenPod/app/library_write_inspection.py +424 -0
  203. iOpenPod/app/lyrics_controller.py +161 -0
  204. iOpenPod/app/media/__init__.py +20 -0
  205. iOpenPod/app/media/content_type.py +42 -0
  206. iOpenPod/app/media/importing.py +252 -0
  207. iOpenPod/app/media/inspection.py +368 -0
  208. iOpenPod/app/media/lyrics.py +143 -0
  209. iOpenPod/app/media/lyrics_reader.py +96 -0
  210. iOpenPod/app/media/models.py +95 -0
  211. iOpenPod/app/media/music_paths.py +95 -0
  212. iOpenPod/app/media/photo_sync.py +266 -0
  213. iOpenPod/app/media/progress.py +23 -0
  214. iOpenPod/app/media/sync_artwork.py +58 -0
  215. iOpenPod/app/media/tags.py +429 -0
  216. iOpenPod/app/media/transcoding.py +1031 -0
  217. iOpenPod/app/media_tools_controller.py +235 -0
  218. iOpenPod/app/metadata_fields.py +212 -0
  219. iOpenPod/app/models/album_list_model.py +285 -0
  220. iOpenPod/app/models/artwork.py +49 -0
  221. iOpenPod/app/models/artwork_seed.py +13 -0
  222. iOpenPod/app/models/collection_list_model.py +332 -0
  223. iOpenPod/app/models/device.py +138 -0
  224. iOpenPod/app/models/filter_compat.py +22 -0
  225. iOpenPod/app/models/ipod_preferences.py +27 -0
  226. iOpenPod/app/models/keyed_projection.py +101 -0
  227. iOpenPod/app/models/library_drag.py +141 -0
  228. iOpenPod/app/models/library_filter_models.py +425 -0
  229. iOpenPod/app/models/photo_album_membership_model.py +189 -0
  230. iOpenPod/app/models/photo_list_model.py +554 -0
  231. iOpenPod/app/models/photos.py +62 -0
  232. iOpenPod/app/models/playback_models.py +525 -0
  233. iOpenPod/app/models/playlist_tree_model.py +295 -0
  234. iOpenPod/app/models/podcast_list_models.py +355 -0
  235. iOpenPod/app/models/selection_grouping.py +323 -0
  236. iOpenPod/app/models/sync_plan_table_model.py +383 -0
  237. iOpenPod/app/models/sync_selection.py +274 -0
  238. iOpenPod/app/models/track_columns.py +982 -0
  239. iOpenPod/app/models/track_order.py +19 -0
  240. iOpenPod/app/models/track_table_model.py +330 -0
  241. iOpenPod/app/music_import_controller.py +205 -0
  242. iOpenPod/app/photo_controller.py +350 -0
  243. iOpenPod/app/playback/__init__.py +21 -0
  244. iOpenPod/app/playback/_qt_io.py +54 -0
  245. iOpenPod/app/playback/_qt_logging.py +101 -0
  246. iOpenPod/app/playback/backend.py +92 -0
  247. iOpenPod/app/playback/qt_backend.py +224 -0
  248. iOpenPod/app/playback/runtime_check.py +82 -0
  249. iOpenPod/app/playback/system_media/__init__.py +25 -0
  250. iOpenPod/app/playback/system_media/bridge.py +284 -0
  251. iOpenPod/app/playback/system_media/factory.py +66 -0
  252. iOpenPod/app/playback/system_media/linux.py +699 -0
  253. iOpenPod/app/playback/system_media/macos.py +430 -0
  254. iOpenPod/app/playback/system_media/session.py +145 -0
  255. iOpenPod/app/playback/system_media/windows.py +554 -0
  256. iOpenPod/app/playback_controller.py +352 -0
  257. iOpenPod/app/podcasts/__init__.py +59 -0
  258. iOpenPod/app/podcasts/artwork_controller.py +215 -0
  259. iOpenPod/app/podcasts/catalog.py +630 -0
  260. iOpenPod/app/podcasts/controller.py +610 -0
  261. iOpenPod/app/podcasts/documents.py +292 -0
  262. iOpenPod/app/podcasts/feed_client.py +460 -0
  263. iOpenPod/app/podcasts/identity.py +115 -0
  264. iOpenPod/app/podcasts/media.py +151 -0
  265. iOpenPod/app/podcasts/models.py +294 -0
  266. iOpenPod/app/podcasts/store.py +236 -0
  267. iOpenPod/app/podcasts/sync.py +459 -0
  268. iOpenPod/app/podcasts/sync_artwork.py +98 -0
  269. iOpenPod/app/podcasts/sync_preparation.py +172 -0
  270. iOpenPod/app/scrobbling/__init__.py +1 -0
  271. iOpenPod/app/scrobbling/_json.py +25 -0
  272. iOpenPod/app/scrobbling/clients.py +373 -0
  273. iOpenPod/app/scrobbling/controller.py +304 -0
  274. iOpenPod/app/scrobbling/credentials.py +98 -0
  275. iOpenPod/app/scrobbling/models.py +80 -0
  276. iOpenPod/app/scrobbling/queue.py +219 -0
  277. iOpenPod/app/scrobbling/reporting.py +132 -0
  278. iOpenPod/app/scrobbling/service.py +191 -0
  279. iOpenPod/app/scrobbling/settings.py +22 -0
  280. iOpenPod/app/scrobbling/timestamps.py +56 -0
  281. iOpenPod/app/services/device_coordinator.py +4175 -0
  282. iOpenPod/app/services/device_time.py +42 -0
  283. iOpenPod/app/services/ipod_preferences.py +359 -0
  284. iOpenPod/app/services/library_resources.py +677 -0
  285. iOpenPod/app/services/linux_identity.py +259 -0
  286. iOpenPod/app/services/media_tools.py +273 -0
  287. iOpenPod/app/services/volume_presentation.py +206 -0
  288. iOpenPod/app/smart_playlist_preview.py +264 -0
  289. iOpenPod/app/sync_controller.py +677 -0
  290. iOpenPod/app/sync_execution.py +2319 -0
  291. iOpenPod/app/sync_plan.py +680 -0
  292. iOpenPod/app/sync_storage.py +131 -0
  293. iOpenPod/app/synesthesia/__init__.py +143 -0
  294. iOpenPod/app/synesthesia/_rhythm.py +167 -0
  295. iOpenPod/app/synesthesia/_signals.py +226 -0
  296. iOpenPod/app/synesthesia/_structure.py +229 -0
  297. iOpenPod/app/synesthesia/audio.py +227 -0
  298. iOpenPod/app/synesthesia/backend.py +202 -0
  299. iOpenPod/app/synesthesia/controller.py +356 -0
  300. iOpenPod/app/synesthesia/dsp.py +882 -0
  301. iOpenPod/app/synesthesia/enrichment.py +407 -0
  302. iOpenPod/app/synesthesia/models.py +796 -0
  303. iOpenPod/app/tag_normalization_controller.py +197 -0
  304. iOpenPod/app/tag_normalizer.py +314 -0
  305. iOpenPod/app/track_conversion.py +110 -0
  306. iOpenPod/app/track_playback_policy.py +60 -0
  307. iOpenPod/app/updates/__init__.py +1 -0
  308. iOpenPod/app/updates/backend.py +97 -0
  309. iOpenPod/app/updates/bootstrap.py +116 -0
  310. iOpenPod/app/updates/controller.py +386 -0
  311. iOpenPod/app/updates/github.py +222 -0
  312. iOpenPod/app/updates/helper.py +228 -0
  313. iOpenPod/app/updates/installation.py +94 -0
  314. iOpenPod/app/updates/platform.py +156 -0
  315. iOpenPod/app/updates/portable.py +430 -0
  316. iOpenPod/app/updates/processes.py +127 -0
  317. iOpenPod/app/updates/processes_windows.py +229 -0
  318. iOpenPod/app/updates/pypi.py +288 -0
  319. iOpenPod/app/updates/python_helper.py +352 -0
  320. iOpenPod/app/updates/python_installation.py +420 -0
  321. iOpenPod/app/updates/releases.py +276 -0
  322. iOpenPod/app/updates/sparkle.py +111 -0
  323. iOpenPod/app/updates/transport.py +183 -0
  324. iOpenPod/app/updates/windows.py +252 -0
  325. iOpenPod/assets/glyphs/README.md +6 -0
  326. iOpenPod/assets/glyphs/album.svg +19 -0
  327. iOpenPod/assets/glyphs/alert.svg +3 -0
  328. iOpenPod/assets/glyphs/bell.svg +4 -0
  329. iOpenPod/assets/glyphs/book.svg +4 -0
  330. iOpenPod/assets/glyphs/box.svg +3 -0
  331. iOpenPod/assets/glyphs/broadcast.svg +8 -0
  332. iOpenPod/assets/glyphs/check-circle.svg +4 -0
  333. iOpenPod/assets/glyphs/chevron-left.svg +1 -0
  334. iOpenPod/assets/glyphs/eject.svg +3 -0
  335. iOpenPod/assets/glyphs/film.svg +4 -0
  336. iOpenPod/assets/glyphs/folder.svg +3 -0
  337. iOpenPod/assets/glyphs/grid.svg +6 -0
  338. iOpenPod/assets/glyphs/list.svg +4 -0
  339. iOpenPod/assets/glyphs/minus.svg +3 -0
  340. iOpenPod/assets/glyphs/monitor.svg +4 -0
  341. iOpenPod/assets/glyphs/music.svg +5 -0
  342. iOpenPod/assets/glyphs/pause.svg +4 -0
  343. iOpenPod/assets/glyphs/photo.svg +3 -0
  344. iOpenPod/assets/glyphs/play-last.svg +6 -0
  345. iOpenPod/assets/glyphs/play-next.svg +6 -0
  346. iOpenPod/assets/glyphs/play.svg +3 -0
  347. iOpenPod/assets/glyphs/playlist.svg +7 -0
  348. iOpenPod/assets/glyphs/plus.svg +3 -0
  349. iOpenPod/assets/glyphs/refresh.svg +3 -0
  350. iOpenPod/assets/glyphs/search.svg +3 -0
  351. iOpenPod/assets/glyphs/settings-sliders.svg +5 -0
  352. iOpenPod/assets/glyphs/skip-back.svg +4 -0
  353. iOpenPod/assets/glyphs/skip-forward.svg +4 -0
  354. iOpenPod/assets/glyphs/smart-playlist.svg +5 -0
  355. iOpenPod/assets/glyphs/synesthesia.svg +8 -0
  356. iOpenPod/assets/glyphs/user.svg +4 -0
  357. iOpenPod/assets/glyphs/video.svg +4 -0
  358. iOpenPod/assets/glyphs/volume.svg +3 -0
  359. iOpenPod/assets/glyphs/x.svg +3 -0
  360. iOpenPod/assets/icons/DJShott-icon.txt +21 -0
  361. iOpenPod/assets/icons/README.md +13 -0
  362. iOpenPod/assets/icons/icon-128.png +0 -0
  363. iOpenPod/assets/icons/icon-16.png +0 -0
  364. iOpenPod/assets/icons/icon-24.png +0 -0
  365. iOpenPod/assets/icons/icon-256.png +0 -0
  366. iOpenPod/assets/icons/icon-32.png +0 -0
  367. iOpenPod/assets/icons/icon-48.png +0 -0
  368. iOpenPod/assets/icons/icon-64.png +0 -0
  369. iOpenPod/assets/icons/icon.ico +0 -0
  370. iOpenPod/assets/ipod_images/iPod1.png +0 -0
  371. iOpenPod/assets/ipod_images/iPod11-Black.png +0 -0
  372. iOpenPod/assets/ipod_images/iPod11-Silver.png +0 -0
  373. iOpenPod/assets/ipod_images/iPod11B-Black.png +0 -0
  374. iOpenPod/assets/ipod_images/iPod12-Black.png +0 -0
  375. iOpenPod/assets/ipod_images/iPod12-Blue.png +0 -0
  376. iOpenPod/assets/ipod_images/iPod12-Green.png +0 -0
  377. iOpenPod/assets/ipod_images/iPod12-Pink.png +0 -0
  378. iOpenPod/assets/ipod_images/iPod12-Red.png +0 -0
  379. iOpenPod/assets/ipod_images/iPod12-Silver.png +0 -0
  380. iOpenPod/assets/ipod_images/iPod15-Black.png +0 -0
  381. iOpenPod/assets/ipod_images/iPod15-Blue.png +0 -0
  382. iOpenPod/assets/ipod_images/iPod15-Green.png +0 -0
  383. iOpenPod/assets/ipod_images/iPod15-Orange.png +0 -0
  384. iOpenPod/assets/ipod_images/iPod15-Pink.png +0 -0
  385. iOpenPod/assets/ipod_images/iPod15-Purple.png +0 -0
  386. iOpenPod/assets/ipod_images/iPod15-Red.png +0 -0
  387. iOpenPod/assets/ipod_images/iPod15-Silver.png +0 -0
  388. iOpenPod/assets/ipod_images/iPod15-Yellow.png +0 -0
  389. iOpenPod/assets/ipod_images/iPod16-Black.png +0 -0
  390. iOpenPod/assets/ipod_images/iPod16-Blue.png +0 -0
  391. iOpenPod/assets/ipod_images/iPod16-Green.png +0 -0
  392. iOpenPod/assets/ipod_images/iPod16-Orange.png +0 -0
  393. iOpenPod/assets/ipod_images/iPod16-Pink.png +0 -0
  394. iOpenPod/assets/ipod_images/iPod16-Purple.png +0 -0
  395. iOpenPod/assets/ipod_images/iPod16-Red.png +0 -0
  396. iOpenPod/assets/ipod_images/iPod16-Silver.png +0 -0
  397. iOpenPod/assets/ipod_images/iPod16-Yellow.png +0 -0
  398. iOpenPod/assets/ipod_images/iPod17-Blue.png +0 -0
  399. iOpenPod/assets/ipod_images/iPod17-DarkGray.png +0 -0
  400. iOpenPod/assets/ipod_images/iPod17-Green.png +0 -0
  401. iOpenPod/assets/ipod_images/iPod17-Orange.png +0 -0
  402. iOpenPod/assets/ipod_images/iPod17-Pink.png +0 -0
  403. iOpenPod/assets/ipod_images/iPod17-Red.png +0 -0
  404. iOpenPod/assets/ipod_images/iPod17-Silver.png +0 -0
  405. iOpenPod/assets/ipod_images/iPod18-Blue.png +0 -0
  406. iOpenPod/assets/ipod_images/iPod18-DarkGray.png +0 -0
  407. iOpenPod/assets/ipod_images/iPod18-Green.png +0 -0
  408. iOpenPod/assets/ipod_images/iPod18-Pink.png +0 -0
  409. iOpenPod/assets/ipod_images/iPod18-Purple.png +0 -0
  410. iOpenPod/assets/ipod_images/iPod18-Red.png +0 -0
  411. iOpenPod/assets/ipod_images/iPod18-Silver.png +0 -0
  412. iOpenPod/assets/ipod_images/iPod18-SpaceGray.png +0 -0
  413. iOpenPod/assets/ipod_images/iPod18-Yellow.png +0 -0
  414. iOpenPod/assets/ipod_images/iPod18A-Blue.png +0 -0
  415. iOpenPod/assets/ipod_images/iPod18A-Gold.png +0 -0
  416. iOpenPod/assets/ipod_images/iPod18A-Pink.png +0 -0
  417. iOpenPod/assets/ipod_images/iPod18A-Red.png +0 -0
  418. iOpenPod/assets/ipod_images/iPod18A-Silver.png +0 -0
  419. iOpenPod/assets/ipod_images/iPod18A-SpaceGray.png +0 -0
  420. iOpenPod/assets/ipod_images/iPod2.png +0 -0
  421. iOpenPod/assets/ipod_images/iPod3-Blue.png +0 -0
  422. iOpenPod/assets/ipod_images/iPod3-Gold.png +0 -0
  423. iOpenPod/assets/ipod_images/iPod3-Green.png +0 -0
  424. iOpenPod/assets/ipod_images/iPod3-Pink.png +0 -0
  425. iOpenPod/assets/ipod_images/iPod3-Silver.png +0 -0
  426. iOpenPod/assets/ipod_images/iPod3B-Blue.png +0 -0
  427. iOpenPod/assets/ipod_images/iPod3B-Green.png +0 -0
  428. iOpenPod/assets/ipod_images/iPod3B-Pink.png +0 -0
  429. iOpenPod/assets/ipod_images/iPod4-BlackRed.png +0 -0
  430. iOpenPod/assets/ipod_images/iPod4-White.png +0 -0
  431. iOpenPod/assets/ipod_images/iPod5-BlackRed.png +0 -0
  432. iOpenPod/assets/ipod_images/iPod5-White.png +0 -0
  433. iOpenPod/assets/ipod_images/iPod6-Black.png +0 -0
  434. iOpenPod/assets/ipod_images/iPod6-BlackRed.png +0 -0
  435. iOpenPod/assets/ipod_images/iPod6-White.png +0 -0
  436. iOpenPod/assets/ipod_images/iPod7-Black.png +0 -0
  437. iOpenPod/assets/ipod_images/iPod7-White.png +0 -0
  438. iOpenPod/assets/ipod_images/iPod9-Black.png +0 -0
  439. iOpenPod/assets/ipod_images/iPod9-Blue.png +0 -0
  440. iOpenPod/assets/ipod_images/iPod9-Green.png +0 -0
  441. iOpenPod/assets/ipod_images/iPod9-Pink.png +0 -0
  442. iOpenPod/assets/ipod_images/iPod9-Red.png +0 -0
  443. iOpenPod/assets/ipod_images/iPod9-Silver.png +0 -0
  444. iOpenPod/assets/ipod_images/iPodGeneric.png +0 -0
  445. iOpenPod/assets/linux/61-iopenpod.rules +24 -0
  446. iPodDB/ArtworkDB/builder/build_ArtworkDB.py +180 -0
  447. iPodDB/ArtworkDB/ithmb.py +453 -0
  448. iPodDB/ArtworkDB/ithmb_writer.py +170 -0
  449. iPodDB/ArtworkDB/parser/mhod_parsers/container_mhod.py +38 -0
  450. iPodDB/ArtworkDB/parser/mhod_parsers/opaque_mhod.py +12 -0
  451. iPodDB/ArtworkDB/parser/mhod_parsers/string_mhod.py +33 -0
  452. iPodDB/ArtworkDB/parser/mhod_payload_spec_registry.py +75 -0
  453. iPodDB/ArtworkDB/parser/parse_ArtworkDB.py +14 -0
  454. iPodDB/ArtworkDB/parser/parser_definition.py +31 -0
  455. iPodDB/ArtworkDB/shared/artwork_index.py +163 -0
  456. iPodDB/ArtworkDB/shared/chunk_defs/mhaf.py +23 -0
  457. iPodDB/ArtworkDB/shared/chunk_defs/mhba.py +44 -0
  458. iPodDB/ArtworkDB/shared/chunk_defs/mhfd.py +36 -0
  459. iPodDB/ArtworkDB/shared/chunk_defs/mhia.py +24 -0
  460. iPodDB/ArtworkDB/shared/chunk_defs/mhif.py +27 -0
  461. iPodDB/ArtworkDB/shared/chunk_defs/mhii.py +34 -0
  462. iPodDB/ArtworkDB/shared/chunk_defs/mhla.py +16 -0
  463. iPodDB/ArtworkDB/shared/chunk_defs/mhlf.py +16 -0
  464. iPodDB/ArtworkDB/shared/chunk_defs/mhli.py +16 -0
  465. iPodDB/ArtworkDB/shared/chunk_defs/mhni.py +34 -0
  466. iPodDB/ArtworkDB/shared/chunk_defs/mhod.py +27 -0
  467. iPodDB/ArtworkDB/shared/chunk_defs/mhod_payloads/container_mhod.py +12 -0
  468. iPodDB/ArtworkDB/shared/chunk_defs/mhod_payloads/opaque_mhod.py +14 -0
  469. iPodDB/ArtworkDB/shared/chunk_defs/mhod_payloads/string_mhod.py +43 -0
  470. iPodDB/ArtworkDB/shared/chunk_defs/mhsd.py +51 -0
  471. iPodDB/ArtworkDB/shared/constants.py +24 -0
  472. iPodDB/ArtworkDB/shared/database_definition.py +36 -0
  473. iPodDB/ArtworkDB/shared/mhod_payload_spec_registry.py +88 -0
  474. iPodDB/ArtworkDB/writer/mhod_encoder.py +172 -0
  475. iPodDB/ArtworkDB/writer/mhod_serializer.py +72 -0
  476. iPodDB/ArtworkDB/writer/write_ArtworkDB.py +24 -0
  477. iPodDB/PhotosDB/builder/build_PhotosDB.py +181 -0
  478. iPodDB/PhotosDB/parser/mhod_parsers/container_mhod.py +38 -0
  479. iPodDB/PhotosDB/parser/mhod_parsers/opaque_mhod.py +12 -0
  480. iPodDB/PhotosDB/parser/mhod_parsers/string_mhod.py +33 -0
  481. iPodDB/PhotosDB/parser/mhod_payload_spec_registry.py +75 -0
  482. iPodDB/PhotosDB/parser/parse_PhotosDB.py +14 -0
  483. iPodDB/PhotosDB/parser/parser_definition.py +29 -0
  484. iPodDB/PhotosDB/shared/chunk_defs/mhaf.py +17 -0
  485. iPodDB/PhotosDB/shared/chunk_defs/mhba.py +22 -0
  486. iPodDB/PhotosDB/shared/chunk_defs/mhfd.py +38 -0
  487. iPodDB/PhotosDB/shared/chunk_defs/mhia.py +17 -0
  488. iPodDB/PhotosDB/shared/chunk_defs/mhif.py +18 -0
  489. iPodDB/PhotosDB/shared/chunk_defs/mhii.py +19 -0
  490. iPodDB/PhotosDB/shared/chunk_defs/mhla.py +16 -0
  491. iPodDB/PhotosDB/shared/chunk_defs/mhlf.py +16 -0
  492. iPodDB/PhotosDB/shared/chunk_defs/mhli.py +16 -0
  493. iPodDB/PhotosDB/shared/chunk_defs/mhni.py +18 -0
  494. iPodDB/PhotosDB/shared/chunk_defs/mhod.py +17 -0
  495. iPodDB/PhotosDB/shared/chunk_defs/mhod_payloads/container_mhod.py +7 -0
  496. iPodDB/PhotosDB/shared/chunk_defs/mhod_payloads/opaque_mhod.py +8 -0
  497. iPodDB/PhotosDB/shared/chunk_defs/mhod_payloads/string_mhod.py +36 -0
  498. iPodDB/PhotosDB/shared/chunk_defs/mhsd.py +43 -0
  499. iPodDB/PhotosDB/shared/constants.py +31 -0
  500. iPodDB/PhotosDB/shared/database_definition.py +36 -0
  501. iPodDB/PhotosDB/shared/mhod_payload_spec_registry.py +21 -0
  502. iPodDB/PhotosDB/writer/write_PhotosDB.py +22 -0
  503. iPodDB/SQLiteDB/artifacts.py +27 -0
  504. iPodDB/SQLiteDB/checksum.py +68 -0
  505. iPodDB/SQLiteDB/database.py +1089 -0
  506. iPodDB/device_time.py +143 -0
  507. iPodDB/iTunesDB/builder/build_iTunesDB.py +94 -0
  508. iPodDB/iTunesDB/cdb.py +136 -0
  509. iPodDB/iTunesDB/parser/mhod_parsers/chapter_data_mhod.py +327 -0
  510. iPodDB/iTunesDB/parser/mhod_parsers/contextual_100_mhod.py +17 -0
  511. iPodDB/iTunesDB/parser/mhod_parsers/library_index_mhod.py +31 -0
  512. iPodDB/iTunesDB/parser/mhod_parsers/library_jump_table_mhod.py +36 -0
  513. iPodDB/iTunesDB/parser/mhod_parsers/opaque_mhod.py +14 -0
  514. iPodDB/iTunesDB/parser/mhod_parsers/plist_mhod.py +52 -0
  515. iPodDB/iTunesDB/parser/mhod_parsers/settings_mhod.py +28 -0
  516. iPodDB/iTunesDB/parser/mhod_parsers/smart_prefs_mhod.py +17 -0
  517. iPodDB/iTunesDB/parser/mhod_parsers/smart_rules_mhod.py +180 -0
  518. iPodDB/iTunesDB/parser/mhod_parsers/string_mhod.py +40 -0
  519. iPodDB/iTunesDB/parser/mhod_parsers/url_mhod.py +18 -0
  520. iPodDB/iTunesDB/parser/mhod_parsers/video_details_mhod.py +16 -0
  521. iPodDB/iTunesDB/parser/mhod_payload_spec_registry.py +128 -0
  522. iPodDB/iTunesDB/parser/parse_iTunesDB.py +23 -0
  523. iPodDB/iTunesDB/parser/parser_definition.py +29 -0
  524. iPodDB/iTunesDB/shared/chunk_defs/mhbd.py +47 -0
  525. iPodDB/iTunesDB/shared/chunk_defs/mhia.py +33 -0
  526. iPodDB/iTunesDB/shared/chunk_defs/mhii.py +28 -0
  527. iPodDB/iTunesDB/shared/chunk_defs/mhip.py +34 -0
  528. iPodDB/iTunesDB/shared/chunk_defs/mhit.py +131 -0
  529. iPodDB/iTunesDB/shared/chunk_defs/mhla.py +16 -0
  530. iPodDB/iTunesDB/shared/chunk_defs/mhli.py +16 -0
  531. iPodDB/iTunesDB/shared/chunk_defs/mhlp.py +16 -0
  532. iPodDB/iTunesDB/shared/chunk_defs/mhlt.py +16 -0
  533. iPodDB/iTunesDB/shared/chunk_defs/mhod.py +26 -0
  534. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/chapter_data_mhod.py +93 -0
  535. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/contextual_100_mhod.py +28 -0
  536. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/library_index_mhod.py +19 -0
  537. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/library_jump_table_mhod.py +27 -0
  538. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/opaque_mhod.py +10 -0
  539. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/plist_mhod.py +52 -0
  540. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/settings_mhod.py +18 -0
  541. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/smart_prefs_mhod.py +28 -0
  542. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/smart_rules_mhod.py +173 -0
  543. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/string_mhod.py +19 -0
  544. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/url_mhod.py +10 -0
  545. iPodDB/iTunesDB/shared/chunk_defs/mhod_payloads/video_details_mhod.py +13 -0
  546. iPodDB/iTunesDB/shared/chunk_defs/mhsd.py +44 -0
  547. iPodDB/iTunesDB/shared/chunk_defs/mhyp.py +50 -0
  548. iPodDB/iTunesDB/shared/constants.py +205 -0
  549. iPodDB/iTunesDB/shared/database_definition.py +36 -0
  550. iPodDB/iTunesDB/shared/device_time.py +39 -0
  551. iPodDB/iTunesDB/shared/field_converters.py +22 -0
  552. iPodDB/iTunesDB/shared/mhod_payload_spec_registry.py +165 -0
  553. iPodDB/iTunesDB/writer/calcHashAB.NOTICE +20 -0
  554. iPodDB/iTunesDB/writer/calcHashAB.wasm.b64 +150 -0
  555. iPodDB/iTunesDB/writer/mhod_encoder.py +607 -0
  556. iPodDB/iTunesDB/writer/signature.py +291 -0
  557. iPodDB/iTunesDB/writer/write_iTunesDB.py +67 -0
  558. iPodDB/library/__init__.py +218 -0
  559. iPodDB/library/_album_index.py +375 -0
  560. iPodDB/library/_artwork_verification.py +171 -0
  561. iPodDB/library/_artwork_writing.py +652 -0
  562. iPodDB/library/_browse_groups.py +68 -0
  563. iPodDB/library/_browse_index.py +103 -0
  564. iPodDB/library/_document_edit.py +23 -0
  565. iPodDB/library/_field_policy.py +387 -0
  566. iPodDB/library/_folder_validation.py +78 -0
  567. iPodDB/library/_folder_writing.py +163 -0
  568. iPodDB/library/_group_verification.py +305 -0
  569. iPodDB/library/_hidden_writing.py +461 -0
  570. iPodDB/library/_hierarchy.py +36 -0
  571. iPodDB/library/_identities.py +40 -0
  572. iPodDB/library/_index_verification.py +126 -0
  573. iPodDB/library/_lyrics_resources.py +71 -0
  574. iPodDB/library/_native_values.py +72 -0
  575. iPodDB/library/_photo_analysis.py +575 -0
  576. iPodDB/library/_photo_projection.py +178 -0
  577. iPodDB/library/_photo_resources.py +238 -0
  578. iPodDB/library/_photo_writing.py +376 -0
  579. iPodDB/library/_playlist_datasets.py +263 -0
  580. iPodDB/library/_playlist_projection.py +207 -0
  581. iPodDB/library/_playlist_verification.py +622 -0
  582. iPodDB/library/_playlist_writing.py +244 -0
  583. iPodDB/library/_podcast_writing.py +272 -0
  584. iPodDB/library/_projection.py +422 -0
  585. iPodDB/library/_reconcile.py +597 -0
  586. iPodDB/library/_resolve_write.py +360 -0
  587. iPodDB/library/_resolved_write.py +36 -0
  588. iPodDB/library/_sidecar_projection.py +168 -0
  589. iPodDB/library/_smart_projection.py +332 -0
  590. iPodDB/library/_smart_writing.py +321 -0
  591. iPodDB/library/_source_warnings.py +87 -0
  592. iPodDB/library/_time_warnings.py +83 -0
  593. iPodDB/library/_track_header_verification.py +168 -0
  594. iPodDB/library/_track_verification.py +206 -0
  595. iPodDB/library/_track_writing.py +467 -0
  596. iPodDB/library/_write_analysis.py +415 -0
  597. iPodDB/library/_write_effects.py +363 -0
  598. iPodDB/library/_write_logging.py +201 -0
  599. iPodDB/library/_write_preparation.py +1052 -0
  600. iPodDB/library/_write_verification.py +147 -0
  601. iPodDB/library/artwork.py +112 -0
  602. iPodDB/library/database.py +414 -0
  603. iPodDB/library/editing.py +114 -0
  604. iPodDB/library/media.py +48 -0
  605. iPodDB/library/models.py +254 -0
  606. iPodDB/library/photos.py +285 -0
  607. iPodDB/library/playlists.py +437 -0
  608. iPodDB/library/signing.py +64 -0
  609. iPodDB/library/smart_rules.py +236 -0
  610. iPodDB/library/sqlite.py +5 -0
  611. iPodDB/library/writing.py +305 -0
  612. iPodDB/preferences/__init__.py +58 -0
  613. iPodDB/preferences/_source.py +9 -0
  614. iPodDB/preferences/codec.py +175 -0
  615. iPodDB/preferences/definitions.py +104 -0
  616. iPodDB/preferences/itunes.py +145 -0
  617. iPodDB/preferences/itunes_definitions.py +93 -0
  618. iPodDB/preferences/models.py +81 -0
  619. iPodDB/preferences/timezones.py +220 -0
  620. iPodDB/py.typed +0 -0
  621. iPodDB/shared/binary_struct.py +271 -0
  622. iPodDB/shared/chunk.py +408 -0
  623. iPodDB/shared/chunk_builder.py +46 -0
  624. iPodDB/shared/chunk_field.py +178 -0
  625. iPodDB/shared/chunk_reader.py +433 -0
  626. iPodDB/shared/chunk_writer.py +325 -0
  627. iPodDB/shared/errors.py +42 -0
  628. iPodDB/shared/measure_memory.py +107 -0
  629. iPodDB/shared/to_json.py +91 -0
  630. iPodDB/shared/types.py +377 -0
  631. iPodDB/sidecars/__init__.py +41 -0
  632. iPodDB/sidecars/codec.py +225 -0
  633. iPodDB/sidecars/models.py +63 -0
  634. iPodDB/sidecars/remapping.py +64 -0
  635. iPodDB/tools/chunk_browser.py +1051 -0
  636. iopenpod-2.0.4.dist-info/METADATA +327 -0
  637. iopenpod-2.0.4.dist-info/RECORD +681 -0
  638. iopenpod-2.0.4.dist-info/WHEEL +4 -0
  639. iopenpod-2.0.4.dist-info/entry_points.txt +2 -0
  640. iopenpod-2.0.4.dist-info/licenses/ACKNOWLEDGEMENTS.md +44 -0
  641. iopenpod-2.0.4.dist-info/licenses/COPYING.md +47 -0
  642. iopenpod-2.0.4.dist-info/licenses/LICENSE +675 -0
  643. iopenpod_launcher.py +283 -0
  644. storage/__init__.py +177 -0
  645. storage/_filesystem.py +238 -0
  646. storage/_transaction_journal.py +242 -0
  647. storage/_transactions.py +1426 -0
  648. storage/_writer_lock.py +189 -0
  649. storage/device_capture.py +39 -0
  650. storage/errors.py +111 -0
  651. storage/host_capture.py +139 -0
  652. storage/host_directory.py +242 -0
  653. storage/host_files.py +221 -0
  654. storage/host_input.py +417 -0
  655. storage/host_installation.py +147 -0
  656. storage/host_tools.py +124 -0
  657. storage/host_usage.py +119 -0
  658. storage/media_probe.py +161 -0
  659. storage/media_processing.py +527 -0
  660. storage/models.py +364 -0
  661. storage/paths.py +97 -0
  662. storage/platform/__init__.py +1 -0
  663. storage/platform/base.py +58 -0
  664. storage/platform/common.py +88 -0
  665. storage/platform/host_install_windows.py +201 -0
  666. storage/platform/linux.py +619 -0
  667. storage/platform/linux_eject.py +392 -0
  668. storage/platform/macos.py +974 -0
  669. storage/platform/macos_eject.py +90 -0
  670. storage/platform/macos_volume_metadata.py +116 -0
  671. storage/platform/macos_vpd.py +546 -0
  672. storage/platform/native.py +29 -0
  673. storage/platform/windows.py +667 -0
  674. storage/platform/windows_eject.py +386 -0
  675. storage/portal.py +403 -0
  676. storage/py.typed +1 -0
  677. storage/scsi_vpd.py +127 -0
  678. storage/session.py +1626 -0
  679. storage/testing.py +282 -0
  680. storage/transactions.py +209 -0
  681. storage/volume_metadata.py +40 -0
@@ -0,0 +1,622 @@
1
+ """Check reparsed playlist artifacts, including structures hidden by the projection."""
2
+
3
+ from collections import Counter
4
+ from dataclasses import replace
5
+
6
+ from iPodDB.device_time import TimeConversion
7
+ from iPodDB.iTunesDB.shared.chunk_defs.mhbd import MhbdHeader
8
+ from iPodDB.iTunesDB.shared.chunk_defs.mhip import MhipHeader
9
+ from iPodDB.iTunesDB.shared.chunk_defs.mhod import MhodHeader
10
+ from iPodDB.iTunesDB.shared.chunk_defs.mhod_payloads.contextual_100_mhod import (
11
+ MhodPlaylistPositionPrefix,
12
+ )
13
+ from iPodDB.iTunesDB.shared.chunk_defs.mhod_payloads.library_index_mhod import (
14
+ MhodLibraryIndexPayload,
15
+ MhodLibraryIndexPrefix,
16
+ )
17
+ from iPodDB.iTunesDB.shared.chunk_defs.mhod_payloads.library_jump_table_mhod import (
18
+ MhodLibraryJumpTablePayload,
19
+ MhodLibraryJumpTablePrefix,
20
+ )
21
+ from iPodDB.iTunesDB.shared.chunk_defs.mhod_payloads.smart_rules_mhod import (
22
+ MhodSmartNumericRuleData,
23
+ MhodSmartRulesPayload,
24
+ MhodSmartRulesPrefix,
25
+ )
26
+ from iPodDB.iTunesDB.shared.chunk_defs.mhod_payloads.string_mhod import (
27
+ MhodStringPayload,
28
+ )
29
+ from iPodDB.iTunesDB.shared.chunk_defs.mhsd import MhsdHeader
30
+ from iPodDB.iTunesDB.shared.chunk_defs.mhyp import MhypHeader
31
+ from iPodDB.library._album_index import master_tracks, verify_album_index
32
+ from iPodDB.library._hierarchy import folder_hierarchy
33
+ from iPodDB.library._index_verification import verify_index
34
+ from iPodDB.library._playlist_datasets import (
35
+ affected_datasets,
36
+ is_master,
37
+ is_podcasts,
38
+ is_visible,
39
+ playlist_rows,
40
+ structure_issues,
41
+ )
42
+ from iPodDB.library._playlist_projection import project_playlist
43
+ from iPodDB.library._projection import project_tracks
44
+ from iPodDB.library.models import LibrarySnapshot
45
+ from iPodDB.library.writing import IdentityMapping, IssueSeverity, WriteIssue
46
+ from iPodDB.shared.chunk import ChunkHeader, DatabaseDocument, ParsedChunk
47
+
48
+
49
+ def _bytes(chunk: ParsedChunk[ChunkHeader], data: bytes) -> bytes:
50
+ return data[
51
+ chunk.offset : chunk.offset + chunk.generic_header.length_or_child_count
52
+ ]
53
+
54
+
55
+ def verify_playlists(
56
+ original: DatabaseDocument[MhbdHeader],
57
+ checked: DatabaseDocument[MhbdHeader],
58
+ before: LibrarySnapshot,
59
+ desired: LibrarySnapshot,
60
+ mappings: tuple[IdentityMapping, ...],
61
+ original_bytes: bytes,
62
+ output_bytes: bytes,
63
+ generated_podcasts: frozenset[int] = frozenset(),
64
+ device_time: TimeConversion = 0,
65
+ ) -> tuple[WriteIssue, ...]:
66
+ issues = list(
67
+ structure_issues(
68
+ checked,
69
+ affected=True,
70
+ affected_kinds=affected_datasets(before, desired),
71
+ phase="verification",
72
+ )
73
+ )
74
+ issues = [i for i in issues if i.severity is IssueSeverity.ERROR]
75
+ source_anomalies = {
76
+ (i.code, i.record_id, i.detail) for i in structure_issues(original)
77
+ }
78
+ seen = {(i.code, i.record_id, i.detail) for i in issues}
79
+ for issue in structure_issues(checked, affected=True, phase="verification"):
80
+ issue_key = (issue.code, issue.record_id, issue.detail)
81
+ if (
82
+ issue.severity is IssueSeverity.ERROR
83
+ and issue_key not in source_anomalies
84
+ and issue_key not in seen
85
+ ):
86
+ issues.append(issue)
87
+ seen.add(issue_key)
88
+ ids = {m.draft_id: m.output_id for m in mappings if m.subject == "playlist"}
89
+ tracks = {m.draft_id: m.output_id for m in mappings if m.subject == "track"}
90
+ track_ids = frozenset(tracks.get(t.track_id, t.track_id) for t in desired.tracks)
91
+ deleted = {t.track_id for t in before.tracks} - {t.track_id for t in desired.tracks}
92
+ old = {
93
+ (k, s.chunk.header.playlist_id): s
94
+ for k, s in playlist_rows(original, (2, 3, 5))
95
+ }
96
+ output = {
97
+ (k, s.chunk.header.playlist_id): s for k, s in playlist_rows(checked, (2, 3, 5))
98
+ }
99
+ prior = {p.playlist_id: p for p in before.playlists}
100
+ wanted = {ids.get(p.playlist_id, p.playlist_id): p for p in desired.playlists}
101
+ ordered, aggregates, children = folder_hierarchy(desired)
102
+ _, old_aggregates, old_children = folder_hierarchy(before)
103
+ changed_folders = {
104
+ p.playlist_id
105
+ for p in desired.playlists
106
+ if p.kind.value == "folder"
107
+ and (
108
+ aggregates[p.playlist_id] != old_aggregates.get(p.playlist_id)
109
+ or tuple(c.playlist_id for c in children[p.playlist_id])
110
+ != tuple(c.playlist_id for c in old_children[p.playlist_id])
111
+ )
112
+ }
113
+ structural_tracks = tuple(t.track_id for t in before.tracks) != tuple(
114
+ t.track_id for t in desired.tracks
115
+ )
116
+ old_albums = {t.track_id: t.album for t in before.tracks}
117
+ albums = {tracks.get(t.track_id, t.track_id): t.album for t in desired.tracks}
118
+
119
+ def fail(
120
+ code: str,
121
+ message: str,
122
+ kind: int,
123
+ chunk: ParsedChunk[MhypHeader] | None = None,
124
+ *,
125
+ detail: str = "",
126
+ item: ParsedChunk[MhipHeader] | None = None,
127
+ ) -> None:
128
+ issues.append(
129
+ WriteIssue(
130
+ f"verification.{code}",
131
+ message,
132
+ phase="verification",
133
+ subject="playlist" if chunk else "library",
134
+ record_id=chunk.header.playlist_id if chunk else None,
135
+ detail=f"Dataset {kind}. "
136
+ + (f"{detail} " if detail else "")
137
+ + "The candidate was withheld; the source remains unchanged.",
138
+ offset=item.offset if item else chunk.offset if chunk else None,
139
+ artifact="iTunesDB",
140
+ )
141
+ )
142
+
143
+ kinds = [s.chunk.header.dataset_type for s in checked.find_chunks(MhsdHeader)]
144
+ if not deleted:
145
+ original_firmware = tuple(
146
+ _bytes(s.chunk, original_bytes)
147
+ for s in original.find_chunks(MhsdHeader)
148
+ if s.chunk.header.dataset_type == 5
149
+ )
150
+ checked_firmware = tuple(
151
+ _bytes(s.chunk, output_bytes)
152
+ for s in checked.find_chunks(MhsdHeader)
153
+ if s.chunk.header.dataset_type == 5
154
+ )
155
+ if original_firmware != checked_firmware:
156
+ fail(
157
+ "firmware_dataset", "An unrelated firmware category dataset changed.", 5
158
+ )
159
+ if (
160
+ affected_datasets(before, desired).intersection((2, 3))
161
+ and 3 in kinds
162
+ and (2 not in kinds or kinds.index(3) > kinds.index(2))
163
+ ):
164
+ fail(
165
+ "playlist_dataset_order",
166
+ "The podcast-aware dataset requires a following standard playlist dataset.",
167
+ 3,
168
+ )
169
+ for key, selection in old.items():
170
+ kind, identity = key
171
+ chunk = selection.chunk
172
+ if is_visible(kind, chunk.header) and identity in wanted and key not in output:
173
+ fail(
174
+ "playlist_missing_mirror",
175
+ "A retained Playlist disappeared from its counterpart dataset.",
176
+ kind,
177
+ chunk,
178
+ )
179
+ if (is_visible(kind, chunk.header) and identity in prior) or is_master(
180
+ kind, chunk.header
181
+ ):
182
+ continue
183
+ retained = output.get(key)
184
+ if retained is None:
185
+ fail(
186
+ "hidden_playlist",
187
+ "A retained firmware Playlist disappeared.",
188
+ kind,
189
+ chunk,
190
+ )
191
+ continue
192
+ if not deleted:
193
+ if _bytes(chunk, original_bytes) != _bytes(retained.chunk, output_bytes):
194
+ fail(
195
+ "hidden_playlist",
196
+ "An unrelated firmware Playlist changed.",
197
+ kind,
198
+ retained.chunk,
199
+ )
200
+ else:
201
+ old_metadata = tuple(
202
+ _bytes(c, original_bytes)
203
+ for c in chunk.children
204
+ if not isinstance(c.header, MhipHeader)
205
+ )
206
+ new_metadata = tuple(
207
+ _bytes(c, output_bytes)
208
+ for c in retained.chunk.children
209
+ if not isinstance(c.header, MhipHeader)
210
+ )
211
+ expected_items = tuple(
212
+ s.chunk.header.track_id
213
+ for s in selection.find_chunks(MhipHeader)
214
+ if s.chunk.header.track_id not in deleted
215
+ )
216
+ actual_items = tuple(
217
+ s.chunk.header.track_id for s in retained.find_chunks(MhipHeader)
218
+ )
219
+ expected_header = replace(
220
+ chunk.header, mhip_child_count=retained.chunk.header.mhip_child_count
221
+ )
222
+ if (
223
+ old_metadata != new_metadata
224
+ or expected_items != actual_items
225
+ or expected_header != retained.chunk.header
226
+ ):
227
+ fail(
228
+ "hidden_playlist",
229
+ "A firmware Playlist changed beyond removing deleted Track references.",
230
+ kind,
231
+ retained.chunk,
232
+ )
233
+
234
+ for (kind, identity), selection in output.items():
235
+ chunk = selection.chunk
236
+ baseline = old.get((kind, identity))
237
+ items = tuple(s.chunk for s in selection.find_chunks(MhipHeader))
238
+ if is_master(kind, chunk.header):
239
+ if before.device_name != desired.device_name or baseline is None:
240
+ master_title = next(
241
+ (
242
+ c.payload.value
243
+ for c in chunk.children
244
+ if isinstance(c.payload, MhodStringPayload)
245
+ ),
246
+ "",
247
+ )
248
+ if master_title != desired.device_name:
249
+ fail(
250
+ "master_name",
251
+ "A Master Playlist does not contain the desired device name.",
252
+ kind,
253
+ chunk,
254
+ )
255
+ if structural_tracks or baseline is None:
256
+ if desired.tracks:
257
+ present_indexes = {
258
+ (c.header.mhod_type, c.prefix.sort_type)
259
+ for c in chunk.children
260
+ if isinstance(c.header, MhodHeader)
261
+ and isinstance(
262
+ c.prefix,
263
+ MhodLibraryIndexPrefix | MhodLibraryJumpTablePrefix,
264
+ )
265
+ }
266
+ if any(
267
+ (payload_type, sort) not in present_indexes
268
+ for payload_type in (52, 53)
269
+ for sort in (3, 4, 5, 7, 0x12)
270
+ ):
271
+ fail(
272
+ "playlist_index",
273
+ "A new or structurally edited Master is missing required browse indexes.",
274
+ kind,
275
+ chunk,
276
+ )
277
+ expected = tuple(
278
+ tracks.get(t.track_id, t.track_id) for t in desired.tracks
279
+ )
280
+ retained_missing = (
281
+ ()
282
+ if baseline is None
283
+ else tuple(
284
+ s.chunk.header.track_id
285
+ for s in baseline.find_chunks(MhipHeader)
286
+ if s.chunk.header.track_id
287
+ not in {t.track_id for t in before.tracks}
288
+ )
289
+ )
290
+ if tuple(c.header.track_id for c in items) != (
291
+ *expected,
292
+ *retained_missing,
293
+ ):
294
+ fail(
295
+ "master_membership",
296
+ "Master Playlist membership does not match the resulting library.",
297
+ kind,
298
+ chunk,
299
+ )
300
+ for child in chunk.children:
301
+ retained_index = baseline is not None and any(
302
+ _bytes(child, output_bytes) == _bytes(c, original_bytes)
303
+ for c in baseline.chunk.children
304
+ if not isinstance(c.header, MhipHeader)
305
+ )
306
+ if isinstance(
307
+ child.payload, MhodLibraryIndexPayload | MhodLibraryJumpTablePayload
308
+ ):
309
+ if not isinstance(
310
+ child.prefix,
311
+ MhodLibraryIndexPrefix | MhodLibraryJumpTablePrefix,
312
+ ):
313
+ fail(
314
+ "playlist_index",
315
+ "A library index has no supported sort prefix.",
316
+ kind,
317
+ chunk,
318
+ )
319
+ continue
320
+ prior_tracks = (
321
+ master_tracks(baseline.chunk, before.tracks) if baseline else ()
322
+ )
323
+ final_tracks = master_tracks(
324
+ chunk,
325
+ tuple(
326
+ replace(t, track_id=tracks.get(t.track_id, t.track_id))
327
+ for t in desired.tracks
328
+ ),
329
+ )
330
+ if child.prefix.sort_type == 36 and isinstance(
331
+ child.payload, MhodLibraryIndexPayload
332
+ ):
333
+ source_master = (
334
+ baseline or old.get((3, identity)) or old.get((2, identity))
335
+ )
336
+ original_indexes = (
337
+ []
338
+ if source_master is None
339
+ else [
340
+ c.payload
341
+ for c in source_master.chunk.children
342
+ if isinstance(c.prefix, MhodLibraryIndexPrefix)
343
+ and c.prefix.sort_type == 36
344
+ and isinstance(c.payload, MhodLibraryIndexPayload)
345
+ ]
346
+ )
347
+ error: str | None = (
348
+ "Sort 36 has no unambiguous retained source index."
349
+ )
350
+ if source_master is not None and len(original_indexes) == 1:
351
+ error = verify_album_index(
352
+ original,
353
+ checked,
354
+ master_tracks(source_master.chunk, before.tracks),
355
+ master_tracks(chunk, project_tracks(checked)),
356
+ original_indexes[0],
357
+ child.payload,
358
+ )
359
+ else:
360
+ if len(prior_tracks) != len(before.tracks):
361
+ prior_tracks = before.tracks
362
+ if len(final_tracks) != len(desired.tracks):
363
+ final_tracks = desired.tracks
364
+ error = verify_index(
365
+ child.payload,
366
+ child.prefix.sort_type,
367
+ prior_tracks,
368
+ final_tracks,
369
+ retained=retained_index,
370
+ )
371
+ if error:
372
+ fail("playlist_index", error, kind, chunk)
373
+ continue
374
+ if not is_visible(kind, chunk.header):
375
+ continue
376
+ playlist = wanted.get(identity)
377
+ if playlist is None:
378
+ if baseline and identity in prior:
379
+ fail(
380
+ "playlist_deleted",
381
+ "A deleted visible Playlist remains in a counterpart dataset.",
382
+ kind,
383
+ chunk,
384
+ )
385
+ continue
386
+ previous = prior.get(playlist.playlist_id)
387
+ if (
388
+ playlist.playlist_id in generated_podcasts
389
+ and not chunk.header.playlist_kind_flags & 1
390
+ ):
391
+ fail(
392
+ "podcast_marker",
393
+ "The generated special Podcasts playlist is missing its native marker.",
394
+ kind,
395
+ chunk,
396
+ )
397
+ if (
398
+ baseline is None
399
+ and playlist.playlist_id not in prior
400
+ and (
401
+ chunk.header.playlist_id_2 != identity
402
+ or chunk.header.db_id_2 != checked.header.db_id_2
403
+ or chunk.header.timestamp != chunk.header.timestamp_2
404
+ )
405
+ ):
406
+ fail(
407
+ "playlist_native_identity",
408
+ "A new Playlist has inconsistent extended identity or timestamp fields.",
409
+ kind,
410
+ chunk,
411
+ )
412
+ affected = playlist != previous or playlist.playlist_id in changed_folders
413
+ if affected or baseline is None:
414
+ expected_playlist = replace(
415
+ playlist,
416
+ playlist_id=identity,
417
+ parent_id=ids.get(playlist.parent_id, playlist.parent_id)
418
+ if playlist.parent_id is not None
419
+ else None,
420
+ entries=(),
421
+ )
422
+ actual = project_playlist(selection, track_ids, device_time)
423
+ if previous and previous.parent_id == playlist.parent_id:
424
+ parent_source = (
425
+ baseline or old.get((3, identity)) or old.get((2, identity))
426
+ )
427
+ if parent_source:
428
+ expected_playlist = replace(
429
+ expected_playlist,
430
+ parent_id=parent_source.chunk.header.parent_folder_playlist_id
431
+ or None,
432
+ )
433
+ expected_tracks = tuple(
434
+ tracks.get(e.track_id, e.track_id) for e in playlist.entries
435
+ )
436
+ matches_membership = actual.track_ids == expected_tracks
437
+ if (
438
+ previous
439
+ and previous.entries == playlist.entries
440
+ and chunk.header.playlist_kind_flags & 1
441
+ ):
442
+ matches_membership = Counter(actual.track_ids) == Counter(
443
+ expected_tracks
444
+ )
445
+ if (
446
+ replace(actual, entries=()) != expected_playlist
447
+ or not matches_membership
448
+ ):
449
+ fail(
450
+ "playlist_mirror",
451
+ "A Playlist counterpart does not contain the desired semantic values and membership.",
452
+ kind,
453
+ chunk,
454
+ )
455
+ membership_changed = previous is None or previous.entries != playlist.entries
456
+ if is_podcasts(kind, chunk.header) and (
457
+ membership_changed
458
+ or any(
459
+ old_albums.get(e.track_id)
460
+ != albums.get(tracks.get(e.track_id, e.track_id))
461
+ for e in playlist.entries
462
+ )
463
+ ):
464
+ groups: dict[int, str] = {}
465
+ native_ids: set[int] = set()
466
+ active_group = 0
467
+ for item in items:
468
+ h = item.header
469
+ if not h.group_id or h.group_id in native_ids:
470
+ fail(
471
+ "podcast_group",
472
+ "Podcast row identifiers are missing or repeated.",
473
+ kind,
474
+ chunk,
475
+ )
476
+ native_ids.add(h.group_id)
477
+ if h.podcast_group_flag & 0x100:
478
+ title = next(
479
+ (
480
+ c.payload.value
481
+ for c in item.children
482
+ if isinstance(c.payload, MhodStringPayload)
483
+ ),
484
+ None,
485
+ )
486
+ if h.track_id or title is None or title in groups.values():
487
+ fail(
488
+ "podcast_group",
489
+ "Podcast group headers have invalid Tracks or show titles.",
490
+ kind,
491
+ chunk,
492
+ )
493
+ groups[h.group_id] = title or ""
494
+ active_group = h.group_id
495
+ elif h.group_id_ref != active_group or groups.get(
496
+ h.group_id_ref
497
+ ) != albums.get(h.track_id):
498
+ fail(
499
+ "podcast_group",
500
+ "A podcast episode does not belong to its preceding show group.",
501
+ kind,
502
+ chunk,
503
+ )
504
+ else:
505
+ positions = [
506
+ c.prefix.position
507
+ for c in item.children
508
+ if isinstance(c.prefix, MhodPlaylistPositionPrefix)
509
+ ]
510
+ if not positions or any(value != h.group_id for value in positions):
511
+ fail(
512
+ "podcast_position",
513
+ "Podcast episode position metadata must reference its native row identifier.",
514
+ kind,
515
+ chunk,
516
+ detail=f"Track {h.track_id}, row ID {h.group_id}, expected position {h.group_id}, actual positions {positions}.",
517
+ item=item,
518
+ )
519
+ elif membership_changed or playlist.playlist_id in changed_folders:
520
+ for position, item in enumerate(items):
521
+ if item.header.podcast_group_flag & 0x100 or item.header.group_id_ref:
522
+ fail(
523
+ "podcast_dataset",
524
+ "Podcast grouping occurs outside dataset 3's special Podcasts playlist.",
525
+ kind,
526
+ chunk,
527
+ detail=f"Occurrence {position}, Track {item.header.track_id}, group flag {item.header.podcast_group_flag}, group reference {item.header.group_id_ref}.",
528
+ item=item,
529
+ )
530
+ break
531
+ positions = [
532
+ c.prefix.position
533
+ for c in item.children
534
+ if isinstance(c.prefix, MhodPlaylistPositionPrefix)
535
+ ]
536
+ if not positions or any(value != position for value in positions):
537
+ fail(
538
+ "playlist_position",
539
+ "A changed Playlist has inconsistent ordinary occurrence positions.",
540
+ kind,
541
+ chunk,
542
+ detail=f"Occurrence {position}, Track {item.header.track_id}, expected position {position}, actual positions {positions}.",
543
+ item=item,
544
+ )
545
+ break
546
+ if playlist.playlist_id in changed_folders:
547
+ expected_members = tuple(
548
+ tracks.get(i, i) for i in aggregates[playlist.playlist_id]
549
+ )
550
+ rules = [
551
+ c
552
+ for c in chunk.children
553
+ if isinstance(c.payload, MhodSmartRulesPayload)
554
+ ]
555
+ expected_children = tuple(
556
+ ids.get(p.playlist_id, p.playlist_id)
557
+ for p in children[playlist.playlist_id]
558
+ )
559
+ if (
560
+ tuple(c.header.track_id for c in items) != expected_members
561
+ or len(rules) != 1
562
+ ):
563
+ fail(
564
+ "folder_membership",
565
+ "Folder membership or direct-child rules are inconsistent.",
566
+ kind,
567
+ chunk,
568
+ )
569
+ elif isinstance(rules[0].payload, MhodSmartRulesPayload):
570
+ row = rules[0]
571
+ assert isinstance(row.payload, MhodSmartRulesPayload)
572
+ actual_children = tuple(
573
+ r.data.from_value
574
+ for r in row.payload.rules
575
+ if r.field_id == 0x28
576
+ and r.action_id == 1
577
+ and isinstance(r.data, MhodSmartNumericRuleData)
578
+ )
579
+ if (
580
+ actual_children != expected_children
581
+ or len(actual_children) != len(row.payload.rules)
582
+ or not isinstance(row.prefix, MhodSmartRulesPrefix)
583
+ or row.prefix.conjunction != 1
584
+ ):
585
+ fail(
586
+ "folder_rules",
587
+ "Folder rules do not describe its ordered direct children.",
588
+ kind,
589
+ chunk,
590
+ )
591
+ for kind in (2, 3):
592
+ if kind not in kinds:
593
+ continue
594
+ for identity, playlist in wanted.items():
595
+ if playlist.playlist_id not in prior and (kind, identity) not in output:
596
+ fail(
597
+ "playlist_missing_mirror",
598
+ "A new Playlist is absent from a required counterpart dataset.",
599
+ kind,
600
+ )
601
+ if tuple((p.playlist_id, p.parent_id) for p in before.playlists) != tuple(
602
+ (p.playlist_id, p.parent_id) for p in desired.playlists
603
+ ):
604
+ actual_order = tuple(
605
+ s.chunk.header.playlist_id
606
+ for k, s in playlist_rows(checked)
607
+ if k == kind
608
+ and s.chunk.header.playlist_id in wanted
609
+ and is_visible(k, s.chunk.header)
610
+ )
611
+ expected_order = tuple(
612
+ ids.get(p.playlist_id, p.playlist_id)
613
+ for p in ordered
614
+ if (kind, ids.get(p.playlist_id, p.playlist_id)) in output
615
+ )
616
+ if actual_order != expected_order:
617
+ fail(
618
+ "folder_order",
619
+ "Visible Playlists are not in the desired folder preorder.",
620
+ kind,
621
+ )
622
+ return tuple(issues)