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,699 @@
1
+ """Linux MPRIS integration through Qt's D-Bus event-loop support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import logging
7
+ import math
8
+ from collections.abc import Callable, Mapping
9
+ from dataclasses import dataclass
10
+ from io import BytesIO
11
+ from typing import Protocol, cast
12
+
13
+ from PySide6.QtCore import (
14
+ ClassInfo,
15
+ Property,
16
+ QDir,
17
+ QElapsedTimer,
18
+ QFile,
19
+ QIODevice,
20
+ QMetaType,
21
+ QObject,
22
+ QSaveFile,
23
+ QTemporaryDir,
24
+ QUrl,
25
+ Signal,
26
+ Slot,
27
+ )
28
+ from PySide6.QtDBus import (
29
+ QDBusAbstractAdaptor,
30
+ QDBusArgument,
31
+ QDBusConnection,
32
+ QDBusMessage,
33
+ QDBusObjectPath,
34
+ QDBusVariant,
35
+ )
36
+
37
+ from iOpenPod.app.playback.system_media.session import (
38
+ SystemMediaArtwork,
39
+ SystemMediaCommand,
40
+ SystemMediaCommandHandler,
41
+ SystemMediaCommandKind,
42
+ SystemMediaSnapshot,
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ _STRING_META_TYPE = QMetaType(QMetaType.Type.QString)
48
+ _DBUS_VARIANT_META_TYPE = QMetaType.fromName(b"QDBusVariant")
49
+ _BUS_NAME = "org.mpris.MediaPlayer2.iOpenPod"
50
+ _OBJECT_PATH = "/org/mpris/MediaPlayer2"
51
+ _ROOT_INTERFACE = "org.mpris.MediaPlayer2"
52
+ _PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player"
53
+ _PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class _MprisState:
58
+ """MPRIS-shaped state kept independent of Qt's D-Bus value wrappers."""
59
+
60
+ track_path: str
61
+ playback_status: str
62
+ metadata: Mapping[str, object]
63
+ duration_us: int
64
+ position_us: int
65
+ volume: float
66
+ can_play: bool
67
+ can_pause: bool
68
+ can_next: bool
69
+ can_previous: bool
70
+ can_seek: bool
71
+
72
+
73
+ _NativeCommandHandler = Callable[[str, tuple[object, ...]], None]
74
+
75
+
76
+ class _ElapsedClock(Protocol):
77
+ """Monotonic elapsed clock used to project a live MPRIS position."""
78
+
79
+ def start(self) -> None: ...
80
+
81
+ def invalidate(self) -> None: ...
82
+
83
+ def isValid(self) -> bool: ...
84
+
85
+ def nsecsElapsed(self) -> int: ...
86
+
87
+
88
+ class _MprisTransport(Protocol):
89
+ """Injectable D-Bus transport used by the platform-neutral adapter tests."""
90
+
91
+ def set_command_handler(
92
+ self,
93
+ handler: _NativeCommandHandler | None,
94
+ ) -> None: ...
95
+
96
+ def publish(self, state: _MprisState | None) -> None: ...
97
+
98
+ def seeked(self, position_us: int) -> None: ...
99
+
100
+ def close(self) -> None: ...
101
+
102
+
103
+ class LinuxSystemMediaSession:
104
+ """Publish controller snapshots through the desktop's MPRIS 2 session."""
105
+
106
+ def __init__(self, transport: _MprisTransport | None = None) -> None:
107
+ self._artwork_cache = _MprisArtworkCache()
108
+ try:
109
+ self._transport = transport or _QtMprisTransport()
110
+ except Exception:
111
+ self._artwork_cache.close()
112
+ raise
113
+ self._handler: SystemMediaCommandHandler | None = None
114
+ self._latest_snapshot: SystemMediaSnapshot | None = None
115
+ self._closed = False
116
+ try:
117
+ self._transport.set_command_handler(self._native_command)
118
+ self.publish(None)
119
+ except Exception:
120
+ self._closed = True
121
+ try:
122
+ self._transport.set_command_handler(None)
123
+ finally:
124
+ try:
125
+ self._transport.close()
126
+ finally:
127
+ self._artwork_cache.close()
128
+ raise
129
+
130
+ def set_command_handler(
131
+ self,
132
+ handler: SystemMediaCommandHandler | None,
133
+ ) -> None:
134
+ self._handler = handler
135
+
136
+ def publish(self, snapshot: SystemMediaSnapshot | None) -> None:
137
+ if self._closed:
138
+ return
139
+ self._latest_snapshot = snapshot
140
+ if snapshot is None:
141
+ self._artwork_cache.clear()
142
+ self._transport.publish(None)
143
+ return
144
+ self._transport.publish(self._state_for(snapshot))
145
+
146
+ def seeked(self, position_ms: int) -> None:
147
+ if not self._closed:
148
+ self._transport.seeked(max(0, position_ms) * 1_000)
149
+
150
+ def close(self) -> None:
151
+ if self._closed:
152
+ return
153
+ try:
154
+ self.publish(None)
155
+ except Exception:
156
+ logger.warning("Could not clear Linux MPRIS state", exc_info=True)
157
+ finally:
158
+ self._closed = True
159
+ self._handler = None
160
+ try:
161
+ self._transport.set_command_handler(None)
162
+ finally:
163
+ try:
164
+ self._transport.close()
165
+ finally:
166
+ self._artwork_cache.close()
167
+
168
+ def _state_for(self, snapshot: SystemMediaSnapshot) -> _MprisState:
169
+ track_path = _track_path(snapshot.entry_id)
170
+ metadata: dict[str, object] = {
171
+ "mpris:trackid": track_path,
172
+ # Linux Now Playing clients use the advertised duration together with
173
+ # Position and CanSeek to decide whether to expose a scrubber.
174
+ "mpris:length": max(0, snapshot.duration_ms) * 1_000,
175
+ "xesam:title": snapshot.title,
176
+ "xesam:artist": [snapshot.artist] if snapshot.artist else [],
177
+ "xesam:album": snapshot.album,
178
+ }
179
+ if snapshot.genre:
180
+ metadata["xesam:genre"] = [snapshot.genre]
181
+ if snapshot.track_number > 0:
182
+ metadata["xesam:trackNumber"] = snapshot.track_number
183
+ artwork_url = self._art_url_for(snapshot.artwork)
184
+ if artwork_url is not None:
185
+ metadata["mpris:artUrl"] = artwork_url
186
+ return _MprisState(
187
+ track_path=track_path,
188
+ playback_status="Playing" if snapshot.playing else "Paused",
189
+ metadata=metadata,
190
+ duration_us=max(0, snapshot.duration_ms) * 1_000,
191
+ position_us=min(
192
+ max(0, snapshot.position_ms),
193
+ max(0, snapshot.duration_ms),
194
+ )
195
+ * 1_000,
196
+ volume=snapshot.volume_percent / 100,
197
+ # MPRIS CanPlay is a player capability, not an enabled-state mirror.
198
+ # Keep it true during active playback so Host clients retain the player.
199
+ can_play=snapshot.can_play or snapshot.playing,
200
+ can_pause=snapshot.can_pause,
201
+ can_next=snapshot.can_next,
202
+ can_previous=snapshot.can_previous,
203
+ can_seek=snapshot.can_seek,
204
+ )
205
+
206
+ def _art_url_for(self, artwork: SystemMediaArtwork | None) -> str | None:
207
+ return self._artwork_cache.url_for(artwork)
208
+
209
+ def _native_command(self, method: str, arguments: tuple[object, ...]) -> None:
210
+ if self._closed:
211
+ return
212
+ snapshot = self._latest_snapshot
213
+ command: SystemMediaCommand | None = None
214
+ if method == "Play" and snapshot is not None and snapshot.can_play:
215
+ command = SystemMediaCommand(SystemMediaCommandKind.PLAY)
216
+ elif method == "Pause" and snapshot is not None and snapshot.can_pause:
217
+ command = SystemMediaCommand(SystemMediaCommandKind.PAUSE)
218
+ elif method == "PlayPause" and snapshot is not None:
219
+ command = SystemMediaCommand(SystemMediaCommandKind.TOGGLE_PLAY_PAUSE)
220
+ elif method == "Stop" and snapshot is not None and snapshot.playing:
221
+ # PlaybackController deliberately models current-item pause rather than
222
+ # a separate stopped state, which is the closest supported MPRIS intent.
223
+ command = SystemMediaCommand(SystemMediaCommandKind.PAUSE)
224
+ elif method == "Next" and snapshot is not None and snapshot.can_next:
225
+ command = SystemMediaCommand(SystemMediaCommandKind.NEXT)
226
+ elif method == "Previous" and snapshot is not None and snapshot.can_previous:
227
+ command = SystemMediaCommand(SystemMediaCommandKind.PREVIOUS)
228
+ elif (
229
+ method == "Seek"
230
+ and snapshot is not None
231
+ and snapshot.can_seek
232
+ and len(arguments) == 1
233
+ ):
234
+ offset_us = _integer(arguments[0])
235
+ if offset_us is not None:
236
+ position_ms = min(
237
+ max(0, snapshot.position_ms + round(offset_us / 1_000)),
238
+ max(0, snapshot.duration_ms),
239
+ )
240
+ command = SystemMediaCommand(
241
+ SystemMediaCommandKind.SEEK,
242
+ position_ms,
243
+ )
244
+ elif (
245
+ method == "SetPosition"
246
+ and snapshot is not None
247
+ and snapshot.can_seek
248
+ and len(arguments) == 2
249
+ and _object_path(arguments[0]) == _track_path(snapshot.entry_id)
250
+ ):
251
+ position_us = _integer(arguments[1])
252
+ if position_us is not None:
253
+ position_ms = min(
254
+ max(0, round(position_us / 1_000)),
255
+ max(0, snapshot.duration_ms),
256
+ )
257
+ command = SystemMediaCommand(
258
+ SystemMediaCommandKind.SEEK,
259
+ position_ms,
260
+ )
261
+ elif method == "SetVolume" and len(arguments) == 1:
262
+ volume = _number(arguments[0])
263
+ if volume is not None:
264
+ command = SystemMediaCommand(
265
+ SystemMediaCommandKind.SET_VOLUME,
266
+ volume_percent=min(100, max(0, round(volume * 100))),
267
+ )
268
+ handler = self._handler
269
+ if command is None or handler is None:
270
+ return
271
+ try:
272
+ handler(command)
273
+ except Exception:
274
+ logger.warning(
275
+ "Could not handle Linux MPRIS command %s",
276
+ method,
277
+ exc_info=True,
278
+ )
279
+
280
+
281
+ class _QtMprisTransport:
282
+ """Own the MPRIS well-known name and typed adaptors on Qt's session bus."""
283
+
284
+ def __init__(self) -> None:
285
+ self._connection = QDBusConnection.sessionBus()
286
+ if not self._connection.isConnected():
287
+ raise RuntimeError("The desktop D-Bus session bus is unavailable")
288
+ self._object = _QtMprisObject(self._connection)
289
+ self._closed = False
290
+ if not self._connection.registerService(_BUS_NAME):
291
+ raise RuntimeError(f"Could not register MPRIS service {_BUS_NAME}")
292
+ if not self._connection.registerObject(
293
+ _OBJECT_PATH,
294
+ self._object,
295
+ QDBusConnection.RegisterOption.ExportAdaptors,
296
+ ):
297
+ self._connection.unregisterService(_BUS_NAME)
298
+ raise RuntimeError(f"Could not register MPRIS object {_OBJECT_PATH}")
299
+
300
+ def set_command_handler(
301
+ self,
302
+ handler: _NativeCommandHandler | None,
303
+ ) -> None:
304
+ self._object.set_command_handler(handler)
305
+
306
+ def publish(self, state: _MprisState | None) -> None:
307
+ self._object.publish(state)
308
+
309
+ def seeked(self, position_us: int) -> None:
310
+ self._object.seeked(position_us)
311
+
312
+ def close(self) -> None:
313
+ if self._closed:
314
+ return
315
+ self._closed = True
316
+ self._object.set_command_handler(None)
317
+ self._connection.unregisterObject(_OBJECT_PATH)
318
+ self._connection.unregisterService(_BUS_NAME)
319
+
320
+
321
+ class _MprisArtworkCache:
322
+ """Keep at most one PNG available for MPRIS clients through a file URL."""
323
+
324
+ def __init__(self) -> None:
325
+ template = QDir(QDir.tempPath()).filePath("iopenpod-mpris-XXXXXX")
326
+ self._directory = QTemporaryDir(template)
327
+ if not self._directory.isValid():
328
+ logger.warning(
329
+ "Could not create the MPRIS artwork directory: %s",
330
+ self._directory.errorString(),
331
+ )
332
+ self._cache_key: str | None = None
333
+ self._path: str | None = None
334
+ self._url: str | None = None
335
+
336
+ def url_for(self, artwork: SystemMediaArtwork | None) -> str | None:
337
+ if artwork is None:
338
+ self.clear()
339
+ return None
340
+ if artwork.cache_key == self._cache_key:
341
+ return self._url
342
+ self._cache_key = artwork.cache_key
343
+ if not self._directory.isValid():
344
+ return None
345
+ old_path = self._path
346
+ self._path = None
347
+ self._url = None
348
+ try:
349
+ from PIL import Image
350
+
351
+ encoded = BytesIO()
352
+ Image.frombytes(
353
+ "RGB",
354
+ (artwork.width, artwork.height),
355
+ artwork.rgb888,
356
+ ).save(encoded, format="PNG")
357
+ digest = hashlib.sha256(artwork.cache_key.encode("utf-8")).hexdigest()[:16]
358
+ path = self._directory.filePath(f"artwork-{digest}.png")
359
+ output = QSaveFile(path)
360
+ if not output.open(QIODevice.OpenModeFlag.WriteOnly):
361
+ raise OSError(output.errorString())
362
+ payload = encoded.getvalue()
363
+ if output.write(payload) != len(payload) or not output.commit():
364
+ output.cancelWriting()
365
+ raise OSError(output.errorString())
366
+ self._path = path
367
+ self._url = QUrl.fromLocalFile(path).toString()
368
+ except Exception:
369
+ logger.warning(
370
+ "Could not convert album artwork for Linux MPRIS",
371
+ exc_info=True,
372
+ )
373
+ if old_path is not None and old_path != self._path:
374
+ QFile.remove(old_path)
375
+ return self._url
376
+
377
+ def clear(self) -> None:
378
+ path = self._path
379
+ self._cache_key = None
380
+ self._path = None
381
+ self._url = None
382
+ if path is not None:
383
+ QFile.remove(path)
384
+
385
+ def close(self) -> None:
386
+ self.clear()
387
+ self._directory.remove()
388
+
389
+
390
+ class _QtMprisObject(QObject):
391
+ """Own typed root/player adaptors and their shared MPRIS state."""
392
+
393
+ def __init__(
394
+ self,
395
+ connection: QDBusConnection,
396
+ elapsed_clock: _ElapsedClock | None = None,
397
+ ) -> None:
398
+ super().__init__()
399
+ self._connection = connection
400
+ self._handler: _NativeCommandHandler | None = None
401
+ self._state: _MprisState | None = None
402
+ self._position_clock = elapsed_clock or QElapsedTimer()
403
+ self.root_adaptor = _QtMprisRootAdaptor(self)
404
+ self.player_adaptor = _QtMprisPlayerAdaptor(self)
405
+
406
+ def set_command_handler(
407
+ self,
408
+ handler: _NativeCommandHandler | None,
409
+ ) -> None:
410
+ self._handler = handler
411
+
412
+ def publish(self, state: _MprisState | None) -> None:
413
+ before = self.player_properties()
414
+ self._state = state
415
+ if state is not None and state.playback_status == "Playing":
416
+ self._position_clock.start()
417
+ else:
418
+ self._position_clock.invalidate()
419
+ after = self.player_properties()
420
+ changed = {
421
+ name: value
422
+ for name, value in after.items()
423
+ if name != "Position" and before.get(name) != value
424
+ }
425
+ if changed:
426
+ self._properties_changed(_PLAYER_INTERFACE, changed)
427
+
428
+ def seeked(self, position_us: int) -> None:
429
+ self.player_adaptor.Seeked.emit(max(0, position_us))
430
+
431
+ def emit_command(self, method: str, arguments: tuple[object, ...]) -> None:
432
+ handler = self._handler
433
+ if handler is not None:
434
+ handler(method, arguments)
435
+
436
+ def player_properties(self) -> dict[str, object]:
437
+ state = self._state
438
+ return {
439
+ "PlaybackStatus": "Stopped" if state is None else state.playback_status,
440
+ "LoopStatus": "None",
441
+ "Rate": 1.0,
442
+ "Shuffle": False,
443
+ "Metadata": {} if state is None else dict(state.metadata),
444
+ "Volume": 1.0 if state is None else state.volume,
445
+ "Position": self._current_position_us(),
446
+ "MinimumRate": 1.0,
447
+ "MaximumRate": 1.0,
448
+ "CanGoNext": state is not None and state.can_next,
449
+ "CanGoPrevious": state is not None and state.can_previous,
450
+ "CanPlay": state is not None and state.can_play,
451
+ "CanPause": state is not None and state.can_pause,
452
+ "CanSeek": state is not None and state.can_seek,
453
+ "CanControl": True,
454
+ }
455
+
456
+ def _current_position_us(self) -> int:
457
+ state = self._state
458
+ if state is None:
459
+ return 0
460
+ elapsed_us = 0
461
+ if state.playback_status == "Playing" and self._position_clock.isValid():
462
+ elapsed_us = max(0, self._position_clock.nsecsElapsed() // 1_000)
463
+ return min(
464
+ max(0, state.position_us) + elapsed_us,
465
+ max(0, state.duration_us),
466
+ )
467
+
468
+ def _properties_changed(
469
+ self,
470
+ interface: str,
471
+ changed: Mapping[str, object],
472
+ ) -> None:
473
+ signal = QDBusMessage.createSignal(
474
+ _OBJECT_PATH,
475
+ _PROPERTIES_INTERFACE,
476
+ "PropertiesChanged",
477
+ )
478
+ signal.setArguments([interface, _variant_map(changed), _string_list(())])
479
+ if not self._connection.send(signal):
480
+ raise RuntimeError("Could not send MPRIS PropertiesChanged")
481
+
482
+
483
+ @ClassInfo(**{"D-Bus Interface": _ROOT_INTERFACE}) # type: ignore[arg-type]
484
+ class _QtMprisRootAdaptor(QDBusAbstractAdaptor): # type: ignore[operator]
485
+ """Export the MPRIS root interface with Qt-owned property replies."""
486
+
487
+ def __init__(self, owner: _QtMprisObject) -> None:
488
+ super().__init__(owner)
489
+
490
+ CanQuit = Property(bool, lambda _self: False)
491
+ Fullscreen = Property(bool, lambda _self: False)
492
+ CanSetFullscreen = Property(bool, lambda _self: False)
493
+ CanRaise = Property(bool, lambda _self: False)
494
+ HasTrackList = Property(bool, lambda _self: False)
495
+ Identity = Property(str, lambda _self: "iOpenPod")
496
+ SupportedUriSchemes = Property(
497
+ "QStringList", # type: ignore[arg-type]
498
+ lambda _self: [],
499
+ )
500
+ SupportedMimeTypes = Property(
501
+ "QStringList", # type: ignore[arg-type]
502
+ lambda _self: [],
503
+ )
504
+
505
+ @Slot()
506
+ def Raise(self) -> None:
507
+ """MPRIS Raise is unavailable because CanRaise is false."""
508
+
509
+ @Slot()
510
+ def Quit(self) -> None:
511
+ """MPRIS Quit is unavailable because CanQuit is false."""
512
+
513
+
514
+ @ClassInfo(**{"D-Bus Interface": _PLAYER_INTERFACE}) # type: ignore[arg-type]
515
+ class _QtMprisPlayerAdaptor(QDBusAbstractAdaptor): # type: ignore[operator]
516
+ """Export typed MPRIS player properties, commands, and signals."""
517
+
518
+ Seeked = Signal("qlonglong") # type: ignore[arg-type]
519
+
520
+ def __init__(self, owner: _QtMprisObject) -> None:
521
+ super().__init__(owner)
522
+ self._owner = owner
523
+
524
+ PlaybackStatus = Property(
525
+ str,
526
+ lambda self: cast("str", self._owner.player_properties()["PlaybackStatus"]),
527
+ )
528
+ LoopStatus = Property(str, lambda _self: "None")
529
+ Rate = Property(float, lambda _self: 1.0)
530
+ Shuffle = Property(bool, lambda _self: False)
531
+ Metadata = Property(
532
+ "QVariantMap", # type: ignore[arg-type]
533
+ lambda self: _qt_property_metadata(
534
+ cast(
535
+ "Mapping[str, object]",
536
+ self._owner.player_properties()["Metadata"],
537
+ )
538
+ ),
539
+ )
540
+ Volume = Property(
541
+ float,
542
+ lambda self: cast("float", self._owner.player_properties()["Volume"]),
543
+ lambda self, value: self._owner.emit_command("SetVolume", (value,)),
544
+ )
545
+ Position = Property(
546
+ "qlonglong", # type: ignore[arg-type]
547
+ lambda self: cast("int", self._owner.player_properties()["Position"]),
548
+ )
549
+ MinimumRate = Property(float, lambda _self: 1.0)
550
+ MaximumRate = Property(float, lambda _self: 1.0)
551
+ CanGoNext = Property(
552
+ bool,
553
+ lambda self: cast("bool", self._owner.player_properties()["CanGoNext"]),
554
+ )
555
+ CanGoPrevious = Property(
556
+ bool,
557
+ lambda self: cast("bool", self._owner.player_properties()["CanGoPrevious"]),
558
+ )
559
+ CanPlay = Property(
560
+ bool,
561
+ lambda self: cast("bool", self._owner.player_properties()["CanPlay"]),
562
+ )
563
+ CanPause = Property(
564
+ bool,
565
+ lambda self: cast("bool", self._owner.player_properties()["CanPause"]),
566
+ )
567
+ CanSeek = Property(
568
+ bool,
569
+ lambda self: cast("bool", self._owner.player_properties()["CanSeek"]),
570
+ )
571
+ CanControl = Property(bool, lambda _self: True)
572
+
573
+ @Slot()
574
+ def Next(self) -> None:
575
+ self._owner.emit_command("Next", ())
576
+
577
+ @Slot()
578
+ def Previous(self) -> None:
579
+ self._owner.emit_command("Previous", ())
580
+
581
+ @Slot()
582
+ def Pause(self) -> None:
583
+ self._owner.emit_command("Pause", ())
584
+
585
+ @Slot()
586
+ def PlayPause(self) -> None:
587
+ self._owner.emit_command("PlayPause", ())
588
+
589
+ @Slot()
590
+ def Stop(self) -> None:
591
+ self._owner.emit_command("Stop", ())
592
+
593
+ @Slot()
594
+ def Play(self) -> None:
595
+ self._owner.emit_command("Play", ())
596
+
597
+ @Slot("qlonglong")
598
+ def Seek(self, offset_us: int) -> None:
599
+ self._owner.emit_command("Seek", (offset_us,))
600
+
601
+ @Slot(QDBusObjectPath, "qlonglong")
602
+ def SetPosition(self, track_path: QDBusObjectPath, position_us: int) -> None:
603
+ self._owner.emit_command("SetPosition", (track_path, position_us))
604
+
605
+ @Slot(str)
606
+ def OpenUri(self, _uri: str) -> None:
607
+ """URI opening is unavailable because no URI schemes are advertised."""
608
+
609
+
610
+ def _variant_map(values: Mapping[str, object]) -> QDBusArgument:
611
+ argument = QDBusArgument()
612
+ argument.beginMap(_STRING_META_TYPE, _DBUS_VARIANT_META_TYPE)
613
+ for name, value in values.items():
614
+ argument.beginMapEntry()
615
+ argument.appendVariant(name)
616
+ argument.appendVariant(QDBusVariant(_dbus_value(name, value)))
617
+ argument.endMapEntry()
618
+ argument.endMap()
619
+ return argument
620
+
621
+
622
+ def _dbus_value(name: str, value: object) -> object:
623
+ if name == "Metadata" and isinstance(value, Mapping):
624
+ return _metadata_map(cast("Mapping[object, object]", value))
625
+ if name in {"SupportedUriSchemes", "SupportedMimeTypes"}:
626
+ return _string_list(cast("list[str]", value))
627
+ return value
628
+
629
+
630
+ def _metadata_value(name: str, value: object) -> object:
631
+ if name == "mpris:trackid":
632
+ return QDBusObjectPath(str(value))
633
+ if name in {"xesam:artist", "xesam:genre"}:
634
+ return _string_list(cast("list[str]", value))
635
+ return value
636
+
637
+
638
+ def _qt_property_metadata(values: Mapping[str, object]) -> dict[str, object]:
639
+ """Preserve D-Bus-only value types in Qt's exported QVariantMap."""
640
+ return {
641
+ name: QDBusObjectPath(str(value)) if name == "mpris:trackid" else value
642
+ for name, value in values.items()
643
+ }
644
+
645
+
646
+ def _metadata_map(values: Mapping[object, object]) -> QDBusArgument:
647
+ normalized = {str(name): value for name, value in values.items()}
648
+ argument = QDBusArgument()
649
+ argument.beginMap(_STRING_META_TYPE, _DBUS_VARIANT_META_TYPE)
650
+ for name, value in normalized.items():
651
+ argument.beginMapEntry()
652
+ argument.appendVariant(name)
653
+ argument.appendVariant(QDBusVariant(_metadata_value(name, value)))
654
+ argument.endMapEntry()
655
+ argument.endMap()
656
+ return argument
657
+
658
+
659
+ def _string_list(values: tuple[str, ...] | list[str]) -> QDBusArgument:
660
+ argument = QDBusArgument()
661
+ argument.beginArray(QMetaType.Type.QString)
662
+ for value in values:
663
+ argument.appendVariant(value)
664
+ argument.endArray()
665
+ return argument
666
+
667
+
668
+ def _track_path(entry_id: int) -> str:
669
+ return f"{_OBJECT_PATH}/Track/entry_{max(0, entry_id)}"
670
+
671
+
672
+ def _object_path(value: object) -> str | None:
673
+ if isinstance(value, QDBusObjectPath):
674
+ return value.path()
675
+ if isinstance(value, str):
676
+ return value
677
+ return None
678
+
679
+
680
+ def _unwrap_variant(value: object) -> object:
681
+ return value.variant() if isinstance(value, QDBusVariant) else value
682
+
683
+
684
+ def _integer(value: object) -> int | None:
685
+ value = _unwrap_variant(value)
686
+ if isinstance(value, bool) or not isinstance(value, int):
687
+ return None
688
+ return value
689
+
690
+
691
+ def _number(value: object) -> float | None:
692
+ value = _unwrap_variant(value)
693
+ if isinstance(value, bool) or not isinstance(value, int | float):
694
+ return None
695
+ converted = float(value)
696
+ return converted if math.isfinite(converted) else None
697
+
698
+
699
+ __all__ = ["LinuxSystemMediaSession"]