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,671 @@
1
+ """Pure SysInfo reconciliation and iOpenPod authority-file support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import plistlib
8
+ from dataclasses import dataclass
9
+ from datetime import UTC, datetime
10
+ from typing import TYPE_CHECKING, Any, cast
11
+ from xml.parsers.expat import ExpatError
12
+
13
+ from device_registry.identifiers import normalize_model_number, normalize_serial
14
+ from device_registry.models import (
15
+ DeviceEvidence,
16
+ DeviceIdentifier,
17
+ DeviceProfile,
18
+ EvidenceAuthority,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Mapping, Sequence
23
+
24
+ SYSINFO_AUTHORITY_FILENAME = "iOpenPodSysInfoAuthority"
25
+
26
+ _CORE_KEYS = frozenset({"pszSerialNumber", "FirewireGuid", "ModelNumStr"})
27
+ _DERIVED_KEYS = frozenset(
28
+ {"ModelFamily", "Generation", "Capacity", "Color", "USBProductID"}
29
+ )
30
+ _CURRENT_HARDWARE_SOURCES = frozenset(
31
+ {
32
+ "scsi_vpd",
33
+ "windows_scsi",
34
+ "linux_scsi",
35
+ "sysfs_vpd",
36
+ "udev_scsi_id",
37
+ "usb_vendor",
38
+ "vpd",
39
+ "iokit",
40
+ "ioctl",
41
+ "device_tree",
42
+ "ioreg",
43
+ "sysfs",
44
+ "udev",
45
+ "wmi",
46
+ }
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class DeviceMetadataPlan:
52
+ """Desired metadata bytes and whether each atomic replacement is needed."""
53
+
54
+ sysinfo: bytes
55
+ sysinfo_extended: bytes
56
+ authority: bytes
57
+ sysinfo_changed: bool
58
+ sysinfo_extended_changed: bool
59
+ authority_changed: bool
60
+
61
+ @property
62
+ def changed(self) -> bool:
63
+ return (
64
+ self.sysinfo_changed
65
+ or self.sysinfo_extended_changed
66
+ or self.authority_changed
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class _ResolvedField:
72
+ value: str
73
+ source: str
74
+ authority: EvidenceAuthority
75
+
76
+
77
+ def authority_covers_metadata(
78
+ authority: bytes | str,
79
+ *,
80
+ sysinfo: bytes,
81
+ sysinfo_extended: bytes,
82
+ ) -> bool:
83
+ """Return whether current hardware covers untampered core metadata."""
84
+
85
+ parsed = _load_authority(authority)
86
+ if parsed.get("version") != 1 or not _authority_hashes_match(
87
+ parsed,
88
+ sysinfo=sysinfo,
89
+ sysinfo_extended=sysinfo_extended,
90
+ ):
91
+ return False
92
+ fields = _string_object_dict(parsed.get("fields"))
93
+ if fields is None:
94
+ return False
95
+ sysinfo_values = _parse_sysinfo(sysinfo)
96
+ for key in _CORE_KEYS:
97
+ entry = _string_object_dict(fields.get(key))
98
+ if entry is None:
99
+ return False
100
+ if _entry_authority(entry) is not EvidenceAuthority.CURRENT_HARDWARE:
101
+ return False
102
+ expected = entry.get("value")
103
+ current = sysinfo_values.get(key, "")
104
+ if not isinstance(expected, str) or not current:
105
+ return False
106
+ if _normalize_sysinfo_value(key, expected) != _normalize_sysinfo_value(
107
+ key,
108
+ current,
109
+ ):
110
+ return False
111
+ return True
112
+
113
+
114
+ def reconcile_device_metadata(
115
+ *,
116
+ evidence: DeviceEvidence,
117
+ profile: DeviceProfile,
118
+ existing_sysinfo: bytes = b"",
119
+ existing_sysinfo_extended: bytes = b"",
120
+ existing_authority: bytes = b"",
121
+ live_sysinfo_extended: bytes = b"",
122
+ observed_at: str | None = None,
123
+ ) -> DeviceMetadataPlan:
124
+ """Build a deterministic, path-free plan from ranked evidence.
125
+
126
+ Unknown SysInfo keys and SysInfoExtended plist keys are retained. Current
127
+ hardware can correct stale device metadata, while lower-authority values cannot
128
+ overwrite higher-authority entries recorded by the original version-1 format.
129
+ """
130
+
131
+ timestamp = observed_at or datetime.now(UTC).isoformat()
132
+ old_authority = _load_authority(existing_authority)
133
+ hashes_match = _authority_hashes_match(
134
+ old_authority,
135
+ sysinfo=existing_sysinfo,
136
+ sysinfo_extended=existing_sysinfo_extended,
137
+ )
138
+ old_entries = (
139
+ _string_object_dict(old_authority.get("fields")) if hashes_match else {}
140
+ )
141
+ if old_entries is None:
142
+ old_entries = {}
143
+
144
+ existing_fields = _parse_sysinfo(existing_sysinfo)
145
+ updated_fields = dict(existing_fields)
146
+ resolved = _resolved_fields(evidence, profile)
147
+ authority_fields: dict[str, object] = {}
148
+
149
+ for key, candidate in resolved.items():
150
+ old_value = existing_fields.get(key, "")
151
+ old_entry = _string_object_dict(old_entries.get(key)) or {}
152
+ old_authority_level = _entry_authority(old_entry)
153
+ if not old_entry:
154
+ old_authority_level = EvidenceAuthority.DEVICE_METADATA
155
+
156
+ same_value = bool(old_value) and _normalize_sysinfo_value(
157
+ key,
158
+ old_value,
159
+ ) == _normalize_sysinfo_value(key, candidate.value)
160
+ should_replace_value = (
161
+ not old_value
162
+ or key in _DERIVED_KEYS
163
+ or candidate.authority >= old_authority_level
164
+ )
165
+ if should_replace_value and not same_value:
166
+ updated_fields[key] = candidate.value
167
+ retained_value = updated_fields.get(key, "")
168
+ if not retained_value:
169
+ continue
170
+
171
+ use_candidate_provenance = (
172
+ not old_entry
173
+ or candidate.authority >= old_authority_level
174
+ or (key in _DERIVED_KEYS and not same_value)
175
+ )
176
+ if use_candidate_provenance:
177
+ source = candidate.source
178
+ authority_level = candidate.authority
179
+ else:
180
+ source = _entry_source(old_entry, default="sysinfo")
181
+ authority_level = old_authority_level
182
+ authority_fields[key] = _authority_entry(
183
+ retained_value,
184
+ source=source,
185
+ authority=authority_level,
186
+ timestamp=timestamp,
187
+ previous=old_entry,
188
+ )
189
+
190
+ for key, entry in old_entries.items():
191
+ if key in authority_fields or key not in updated_fields:
192
+ continue
193
+ typed_entry = _string_object_dict(entry)
194
+ if typed_entry is not None:
195
+ authority_fields[key] = typed_entry
196
+
197
+ sysinfo = _serialize_sysinfo(updated_fields)
198
+ sysinfo_extended = _build_sysinfo_extended(
199
+ existing=existing_sysinfo_extended,
200
+ live=live_sysinfo_extended,
201
+ sysinfo_fields=updated_fields,
202
+ profile=profile,
203
+ )
204
+
205
+ file_hashes = {
206
+ "SysInfo": _sha256(sysinfo),
207
+ "SysInfoExtended": _sha256(sysinfo_extended),
208
+ }
209
+ files: dict[str, object] = dict(
210
+ _string_object_dict(old_authority.get("files")) or {}
211
+ )
212
+ previous_extended_file = _string_object_dict(files.get("SysInfoExtended"))
213
+ extended_source = "device_registry"
214
+ if live_sysinfo_extended:
215
+ extended_source = _live_payload_source(evidence)
216
+ elif (
217
+ existing_sysinfo_extended == sysinfo_extended
218
+ and previous_extended_file is not None
219
+ and isinstance(previous_extended_file.get("source"), str)
220
+ ):
221
+ extended_source = cast("str", previous_extended_file["source"])
222
+ elif existing_sysinfo_extended:
223
+ extended_source = "sysinfo_extended"
224
+ if not (
225
+ previous_extended_file is not None
226
+ and previous_extended_file.get("source") == extended_source
227
+ and previous_extended_file.get("bytes") == len(sysinfo_extended)
228
+ and existing_sysinfo_extended == sysinfo_extended
229
+ ):
230
+ files["SysInfoExtended"] = {
231
+ "source": extended_source,
232
+ "updated": timestamp,
233
+ "bytes": len(sysinfo_extended),
234
+ }
235
+
236
+ authority_document: dict[str, object] = {
237
+ "version": 1,
238
+ "fields": authority_fields,
239
+ "files": files,
240
+ "file_hashes": file_hashes,
241
+ }
242
+ old_without_timestamp = {
243
+ key: value for key, value in old_authority.items() if key != "last_updated"
244
+ }
245
+ if old_without_timestamp == authority_document and existing_authority:
246
+ authority = existing_authority
247
+ else:
248
+ authority_document["last_updated"] = timestamp
249
+ authority = (
250
+ json.dumps(authority_document, indent=2, ensure_ascii=False) + "\n"
251
+ ).encode("utf-8")
252
+
253
+ return DeviceMetadataPlan(
254
+ sysinfo=sysinfo,
255
+ sysinfo_extended=sysinfo_extended,
256
+ authority=authority,
257
+ sysinfo_changed=sysinfo != existing_sysinfo,
258
+ sysinfo_extended_changed=sysinfo_extended != existing_sysinfo_extended,
259
+ authority_changed=authority != existing_authority,
260
+ )
261
+
262
+
263
+ def _resolved_fields(
264
+ evidence: DeviceEvidence,
265
+ profile: DeviceProfile,
266
+ ) -> dict[str, _ResolvedField]:
267
+ result: dict[str, _ResolvedField] = {}
268
+ serial = _best_identifier(evidence.product_serials)
269
+ transport = _best_identifier(evidence.transport_serials)
270
+ firmware = _best_identifier(evidence.firmware_versions)
271
+ board = _best_identifier(evidence.board_hardware_names)
272
+ family_id = _best_identifier(evidence.family_ids)
273
+ updater_family_id = _best_identifier(evidence.updater_family_ids)
274
+ usb = _best_identifier(evidence.usb_identifiers)
275
+ model_proof = _model_proof(evidence, profile)
276
+
277
+ _put(result, "pszSerialNumber", serial, _format_serial)
278
+ _put(result, "FirewireGuid", transport, _format_guid)
279
+ _put(result, "visibleBuildID", firmware, str)
280
+ _put(result, "BoardHwName", board, str)
281
+ result["ModelNumStr"] = _ResolvedField(
282
+ _format_model_number(profile.model_number),
283
+ model_proof.source,
284
+ model_proof.authority,
285
+ )
286
+ _put(result, "FamilyID", family_id, str)
287
+ _put(result, "UpdaterFamilyID", updater_family_id, str)
288
+
289
+ result["ModelFamily"] = _ResolvedField(
290
+ profile.family,
291
+ "model_table",
292
+ EvidenceAuthority.DERIVED,
293
+ )
294
+ result["Generation"] = _ResolvedField(
295
+ profile.generation,
296
+ "model_table",
297
+ EvidenceAuthority.DERIVED,
298
+ )
299
+ result["Capacity"] = _ResolvedField(
300
+ profile.advertised_capacity,
301
+ "model_table",
302
+ EvidenceAuthority.DERIVED,
303
+ )
304
+ result["Color"] = _ResolvedField(
305
+ profile.finish,
306
+ "model_table",
307
+ EvidenceAuthority.DERIVED,
308
+ )
309
+ if usb is not None:
310
+ result["USBProductID"] = _ResolvedField(
311
+ f"0x{usb.value.product_id:04X}",
312
+ usb.source,
313
+ usb.authority,
314
+ )
315
+ return result
316
+
317
+
318
+ def _put[Value](
319
+ result: dict[str, _ResolvedField],
320
+ key: str,
321
+ identifier: DeviceIdentifier[Value] | None,
322
+ formatter: Any,
323
+ ) -> None:
324
+ if identifier is None:
325
+ return
326
+ formatted = str(formatter(identifier.value)).strip()
327
+ if formatted:
328
+ result[key] = _ResolvedField(
329
+ formatted,
330
+ identifier.source,
331
+ identifier.authority,
332
+ )
333
+
334
+
335
+ def _best_identifier[Value](
336
+ identifiers: Sequence[DeviceIdentifier[Value]],
337
+ ) -> DeviceIdentifier[Value] | None:
338
+ if not identifiers:
339
+ return None
340
+ return max(
341
+ identifiers,
342
+ key=lambda item: (int(item.authority), item.source.casefold()),
343
+ )
344
+
345
+
346
+ def _model_proof(
347
+ evidence: DeviceEvidence,
348
+ profile: DeviceProfile,
349
+ ) -> _ResolvedField:
350
+ matching = tuple(
351
+ identifier
352
+ for identifier in evidence.model_numbers
353
+ if normalize_model_number(identifier.value) == profile.model_number
354
+ )
355
+ direct = _best_identifier(matching)
356
+ serial = _best_identifier(
357
+ tuple(
358
+ identifier
359
+ for identifier in evidence.product_serials
360
+ if _serial_resolves_to_profile(identifier.value, profile)
361
+ )
362
+ )
363
+ proofs = tuple(
364
+ _ResolvedField(profile.model_number, item.source, item.authority)
365
+ for item in (direct, serial)
366
+ if item is not None
367
+ )
368
+ if proofs:
369
+ return max(
370
+ proofs,
371
+ key=lambda proof: (int(proof.authority), proof.source.casefold()),
372
+ )
373
+ return _ResolvedField(
374
+ profile.model_number,
375
+ "model_table",
376
+ EvidenceAuthority.DERIVED,
377
+ )
378
+
379
+
380
+ def _serial_resolves_to_profile(value: str, profile: DeviceProfile) -> bool:
381
+ # Local import avoids coupling catalog construction back to reconciliation.
382
+ from device_registry.data.catalog import DEFAULT_CATALOG
383
+
384
+ serial = normalize_serial(value)
385
+ matches = tuple(
386
+ definition
387
+ for definition in DEFAULT_CATALOG.serial_suffixes
388
+ if serial.endswith(definition.suffix)
389
+ )
390
+ if not matches:
391
+ return False
392
+ longest = max(len(definition.suffix) for definition in matches)
393
+ models = {
394
+ definition.model_number
395
+ for definition in matches
396
+ if len(definition.suffix) == longest
397
+ }
398
+ return models == {profile.model_number}
399
+
400
+
401
+ def _build_sysinfo_extended(
402
+ *,
403
+ existing: bytes,
404
+ live: bytes,
405
+ sysinfo_fields: Mapping[str, str],
406
+ profile: DeviceProfile,
407
+ ) -> bytes:
408
+ values = _load_plist_mapping(live) if live else _load_plist_mapping(existing)
409
+ values = dict(values)
410
+
411
+ _set_if(values, "SerialNumber", sysinfo_fields.get("pszSerialNumber", ""))
412
+ _set_if(
413
+ values,
414
+ "FireWireGUID",
415
+ _normalize_sysinfo_value(
416
+ "FirewireGuid",
417
+ sysinfo_fields.get("FirewireGuid", ""),
418
+ ),
419
+ )
420
+ _set_if(values, "FireWireVersion", sysinfo_fields.get("visibleBuildID", ""))
421
+ _set_if(values, "BoardHwName", sysinfo_fields.get("BoardHwName", ""))
422
+ _set_if(values, "ModelNumStr", profile.model_number)
423
+ _set_integer(values, "FamilyID", sysinfo_fields.get("FamilyID", ""))
424
+ _set_integer(
425
+ values,
426
+ "UpdaterFamilyID",
427
+ sysinfo_fields.get("UpdaterFamilyID", ""),
428
+ )
429
+ usb_product_id = _parse_integer(sysinfo_fields.get("USBProductID", ""))
430
+ if usb_product_id is not None:
431
+ values["usb_vid"] = 0x05AC
432
+ values["usb_pid"] = usb_product_id
433
+
434
+ capabilities = profile.capabilities
435
+ values["ProductType"] = profile.family
436
+ values["DBVersion"] = capabilities.database.binary_version
437
+ values["SQLiteDB"] = capabilities.database.uses_sqlite_database
438
+ values["PodcastsSupported"] = capabilities.audio.supports_podcasts
439
+ values["ScreenWidth"] = capabilities.display.width
440
+ values["ScreenHeight"] = capabilities.display.height
441
+ values["ColorDisplay"] = capabilities.display.color
442
+ values["VideoSupported"] = capabilities.video.supported
443
+ if capabilities.video.supported:
444
+ values["MaxVideoWidth"] = capabilities.video.max_width
445
+ values["MaxVideoHeight"] = capabilities.video.max_height
446
+ else:
447
+ values.pop("MaxVideoWidth", None)
448
+ values.pop("MaxVideoHeight", None)
449
+ if capabilities.artwork.cover_formats:
450
+ values["AlbumArt"] = [
451
+ _artwork_plist_entry(item) for item in capabilities.artwork.cover_formats
452
+ ]
453
+ else:
454
+ values.pop("AlbumArt", None)
455
+ if capabilities.artwork.photo_formats:
456
+ values["ImageSpecifications"] = [
457
+ _artwork_plist_entry(item) for item in capabilities.artwork.photo_formats
458
+ ]
459
+ else:
460
+ values.pop("ImageSpecifications", None)
461
+ return plistlib.dumps(values, fmt=plistlib.FMT_XML, sort_keys=True)
462
+
463
+
464
+ def _artwork_plist_entry(item: Any) -> dict[str, object]:
465
+ return {
466
+ "FormatId": item.format_id,
467
+ "RenderWidth": item.width,
468
+ "RenderHeight": item.height,
469
+ "RowBytes": item.row_bytes,
470
+ "PixelFormat": item.pixel_format.value,
471
+ }
472
+
473
+
474
+ def _string_object_dict(value: object) -> dict[str, object] | None:
475
+ """Validate and narrow an untyped serialized mapping."""
476
+
477
+ if not isinstance(value, dict):
478
+ return None
479
+ raw = cast("dict[object, object]", value)
480
+ result: dict[str, object] = {}
481
+ for key, item in raw.items():
482
+ if not isinstance(key, str):
483
+ return None
484
+ result[key] = item
485
+ return result
486
+
487
+
488
+ def _load_plist_mapping(content: bytes) -> Mapping[str, object]:
489
+ if not content:
490
+ return {}
491
+ raw = content.rstrip(b"\x00")
492
+ starts = tuple(
493
+ index
494
+ for marker in (b"<?xml", b"<plist", b"bplist")
495
+ if (index := raw.find(marker)) >= 0
496
+ )
497
+ if starts:
498
+ raw = raw[min(starts) :]
499
+ candidates = [raw]
500
+ if b"<plist" in raw and b"</plist>" not in raw:
501
+ candidates.append(raw + b"\n</dict>\n</plist>")
502
+ for candidate in candidates:
503
+ try:
504
+ parsed = cast("object", plistlib.loads(candidate))
505
+ except (ExpatError, plistlib.InvalidFileException, TypeError, ValueError):
506
+ continue
507
+ mapping = _string_object_dict(parsed)
508
+ if mapping is not None:
509
+ return mapping
510
+ return {}
511
+
512
+
513
+ def _parse_sysinfo(content: bytes) -> dict[str, str]:
514
+ result: dict[str, str] = {}
515
+ text = content.decode("utf-8-sig", errors="replace")
516
+ for raw_line in text.splitlines():
517
+ line = raw_line.strip()
518
+ if not line or line.startswith("#") or ":" not in line:
519
+ continue
520
+ key, value = line.split(":", maxsplit=1)
521
+ normalized_key = key.strip()
522
+ if normalized_key:
523
+ result[normalized_key] = value.strip()
524
+ return result
525
+
526
+
527
+ def _serialize_sysinfo(values: Mapping[str, str]) -> bytes:
528
+ return (
529
+ "".join(f"{key}: {value}\n" for key, value in values.items() if value)
530
+ ).encode("utf-8")
531
+
532
+
533
+ def _load_authority(content: bytes | str) -> dict[str, object]:
534
+ if not content:
535
+ return {}
536
+ try:
537
+ parsed = cast("object", json.loads(content))
538
+ except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
539
+ return {}
540
+ return _string_object_dict(parsed) or {}
541
+
542
+
543
+ def _authority_hashes_match(
544
+ authority: Mapping[str, object],
545
+ *,
546
+ sysinfo: bytes,
547
+ sysinfo_extended: bytes,
548
+ ) -> bool:
549
+ hashes = _string_object_dict(authority.get("file_hashes"))
550
+ if not hashes:
551
+ return False
552
+ expected_sysinfo = hashes.get("SysInfo")
553
+ if not isinstance(expected_sysinfo, str) or expected_sysinfo != _sha256(sysinfo):
554
+ return False
555
+ expected_extended = hashes.get("SysInfoExtended")
556
+ return expected_extended is None or (
557
+ isinstance(expected_extended, str)
558
+ and expected_extended == _sha256(sysinfo_extended)
559
+ )
560
+
561
+
562
+ def _authority_entry(
563
+ value: str,
564
+ *,
565
+ source: str,
566
+ authority: EvidenceAuthority,
567
+ timestamp: str,
568
+ previous: Mapping[str, object],
569
+ ) -> dict[str, object]:
570
+ authority_name = authority.name.casefold()
571
+ if (
572
+ previous.get("value") == value
573
+ and previous.get("source") == source
574
+ and previous.get("authority", authority_name) == authority_name
575
+ and isinstance(previous.get("updated"), str)
576
+ ):
577
+ updated = cast("str", previous["updated"])
578
+ else:
579
+ updated = timestamp
580
+ return {
581
+ "value": value,
582
+ "source": source,
583
+ "authority": authority_name,
584
+ "updated": updated,
585
+ }
586
+
587
+
588
+ def _entry_authority(entry: Mapping[str, object]) -> EvidenceAuthority:
589
+ raw = entry.get("authority")
590
+ if isinstance(raw, str):
591
+ try:
592
+ return EvidenceAuthority[raw.upper()]
593
+ except KeyError:
594
+ pass
595
+ source = _entry_source(entry, default="sysinfo")
596
+ if source in _CURRENT_HARDWARE_SOURCES:
597
+ return EvidenceAuthority.CURRENT_HARDWARE
598
+ if source in {"model_table", "serial_lookup", "usb_pid", "inferred"}:
599
+ return EvidenceAuthority.DERIVED
600
+ return EvidenceAuthority.DEVICE_METADATA
601
+
602
+
603
+ def _entry_source(entry: Mapping[str, object], *, default: str) -> str:
604
+ source = entry.get("source")
605
+ return source if isinstance(source, str) and source.strip() else default
606
+
607
+
608
+ def _format_serial(value: object) -> str:
609
+ return normalize_serial(str(value))
610
+
611
+
612
+ def _format_guid(value: object) -> str:
613
+ normalized = normalize_serial(str(value).removeprefix("0x").removeprefix("0X"))
614
+ return f"0x{normalized}" if normalized else ""
615
+
616
+
617
+ def _format_model_number(value: str) -> str:
618
+ normalized = normalize_model_number(value)
619
+ return f"x{normalized[1:]}" if normalized.startswith("M") else normalized
620
+
621
+
622
+ def _normalize_sysinfo_value(key: str, value: object) -> str:
623
+ text = str(value).strip().rstrip("\x00")
624
+ if key == "FirewireGuid":
625
+ return text.removeprefix("0x").removeprefix("0X").upper()
626
+ if key == "ModelNumStr":
627
+ return normalize_model_number(text)
628
+ if key == "USBProductID":
629
+ parsed = _parse_integer(text)
630
+ return str(parsed) if parsed is not None else text.upper()
631
+ return text
632
+
633
+
634
+ def _parse_integer(value: str) -> int | None:
635
+ text = value.strip()
636
+ if not text:
637
+ return None
638
+ try:
639
+ return int(text, 0)
640
+ except ValueError:
641
+ return None
642
+
643
+
644
+ def _set_if(values: dict[str, object], key: str, value: str) -> None:
645
+ if value:
646
+ values[key] = value
647
+
648
+
649
+ def _set_integer(values: dict[str, object], key: str, value: str) -> None:
650
+ parsed = _parse_integer(value)
651
+ if parsed is not None:
652
+ values[key] = parsed
653
+
654
+
655
+ def _sha256(content: bytes) -> str:
656
+ return hashlib.sha256(content).hexdigest()
657
+
658
+
659
+ def _live_payload_source(evidence: DeviceEvidence) -> str:
660
+ candidates: list[DeviceIdentifier[object]] = [
661
+ *evidence.product_serials,
662
+ *evidence.transport_serials,
663
+ *evidence.firmware_versions,
664
+ ]
665
+ current = tuple(
666
+ item
667
+ for item in candidates
668
+ if item.authority is EvidenceAuthority.CURRENT_HARDWARE
669
+ )
670
+ best = _best_identifier(current)
671
+ return best.source if best is not None else "current_hardware"
@@ -0,0 +1 @@
1
+ """Built-in Device Registry definitions."""