iopenpod 1.0.47__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 (363) hide show
  1. ArtworkDB_Parser/__init__.py +3 -0
  2. ArtworkDB_Parser/chunk_parser.py +66 -0
  3. ArtworkDB_Parser/constants.py +37 -0
  4. ArtworkDB_Parser/mhfd_parser.py +61 -0
  5. ArtworkDB_Parser/mhii_parser.py +52 -0
  6. ArtworkDB_Parser/mhli_parser.py +16 -0
  7. ArtworkDB_Parser/mhni_parser.py +196 -0
  8. ArtworkDB_Parser/mhod_parser.py +73 -0
  9. ArtworkDB_Parser/mhsd_parser.py +16 -0
  10. ArtworkDB_Parser/parser.py +15 -0
  11. ArtworkDB_Writer/__init__.py +63 -0
  12. ArtworkDB_Writer/art_extractor.py +303 -0
  13. ArtworkDB_Writer/artwork_writer.py +1139 -0
  14. ArtworkDB_Writer/ithmb_codecs.py +631 -0
  15. ArtworkDB_Writer/rgb565.py +250 -0
  16. GUI/__init__.py +3 -0
  17. GUI/app.py +1871 -0
  18. GUI/auto_updater.py +590 -0
  19. GUI/fonts.py +88 -0
  20. GUI/glyphs.py +104 -0
  21. GUI/hidpi.py +79 -0
  22. GUI/imgMaker.py +588 -0
  23. GUI/ipod_images.py +68 -0
  24. GUI/notifications.py +121 -0
  25. GUI/styles.py +1847 -0
  26. GUI/widgets/MBGridView.py +504 -0
  27. GUI/widgets/MBGridViewItem.py +180 -0
  28. GUI/widgets/MBListView.py +2878 -0
  29. GUI/widgets/__init__.py +0 -0
  30. GUI/widgets/backupBrowser.py +1245 -0
  31. GUI/widgets/browserChrome.py +115 -0
  32. GUI/widgets/devicePicker.py +444 -0
  33. GUI/widgets/dropOverlay.py +107 -0
  34. GUI/widgets/formatters.py +286 -0
  35. GUI/widgets/gridHeaderBar.py +141 -0
  36. GUI/widgets/musicBrowser.py +399 -0
  37. GUI/widgets/photoBrowser.py +1045 -0
  38. GUI/widgets/photoTile.py +190 -0
  39. GUI/widgets/photoViewer.py +358 -0
  40. GUI/widgets/playlistBrowser.py +1426 -0
  41. GUI/widgets/playlistEditor.py +1387 -0
  42. GUI/widgets/podcastBrowser.py +1856 -0
  43. GUI/widgets/podcastSearchDialog.py +343 -0
  44. GUI/widgets/scrollingLabel.py +96 -0
  45. GUI/widgets/selectiveSyncBrowser.py +1839 -0
  46. GUI/widgets/settingsPage.py +2533 -0
  47. GUI/widgets/sidebar.py +1096 -0
  48. GUI/widgets/syncReview.py +2828 -0
  49. GUI/widgets/trackListTitleBar.py +218 -0
  50. PodcastManager/__init__.py +1 -0
  51. PodcastManager/downloader.py +481 -0
  52. PodcastManager/feed_parser.py +287 -0
  53. PodcastManager/itunes_search.py +89 -0
  54. PodcastManager/models.py +241 -0
  55. PodcastManager/podcast_sync.py +629 -0
  56. PodcastManager/subscription_store.py +193 -0
  57. SQLiteDB_Writer/__init__.py +21 -0
  58. SQLiteDB_Writer/_helpers.py +64 -0
  59. SQLiteDB_Writer/cbk_writer.py +171 -0
  60. SQLiteDB_Writer/dynamic_writer.py +124 -0
  61. SQLiteDB_Writer/extras_writer.py +82 -0
  62. SQLiteDB_Writer/genius_writer.py +56 -0
  63. SQLiteDB_Writer/library_writer.py +1184 -0
  64. SQLiteDB_Writer/locations_writer.py +145 -0
  65. SQLiteDB_Writer/sqlite_writer.py +211 -0
  66. SyncEngine/__init__.py +135 -0
  67. SyncEngine/_db_io.py +244 -0
  68. SyncEngine/_formats.py +60 -0
  69. SyncEngine/_playlist_builder.py +397 -0
  70. SyncEngine/_track_conversion.py +388 -0
  71. SyncEngine/audio_fingerprint.py +454 -0
  72. SyncEngine/backup_manager.py +1268 -0
  73. SyncEngine/contracts.py +114 -0
  74. SyncEngine/dependency_manager.py +292 -0
  75. SyncEngine/fingerprint_diff_engine.py +1604 -0
  76. SyncEngine/integrity.py +328 -0
  77. SyncEngine/itunes_prefs.py +563 -0
  78. SyncEngine/mapping.py +389 -0
  79. SyncEngine/pc_library.py +1499 -0
  80. SyncEngine/photos.py +2092 -0
  81. SyncEngine/playlist_parser.py +181 -0
  82. SyncEngine/quick_writes.py +538 -0
  83. SyncEngine/scrobbler.py +590 -0
  84. SyncEngine/spl_evaluator.py +567 -0
  85. SyncEngine/sync_executor.py +2090 -0
  86. SyncEngine/transcode_cache.py +692 -0
  87. SyncEngine/transcoder.py +1274 -0
  88. app_core/__init__.py +122 -0
  89. app_core/bootstrap.py +220 -0
  90. app_core/context.py +157 -0
  91. app_core/controllers.py +335 -0
  92. app_core/device_identity.py +110 -0
  93. app_core/errors.py +19 -0
  94. app_core/jobs.py +1765 -0
  95. app_core/progress.py +210 -0
  96. app_core/runtime.py +928 -0
  97. app_core/services.py +440 -0
  98. app_core/sync_options.py +13 -0
  99. app_core/sync_plan_builder.py +203 -0
  100. app_core/sync_review_model.py +161 -0
  101. assets/fonts/NotoEmoji-Regular.ttf +0 -0
  102. assets/fonts/NotoSans-Italic.ttf +0 -0
  103. assets/fonts/NotoSans-Regular.ttf +0 -0
  104. assets/fonts/NotoSansMono-Regular.ttf +0 -0
  105. assets/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  106. assets/fonts/README.md +17 -0
  107. assets/glyphs/advisory-clean.svg +5 -0
  108. assets/glyphs/advisory-explicit.svg +8 -0
  109. assets/glyphs/annotation-dots.svg +6 -0
  110. assets/glyphs/annotation-warning.svg +5 -0
  111. assets/glyphs/archive.svg +5 -0
  112. assets/glyphs/arrow-down.svg +4 -0
  113. assets/glyphs/arrow-left.svg +4 -0
  114. assets/glyphs/arrow-right.svg +4 -0
  115. assets/glyphs/arrow-up.svg +4 -0
  116. assets/glyphs/book.svg +4 -0
  117. assets/glyphs/box.svg +7 -0
  118. assets/glyphs/broadcast.svg +8 -0
  119. assets/glyphs/chart.svg +6 -0
  120. assets/glyphs/check-circle.svg +4 -0
  121. assets/glyphs/check.svg +3 -0
  122. assets/glyphs/chevron-down.svg +3 -0
  123. assets/glyphs/chevron-left.svg +3 -0
  124. assets/glyphs/chevron-right.svg +3 -0
  125. assets/glyphs/chevron-up.svg +3 -0
  126. assets/glyphs/clock.svg +4 -0
  127. assets/glyphs/close-circle.svg +5 -0
  128. assets/glyphs/close.svg +4 -0
  129. assets/glyphs/cloud.svg +3 -0
  130. assets/glyphs/code.svg +4 -0
  131. assets/glyphs/download.svg +5 -0
  132. assets/glyphs/edit.svg +4 -0
  133. assets/glyphs/eject.svg +4 -0
  134. assets/glyphs/film.svg +10 -0
  135. assets/glyphs/filter.svg +3 -0
  136. assets/glyphs/flag.svg +3 -0
  137. assets/glyphs/floppy-disc.svg +5 -0
  138. assets/glyphs/folder.svg +4 -0
  139. assets/glyphs/grid.svg +6 -0
  140. assets/glyphs/heart.svg +3 -0
  141. assets/glyphs/home.svg +4 -0
  142. assets/glyphs/minus.svg +3 -0
  143. assets/glyphs/monitor.svg +4 -0
  144. assets/glyphs/music.svg +5 -0
  145. assets/glyphs/photo.svg +3 -0
  146. assets/glyphs/plus.svg +4 -0
  147. assets/glyphs/refresh.svg +6 -0
  148. assets/glyphs/search.svg +3 -0
  149. assets/glyphs/settings-sliders.svg +8 -0
  150. assets/glyphs/settings.svg +4 -0
  151. assets/glyphs/shield-warning.svg +5 -0
  152. assets/glyphs/spinner.svg +1 -0
  153. assets/glyphs/star.svg +3 -0
  154. assets/glyphs/tablet.svg +4 -0
  155. assets/glyphs/trash.svg +5 -0
  156. assets/glyphs/user.svg +4 -0
  157. assets/glyphs/video.svg +4 -0
  158. assets/glyphs/warning-triangle.svg +5 -0
  159. assets/glyphs/wifi-no-connection.svg +7 -0
  160. assets/glyphs/wifi.svg +6 -0
  161. assets/icons/icon-128.png +0 -0
  162. assets/icons/icon-16.png +0 -0
  163. assets/icons/icon-24.png +0 -0
  164. assets/icons/icon-256.png +0 -0
  165. assets/icons/icon-32.png +0 -0
  166. assets/icons/icon-48.png +0 -0
  167. assets/icons/icon-64.png +0 -0
  168. assets/icons/icon.ico +0 -0
  169. assets/ipod_images/iPod1.png +0 -0
  170. assets/ipod_images/iPod11-Black.png +0 -0
  171. assets/ipod_images/iPod11-Silver.png +0 -0
  172. assets/ipod_images/iPod11B-Black.png +0 -0
  173. assets/ipod_images/iPod12-Black.png +0 -0
  174. assets/ipod_images/iPod12-Blue.png +0 -0
  175. assets/ipod_images/iPod12-Green.png +0 -0
  176. assets/ipod_images/iPod12-Pink.png +0 -0
  177. assets/ipod_images/iPod12-Red.png +0 -0
  178. assets/ipod_images/iPod12-Silver.png +0 -0
  179. assets/ipod_images/iPod128.png +0 -0
  180. assets/ipod_images/iPod130-Blue.png +0 -0
  181. assets/ipod_images/iPod130-Green.png +0 -0
  182. assets/ipod_images/iPod130-Orange.png +0 -0
  183. assets/ipod_images/iPod130-Pink.png +0 -0
  184. assets/ipod_images/iPod130-Silver.png +0 -0
  185. assets/ipod_images/iPod130C-Blue.png +0 -0
  186. assets/ipod_images/iPod130C-Green.png +0 -0
  187. assets/ipod_images/iPod130C-Purple.png +0 -0
  188. assets/ipod_images/iPod130C-Red.png +0 -0
  189. assets/ipod_images/iPod130F-Blue.png +0 -0
  190. assets/ipod_images/iPod130F-Gold.png +0 -0
  191. assets/ipod_images/iPod130F-Green.png +0 -0
  192. assets/ipod_images/iPod130F-Pink.png +0 -0
  193. assets/ipod_images/iPod130F-Red.png +0 -0
  194. assets/ipod_images/iPod132-Blue.png +0 -0
  195. assets/ipod_images/iPod132-DarkGray.png +0 -0
  196. assets/ipod_images/iPod132-Green.png +0 -0
  197. assets/ipod_images/iPod132-Pink.png +0 -0
  198. assets/ipod_images/iPod132-Silver.png +0 -0
  199. assets/ipod_images/iPod132B-Silver.png +0 -0
  200. assets/ipod_images/iPod133-Blue.png +0 -0
  201. assets/ipod_images/iPod133-Green.png +0 -0
  202. assets/ipod_images/iPod133-Orange.png +0 -0
  203. assets/ipod_images/iPod133-Pink.png +0 -0
  204. assets/ipod_images/iPod133-Silver.png +0 -0
  205. assets/ipod_images/iPod133B-Blue.png +0 -0
  206. assets/ipod_images/iPod133B-DarkGray.png +0 -0
  207. assets/ipod_images/iPod133B-Green.png +0 -0
  208. assets/ipod_images/iPod133B-Pink.png +0 -0
  209. assets/ipod_images/iPod133B-Purple.png +0 -0
  210. assets/ipod_images/iPod133B-Red.png +0 -0
  211. assets/ipod_images/iPod133B-Silver.png +0 -0
  212. assets/ipod_images/iPod133B-SpaceGray.png +0 -0
  213. assets/ipod_images/iPod133B-Yellow.png +0 -0
  214. assets/ipod_images/iPod133D-Blue.png +0 -0
  215. assets/ipod_images/iPod133D-Gold.png +0 -0
  216. assets/ipod_images/iPod133D-Pink.png +0 -0
  217. assets/ipod_images/iPod133D-Red.png +0 -0
  218. assets/ipod_images/iPod133D-Silver.png +0 -0
  219. assets/ipod_images/iPod133D-SpaceGray.png +0 -0
  220. assets/ipod_images/iPod15-Black.png +0 -0
  221. assets/ipod_images/iPod15-Blue.png +0 -0
  222. assets/ipod_images/iPod15-Green.png +0 -0
  223. assets/ipod_images/iPod15-Orange.png +0 -0
  224. assets/ipod_images/iPod15-Pink.png +0 -0
  225. assets/ipod_images/iPod15-Purple.png +0 -0
  226. assets/ipod_images/iPod15-Red.png +0 -0
  227. assets/ipod_images/iPod15-Silver.png +0 -0
  228. assets/ipod_images/iPod15-Yellow.png +0 -0
  229. assets/ipod_images/iPod16-Black.png +0 -0
  230. assets/ipod_images/iPod16-Blue.png +0 -0
  231. assets/ipod_images/iPod16-Green.png +0 -0
  232. assets/ipod_images/iPod16-Orange.png +0 -0
  233. assets/ipod_images/iPod16-Pink.png +0 -0
  234. assets/ipod_images/iPod16-Purple.png +0 -0
  235. assets/ipod_images/iPod16-Red.png +0 -0
  236. assets/ipod_images/iPod16-Silver.png +0 -0
  237. assets/ipod_images/iPod16-Yellow.png +0 -0
  238. assets/ipod_images/iPod17-Blue.png +0 -0
  239. assets/ipod_images/iPod17-DarkGray.png +0 -0
  240. assets/ipod_images/iPod17-Green.png +0 -0
  241. assets/ipod_images/iPod17-Orange.png +0 -0
  242. assets/ipod_images/iPod17-Pink.png +0 -0
  243. assets/ipod_images/iPod17-Red.png +0 -0
  244. assets/ipod_images/iPod17-Silver.png +0 -0
  245. assets/ipod_images/iPod18-Blue.png +0 -0
  246. assets/ipod_images/iPod18-DarkGray.png +0 -0
  247. assets/ipod_images/iPod18-Green.png +0 -0
  248. assets/ipod_images/iPod18-Pink.png +0 -0
  249. assets/ipod_images/iPod18-Purple.png +0 -0
  250. assets/ipod_images/iPod18-Red.png +0 -0
  251. assets/ipod_images/iPod18-Silver.png +0 -0
  252. assets/ipod_images/iPod18-SpaceGray.png +0 -0
  253. assets/ipod_images/iPod18-Yellow.png +0 -0
  254. assets/ipod_images/iPod18A-Blue.png +0 -0
  255. assets/ipod_images/iPod18A-Gold.png +0 -0
  256. assets/ipod_images/iPod18A-Pink.png +0 -0
  257. assets/ipod_images/iPod18A-Red.png +0 -0
  258. assets/ipod_images/iPod18A-Silver.png +0 -0
  259. assets/ipod_images/iPod18A-SpaceGray.png +0 -0
  260. assets/ipod_images/iPod2.png +0 -0
  261. assets/ipod_images/iPod3-Blue.png +0 -0
  262. assets/ipod_images/iPod3-Gold.png +0 -0
  263. assets/ipod_images/iPod3-Green.png +0 -0
  264. assets/ipod_images/iPod3-Pink.png +0 -0
  265. assets/ipod_images/iPod3-Silver.png +0 -0
  266. assets/ipod_images/iPod3B-Blue.png +0 -0
  267. assets/ipod_images/iPod3B-Green.png +0 -0
  268. assets/ipod_images/iPod3B-Pink.png +0 -0
  269. assets/ipod_images/iPod4-BlackRed.png +0 -0
  270. assets/ipod_images/iPod4-White.png +0 -0
  271. assets/ipod_images/iPod5-BlackRed.png +0 -0
  272. assets/ipod_images/iPod5-White.png +0 -0
  273. assets/ipod_images/iPod6-Black.png +0 -0
  274. assets/ipod_images/iPod6-BlackRed.png +0 -0
  275. assets/ipod_images/iPod6-White.png +0 -0
  276. assets/ipod_images/iPod7-Black.png +0 -0
  277. assets/ipod_images/iPod7-White.png +0 -0
  278. assets/ipod_images/iPod9-Black.png +0 -0
  279. assets/ipod_images/iPod9-Blue.png +0 -0
  280. assets/ipod_images/iPod9-Green.png +0 -0
  281. assets/ipod_images/iPod9-Pink.png +0 -0
  282. assets/ipod_images/iPod9-Red.png +0 -0
  283. assets/ipod_images/iPod9-Silver.png +0 -0
  284. assets/ipod_images/iPodGeneric.png +0 -0
  285. iTunesDB_Parser/__init__.py +22 -0
  286. iTunesDB_Parser/_parsing.py +69 -0
  287. iTunesDB_Parser/chunk_parser.py +143 -0
  288. iTunesDB_Parser/exceptions.py +59 -0
  289. iTunesDB_Parser/ipod_library.py +134 -0
  290. iTunesDB_Parser/mhbd_parser.py +38 -0
  291. iTunesDB_Parser/mhia_parser.py +26 -0
  292. iTunesDB_Parser/mhii_parser.py +29 -0
  293. iTunesDB_Parser/mhip_parser.py +26 -0
  294. iTunesDB_Parser/mhit_parser.py +29 -0
  295. iTunesDB_Parser/mhod_parser.py +612 -0
  296. iTunesDB_Parser/mhsd_parser.py +29 -0
  297. iTunesDB_Parser/mhyp_parser.py +33 -0
  298. iTunesDB_Parser/otg.py +187 -0
  299. iTunesDB_Parser/parser.py +112 -0
  300. iTunesDB_Parser/playcounts.py +260 -0
  301. iTunesDB_Shared/__init__.py +33 -0
  302. iTunesDB_Shared/constants.py +378 -0
  303. iTunesDB_Shared/extraction.py +99 -0
  304. iTunesDB_Shared/field_base.py +414 -0
  305. iTunesDB_Shared/mhbd_defs.py +48 -0
  306. iTunesDB_Shared/mhia_defs.py +21 -0
  307. iTunesDB_Shared/mhii_defs.py +18 -0
  308. iTunesDB_Shared/mhip_defs.py +26 -0
  309. iTunesDB_Shared/mhit_defs.py +141 -0
  310. iTunesDB_Shared/mhod_defs.py +586 -0
  311. iTunesDB_Shared/mhsd_defs.py +15 -0
  312. iTunesDB_Shared/mhyp_defs.py +35 -0
  313. iTunesDB_Writer/__init__.py +148 -0
  314. iTunesDB_Writer/hash58.py +280 -0
  315. iTunesDB_Writer/hash72.py +476 -0
  316. iTunesDB_Writer/hashab.py +287 -0
  317. iTunesDB_Writer/mhbd_writer.py +1151 -0
  318. iTunesDB_Writer/mhip_writer.py +138 -0
  319. iTunesDB_Writer/mhit_writer.py +385 -0
  320. iTunesDB_Writer/mhla_writer.py +217 -0
  321. iTunesDB_Writer/mhli_writer.py +136 -0
  322. iTunesDB_Writer/mhlp_writer.py +238 -0
  323. iTunesDB_Writer/mhlt_writer.py +59 -0
  324. iTunesDB_Writer/mhod52_writer.py +334 -0
  325. iTunesDB_Writer/mhod_spl_writer.py +283 -0
  326. iTunesDB_Writer/mhod_writer.py +434 -0
  327. iTunesDB_Writer/mhsd_writer.py +101 -0
  328. iTunesDB_Writer/mhyp_writer.py +538 -0
  329. iTunesDB_Writer/wasm/calcHashAB.wasm +0 -0
  330. infrastructure/__init__.py +1 -0
  331. infrastructure/settings_paths.py +83 -0
  332. infrastructure/settings_persistence.py +93 -0
  333. infrastructure/settings_runtime.py +441 -0
  334. infrastructure/settings_schema.py +94 -0
  335. infrastructure/settings_secrets.py +127 -0
  336. infrastructure/version.py +18 -0
  337. iopenpod-1.0.47.dist-info/METADATA +234 -0
  338. iopenpod-1.0.47.dist-info/RECORD +363 -0
  339. iopenpod-1.0.47.dist-info/WHEEL +4 -0
  340. iopenpod-1.0.47.dist-info/entry_points.txt +2 -0
  341. iopenpod-1.0.47.dist-info/licenses/LICENSE +21 -0
  342. ipod_device/__init__.py +131 -0
  343. ipod_device/artwork.py +173 -0
  344. ipod_device/artwork_presets.py +114 -0
  345. ipod_device/authority.py +671 -0
  346. ipod_device/capabilities.py +737 -0
  347. ipod_device/checksum.py +41 -0
  348. ipod_device/diagnostic_log.py +178 -0
  349. ipod_device/dump.py +496 -0
  350. ipod_device/eject.py +1007 -0
  351. ipod_device/images.py +409 -0
  352. ipod_device/info.py +2408 -0
  353. ipod_device/lookup.py +171 -0
  354. ipod_device/models.py +509 -0
  355. ipod_device/scanner.py +2496 -0
  356. ipod_device/sysinfo.py +528 -0
  357. ipod_device/usb_backend.py +132 -0
  358. ipod_device/vpd_iokit.py +618 -0
  359. ipod_device/vpd_libusb.py +1354 -0
  360. ipod_device/vpd_linux.py +267 -0
  361. ipod_device/vpd_usb_control.py +215 -0
  362. ipod_device/vpd_windows.py +310 -0
  363. main.py +9 -0
@@ -0,0 +1,3 @@
1
+ from .parser import parse_artworkdb
2
+
3
+ __all__ = ["parse_artworkdb"]
@@ -0,0 +1,66 @@
1
+ import struct
2
+ from typing import Any
3
+
4
+
5
+ def parse_chunk(data, offset) -> dict[str, Any]:
6
+ chunk_type = data[offset: offset + 4].decode("utf-8")
7
+ header_length = struct.unpack("<I", data[offset + 4: offset + 8])[0]
8
+ chunk_length = struct.unpack("<I", data[offset + 8: offset + 12])[0]
9
+
10
+ match chunk_type:
11
+ case "mhfd":
12
+ # Data file
13
+ from .mhfd_parser import parse_mhfd
14
+
15
+ result = parse_mhfd(data, offset, header_length, chunk_length)
16
+ return result
17
+ case "mhsd":
18
+ # Data Set
19
+ from .mhsd_parser import parse_mhsd
20
+
21
+ result = parse_mhsd(data, offset, header_length, chunk_length)
22
+ return result
23
+ case "mhli":
24
+ # Image List
25
+ from .mhli_parser import parse_mhli
26
+
27
+ result = parse_mhli(data, offset, header_length, chunk_length)
28
+ return result
29
+ case "mhii":
30
+ # Image Item
31
+ from .mhii_parser import parse_imageItem
32
+
33
+ result = parse_imageItem(data, offset, header_length, chunk_length)
34
+ return result
35
+ case "mhni":
36
+ # Image Name
37
+ from .mhni_parser import parse_mhni
38
+
39
+ result = parse_mhni(data, offset, header_length, chunk_length)
40
+ return result
41
+ case "mhla":
42
+ # Photo Album List
43
+ return {}
44
+ case "mhba":
45
+ # Photo Album
46
+ return {}
47
+ case "mhia":
48
+ # Photo Album Item
49
+ return {}
50
+ case "mhlf":
51
+ # File List
52
+ return {}
53
+ case "mhif":
54
+ # File Item
55
+ return {}
56
+ case "mhod":
57
+ # Data Object
58
+ from .mhod_parser import parse_mhod
59
+
60
+ result = parse_mhod(data, offset, header_length, chunk_length)
61
+ return result
62
+ case "mhaf":
63
+ # Unknown
64
+ return {}
65
+ case _:
66
+ raise ValueError(f"Unknown chunk type: {chunk_type}")
@@ -0,0 +1,37 @@
1
+ # maps the id used in mhsd to the proper header marker
2
+ chunk_type_map = {
3
+ 1: "mhli", # Image List chunk
4
+ 2: "mhla", # Photo ALbum List chunk
5
+ 3: "mhlf", # File List Chunk
6
+ }
7
+
8
+ # maps the chunk header marker to a readable name
9
+ # the identifier appears to be backward, I estimate that it should read like
10
+ # DataBaseHeaderMarker(DBHM) and DataStructureHeaderMarker(DSHM) and
11
+ # TrackListHeaderMarker(TLHM)...
12
+ identifier_readable_map = {
13
+ "mhfd": "Data File",
14
+ "mhsd": "Data Set",
15
+ "mhli": "Image List",
16
+ "mhii": "Image Item",
17
+ "mhni": "Image Name",
18
+ "mhla": "Photo Album List",
19
+ "mhba": "Photo Album",
20
+ "mhia": "Photo Album Item",
21
+ "mhlf": "File List",
22
+ "mhif": "File List Item",
23
+ "mhod": "Data Object",
24
+ }
25
+
26
+ # maps the mhod type to its readable name
27
+ # There are 2 groups of types of MHODs in the ArtworkDB:
28
+ # container MHODs contain a MHNI as a child,
29
+ # while 'normal' string MHODs contain a string.
30
+
31
+ mhod_type_map = {
32
+ 1: {"type": "String", "name": "Album Name"},
33
+ 2: {"type": "Container", "name": "Thumbnail Image"},
34
+ 3: {"type": "String", "name": "File Name"},
35
+ 5: {"type": "Container", "name": "Full Res Image"},
36
+ 6: {"type": "Container", "name": "UNK MHOD 6"},
37
+ }
@@ -0,0 +1,61 @@
1
+ import base64
2
+ import struct
3
+ from typing import Any
4
+
5
+
6
+ def parse_mhfd(data, offset, header_length, chunk_length) -> dict[str, Any]:
7
+ from .chunk_parser import parse_chunk
8
+ from .constants import chunk_type_map
9
+
10
+ datafile: dict[str, Any] = {}
11
+
12
+ datafile["unk1"] = struct.unpack(
13
+ "<I", data[offset + 12:offset + 16])[0] # always 0
14
+ datafile["unk2"] = struct.unpack(
15
+ "<I", data[offset + 16:offset + 20])[0] # 1 until iTunes 4.9. 2 after
16
+
17
+ datafile["childCount"] = struct.unpack("<I", data[offset + 20:offset + 24])[0]
18
+
19
+ datafile["unk3"] = struct.unpack(
20
+ "<I", data[offset + 24:offset + 28])[0] # always 0
21
+
22
+ # ID of last mhii + 1. On iPod video seems to be last mhii + mhba count + 1
23
+ datafile["next_mhii_id"] = struct.unpack(
24
+ "<I", data[offset + 28:offset + 32])[0]
25
+
26
+ datafile["unk4"] = struct.unpack("<Q", data[offset + 32:offset + 40])[0]
27
+ datafile["unk5"] = struct.unpack("<Q", data[offset + 40:offset + 48])[0]
28
+
29
+ datafile["unk6"] = struct.unpack(
30
+ "<I", data[offset + 48:offset + 52])[0] # always 2
31
+ datafile["unk7"] = struct.unpack(
32
+ "<I", data[offset + 52:offset + 56])[0] # always 0
33
+ datafile["unk8"] = struct.unpack(
34
+ "<I", data[offset + 56:offset + 60])[0] # always 0
35
+
36
+ datafile["unk9"] = struct.unpack("<I", data[offset + 60:offset + 64])[0]
37
+ datafile["unk10"] = struct.unpack("<I", data[offset + 64:offset + 68])[0]
38
+
39
+ # parse children
40
+ next_offset = offset + header_length
41
+ for i in range(datafile["childCount"]):
42
+ childResult = parse_chunk(data, next_offset)
43
+ next_offset = childResult["nextOffset"]
44
+ resultData = childResult["result"]
45
+ resultType = childResult["datasetType"]
46
+ datafile[chunk_type_map[resultType]] = resultData
47
+
48
+ # Convert byte fields to base64 for JSON serialization
49
+ def replace_bytes_with_base64(data: Any) -> Any:
50
+ if isinstance(data, dict): # If it's a dictionary, process each key-value pair
51
+ return {key: replace_bytes_with_base64(value) for key, value in data.items()}
52
+ elif isinstance(data, list): # If it's a list, process each item
53
+ return [replace_bytes_with_base64(item) for item in data]
54
+ elif isinstance(data, bytes): # If it's bytes, encode to Base64
55
+ return base64.b64encode(data).decode("utf-8")
56
+ else:
57
+ return data # If it's not bytes, return as-is
58
+
59
+ cleaned_database = replace_bytes_with_base64(datafile)
60
+
61
+ return {"nextOffset": next_offset, "result": cleaned_database}
@@ -0,0 +1,52 @@
1
+ import struct
2
+
3
+
4
+ def parse_imageItem(data, offset, header_length, chunk_length) -> dict:
5
+ from .chunk_parser import parse_chunk
6
+ from .constants import mhod_type_map
7
+
8
+ image = {}
9
+
10
+ childCount = struct.unpack("<I", data[offset + 12: offset + 16])[0]
11
+
12
+ image["img_id"] = struct.unpack("<I", data[offset + 16: offset + 20])[0]
13
+
14
+ # First mhii is 0x40, second is 0x41, ...
15
+ # (on mobile phones the first mhii appears to be 0x64, second 0x65, ...)
16
+
17
+ image["songId"] = struct.unpack("<Q", data[offset + 20: offset + 28])[0]
18
+ # unique ID that matches the db_track_id field in the iTunesDB Track Item record.
19
+ # this is what is used to map the ArtworkDB items to iTunesDB Items.
20
+
21
+ image["unk1"] = struct.unpack(
22
+ "<I", data[offset + 28: offset + 32])[0] # always 0
23
+
24
+ image["rating"] = struct.unpack("<I", data[offset + 32: offset + 36])[
25
+ 0
26
+ ] # iPhoto rating x20
27
+
28
+ image["unk2"] = struct.unpack(
29
+ "<I", data[offset + 36: offset + 40])[0] # always 0
30
+
31
+ image["originalDate"] = struct.unpack(
32
+ "<I", data[offset + 40: offset + 44])[0]
33
+ # always 0 in ArtworkDB. creation timestamp of file in photo database.
34
+
35
+ image["exifTakenDate"] = struct.unpack(
36
+ "<I", data[offset + 44: offset + 48])[0]
37
+ # always 0 in ArtworkDB. timestamp of taken time probably from exif in photo db.
38
+
39
+ image["srcImgSize"] = struct.unpack(
40
+ "<I", data[offset + 48: offset + 52])[0]
41
+ # size in bytes of the original source image.
42
+
43
+ # Parse Children
44
+ next_offset = offset + header_length
45
+ for i in range(childCount):
46
+ response = parse_chunk(data, next_offset)
47
+ next_offset = response["nextOffset"]
48
+
49
+ mhodData = response["result"]
50
+ image[mhod_type_map[mhodData["mhodType"]]["name"]] = mhodData
51
+
52
+ return {"nextOffset": offset + chunk_length, "result": image}
@@ -0,0 +1,16 @@
1
+ from typing import Any
2
+
3
+
4
+ def parse_mhli(data, offset, header_length, imageCount) -> dict[str, Any]:
5
+ from .chunk_parser import parse_chunk
6
+
7
+ imageList = []
8
+
9
+ # Parse Children
10
+ next_offset = offset + header_length
11
+ for i in range(imageCount):
12
+ response = parse_chunk(data, next_offset)
13
+ next_offset = response["nextOffset"]
14
+ imageList.append(response["result"])
15
+
16
+ return {"nextOffset": next_offset, "result": imageList}
@@ -0,0 +1,196 @@
1
+ import struct
2
+
3
+ from ipod_device import ITHMB_FORMAT_MAP
4
+ from ipod_device.artwork_presets import artwork_format_candidates
5
+
6
+
7
+ def _expected_img_size_bytes(candidate) -> int:
8
+ pf = candidate.pixel_format
9
+ if pf in (
10
+ "RGB565_LE",
11
+ "RGB565_BE",
12
+ "RGB565_BE_90",
13
+ "UYVY",
14
+ "RGB555_LE",
15
+ "RGB555_BE",
16
+ ):
17
+ return candidate.row_bytes * candidate.height
18
+ if pf.startswith("REC_RGB555"):
19
+ return candidate.row_bytes * candidate.height
20
+ if pf == "I420_LE":
21
+ w = candidate.width & ~1
22
+ h = candidate.height & ~1
23
+ return (w * h * 3) // 2
24
+ if pf == "JPEG":
25
+ return 0
26
+ return 0
27
+
28
+
29
+ def parse_mhni(data, offset, header_length, chunk_length) -> dict:
30
+ from .chunk_parser import parse_chunk
31
+
32
+ imageName = {}
33
+
34
+ childCount = struct.unpack("<I", data[offset + 12: offset + 16])[0]
35
+ # a type 3 mhod
36
+
37
+ imageName["correlationID"] = struct.unpack(
38
+ "<I", data[offset + 16: offset + 20])[0]
39
+ # maps to mhif correlationID. generates name of the file
40
+ # Also serves as the format_id to identify image format (libgpod approach)
41
+
42
+ imageName["ithmbOffset"] = struct.unpack(
43
+ "<I", data[offset + 20: offset + 24])[0]
44
+ # where the image data starts in the .ithmb file
45
+
46
+ imageName["imgSize"] = struct.unpack(
47
+ "<I", data[offset + 24: offset + 28])[0]
48
+ # in bytes
49
+
50
+ imageName["verticalPadding"] = struct.unpack("<h", data[offset + 28: offset + 30])[
51
+ 0
52
+ ]
53
+ imageName["horizontalPadding"] = struct.unpack(
54
+ "<h", data[offset + 30: offset + 32]
55
+ )[0]
56
+
57
+ imageName["imageHeight"] = struct.unpack(
58
+ "<H", data[offset + 32: offset + 34])[0]
59
+ imageName["imageWidth"] = struct.unpack(
60
+ "<H", data[offset + 34: offset + 36])[0]
61
+
62
+ imageName["unk1"] = struct.unpack("<I", data[offset + 36: offset + 40])[0]
63
+ # always 0
64
+
65
+ imageName["imgSize2"] = struct.unpack(
66
+ "<I", data[offset + 40: offset + 44])[0]
67
+ # Same as imgSize, seen after iTunes 7.4
68
+
69
+ # Estimate pixmap dimensions (for debugging/fallback)
70
+ imageName["estimatedPixmapHeight"] = (
71
+ imageName["verticalPadding"] + imageName["imageHeight"]
72
+ )
73
+ imageName["estimatedPixmapWidth"] = (
74
+ imageName["horizontalPadding"] + imageName["imageWidth"]
75
+ )
76
+
77
+ image_format = None
78
+
79
+ format_id = imageName["correlationID"]
80
+ candidates = artwork_format_candidates()
81
+ same_id_candidates = [
82
+ candidate
83
+ for candidate in candidates
84
+ if candidate.format_id == format_id
85
+ ]
86
+ est_w = imageName["estimatedPixmapWidth"]
87
+ est_h = imageName["estimatedPixmapHeight"]
88
+ img_size = imageName["imgSize"]
89
+
90
+ # Prefer correlationID mapping only when it is plausibly compatible with
91
+ # observed MHNI geometry or payload size. Some legacy/corrupt databases
92
+ # carry mismatched correlation IDs (e.g. 140x140 metadata for ~57x58 data).
93
+ if same_id_candidates:
94
+ best_match = None
95
+ best_match_score = float("inf")
96
+ for af in same_id_candidates:
97
+ expected = _expected_img_size_bytes(af)
98
+ corr_exact = expected > 0 and expected == img_size
99
+ corr_close = (
100
+ est_w > 0
101
+ and est_h > 0
102
+ and (
103
+ (abs(est_w - af.width) <= 2 and abs(est_h - af.height) <= 2)
104
+ or (abs(est_w - af.height) <= 2 and abs(est_h - af.width) <= 2)
105
+ )
106
+ )
107
+ # For variable-sized payloads (e.g. JPEG), trust correlation ID + geometry.
108
+ if expected == 0 and corr_close:
109
+ corr_exact = True
110
+ if not (corr_exact or corr_close):
111
+ continue
112
+
113
+ size_delta = abs(img_size - expected) if expected > 0 else 0
114
+ dim_delta = abs(est_w - af.width) + abs(est_h - af.height)
115
+ score = size_delta + dim_delta
116
+ if score < best_match_score:
117
+ best_match = af
118
+ best_match_score = score
119
+
120
+ if best_match is not None:
121
+ image_format = {
122
+ "height": best_match.height,
123
+ "width": best_match.width,
124
+ "format": best_match.pixel_format,
125
+ "description": best_match.description,
126
+ "format_id": format_id,
127
+ }
128
+
129
+ if image_format is None:
130
+ af = ITHMB_FORMAT_MAP.get(format_id)
131
+ else:
132
+ af = None
133
+
134
+ if image_format is None and af is not None:
135
+ expected = _expected_img_size_bytes(af)
136
+ corr_exact = expected > 0 and expected == img_size
137
+ corr_close = (
138
+ est_w > 0
139
+ and est_h > 0
140
+ and (
141
+ (abs(est_w - af.width) <= 2 and abs(est_h - af.height) <= 2)
142
+ or (abs(est_w - af.height) <= 2 and abs(est_h - af.width) <= 2)
143
+ )
144
+ )
145
+ # For variable-sized payloads (e.g. JPEG), trust correlation ID + geometry.
146
+ if expected == 0 and corr_close:
147
+ corr_exact = True
148
+ if corr_exact or corr_close:
149
+ image_format = {
150
+ "height": af.height,
151
+ "width": af.width,
152
+ "format": af.pixel_format,
153
+ "description": af.description,
154
+ "format_id": format_id,
155
+ }
156
+
157
+ if image_format is None:
158
+ # Fallback: choose the candidate that best matches observed geometry
159
+ # and payload pixel count.
160
+ best_candidate = None
161
+ best_score = float("inf")
162
+
163
+ for candidate in candidates:
164
+ dim_diff = abs(est_h - candidate.height) + abs(est_w - candidate.width)
165
+ expected = _expected_img_size_bytes(candidate)
166
+ if expected > 0:
167
+ size_delta = abs(img_size - expected)
168
+ score = dim_diff + (
169
+ size_delta / max(1, candidate.row_bytes, candidate.width)
170
+ )
171
+ else:
172
+ score = dim_diff
173
+ if score < best_score:
174
+ best_score = score
175
+ best_candidate = candidate
176
+
177
+ if best_candidate is not None:
178
+ image_format = {
179
+ "height": best_candidate.height,
180
+ "width": best_candidate.width,
181
+ "format": best_candidate.pixel_format,
182
+ "description": best_candidate.description,
183
+ "format_id": best_candidate.format_id,
184
+ "score": best_score,
185
+ }
186
+
187
+ imageName["image_format"] = image_format
188
+
189
+ # parse children
190
+ next_offset = offset + header_length
191
+ for i in range(childCount):
192
+ response = parse_chunk(data, next_offset)
193
+ next_offset = response["nextOffset"]
194
+ imageName[response["result"]["mhodType"]] = response["result"]
195
+
196
+ return {"nextOffset": offset + chunk_length, "result": imageName}
@@ -0,0 +1,73 @@
1
+ import struct
2
+
3
+
4
+ def parse_mhod(data, offset, header_length, chunk_length) -> dict:
5
+ from .constants import mhod_type_map
6
+ from .chunk_parser import parse_chunk
7
+
8
+ dataObject = {}
9
+
10
+ dataObject["mhodType"] = struct.unpack(
11
+ "<H", data[offset + 12: offset + 14])[0]
12
+
13
+ # unk0 = struct.unpack("<B", data[offset + 14: offset + 15])[0] # always 0
14
+
15
+ # paddingLength = struct.unpack("<B", data[offset + 15: offset + 16])[0]
16
+ # all MHOD pad to be be a multiple of 4. the length will be 0,1,3
17
+
18
+ # There is a bug in the iPod code that causes an MHBA to have an MHOD
19
+ # of type 2 that is ont a container but is actually a string
20
+
21
+ # MHOD type 2 contain a MHNI that cotains a MHOD type 3 with a thmbnl ref
22
+ # MHOD type 5 contain a MHNI that cotains a MHOD type 3 with a fulrez ref
23
+
24
+ if dataObject["mhodType"] not in mhod_type_map:
25
+ return {
26
+ "nextOffset": offset + chunk_length,
27
+ "result": {"mhodType": dataObject["mhodType"], "_unknown": True},
28
+ }
29
+
30
+ match mhod_type_map[dataObject["mhodType"]]["type"]:
31
+ case "String":
32
+ content_offset = offset + header_length
33
+
34
+ stringByteLength = struct.unpack(
35
+ "<I", data[content_offset: content_offset + 4])[0]
36
+
37
+ # Encoding byte at content_offset+4 (ArtworkDB_MhodHeaderString.encoding)
38
+ # Per libgpod db-itunes-parser.h: 0,1 = UTF-8; 2 = UTF-16-LE
39
+ encoding = data[content_offset + 4]
40
+
41
+ # content_offset+8: unknown (always 0)
42
+
43
+ stringContent = data[
44
+ content_offset + 12: content_offset + 12 + stringByteLength]
45
+
46
+ # padding would be offset+stringByteLength:offset+paddingLength
47
+ # but for the purposes of parsing it is not needed.
48
+
49
+ if encoding == 2:
50
+ string_decode = stringContent.decode("utf-16-le", errors="replace")
51
+ else:
52
+ string_decode = stringContent.decode("utf-8", errors="replace")
53
+
54
+ dataObject[mhod_type_map[dataObject["mhodType"]]
55
+ ["name"]] = string_decode
56
+
57
+ return {"nextOffset": offset + chunk_length, "result": dataObject}
58
+ case "Container":
59
+
60
+ # parse children (MHNI)
61
+ next_offset = offset + header_length
62
+ childResult = parse_chunk(data, next_offset)
63
+
64
+ dataObject[mhod_type_map[dataObject["mhodType"]]
65
+ ["name"]] = childResult
66
+
67
+ return {"nextOffset": offset + chunk_length, "result": dataObject}
68
+
69
+ case _:
70
+ return {
71
+ "nextOffset": offset + chunk_length,
72
+ "result": {"mhodType": "ERROR"},
73
+ }
@@ -0,0 +1,16 @@
1
+ import struct
2
+
3
+
4
+ def parse_mhsd(data, offset, header_length, chunk_length) -> dict:
5
+ from .chunk_parser import parse_chunk
6
+
7
+ # ArtworkDB MHSD index is u16, not u32 like iTunesDB
8
+ # (per libgpod ArtworkDB_MhsdHeader struct)
9
+ datasetType = struct.unpack("<H", data[offset + 12:offset + 14])[0]
10
+
11
+ # Parse Child
12
+ next_offset = offset + header_length
13
+ childResult = parse_chunk(data, next_offset)
14
+ # Extract the actual result from the wrapper
15
+ result = childResult.get("result", childResult)
16
+ return {"datasetType": datasetType, "result": result, "nextOffset": offset + chunk_length}
@@ -0,0 +1,15 @@
1
+ def parse_artworkdb(file) -> dict:
2
+ from .chunk_parser import parse_chunk
3
+
4
+ if isinstance(file, str): # If it's a file path, open the file
5
+ with open(file, "rb") as f:
6
+ data = f.read()
7
+ elif hasattr(file, "read"): # If it's a file-like object, read it directly
8
+ data = file.read()
9
+ else:
10
+ raise TypeError("file must be a path (str) or a file-like object")
11
+
12
+ result = parse_chunk(data, 0)
13
+
14
+ # Return just the parsed data, not the wrapper with nextOffset
15
+ return result.get("result", result)
@@ -0,0 +1,63 @@
1
+ """
2
+ ArtworkDB Writer for iPod Classic/Nano.
3
+
4
+ Writes ArtworkDB binary files and .ithmb image files from PC music
5
+ file embedded album art.
6
+
7
+ Usage:
8
+ from ArtworkDB_Writer import write_artworkdb
9
+
10
+ # pc_file_paths maps track db_track_id → PC source file path
11
+ db_track_id_to_art = write_artworkdb(
12
+ ipod_path="/media/ipod",
13
+ tracks=track_list,
14
+ pc_file_paths={12345: "/home/user/Music/song.mp3", ...},
15
+ )
16
+
17
+ # Then set mhiiLink and artworkSize on each track in iTunesDB
18
+ for track in tracks:
19
+ art_info = db_track_id_to_art.get(track.db_track_id)
20
+ if art_info:
21
+ img_id, src_size = art_info
22
+ track.mhii_link = img_id
23
+ track.artwork_size = src_size
24
+ """
25
+
26
+ from .artwork_writer import write_artworkdb, ArtworkEntry
27
+ from .art_extractor import extract_art, art_hash
28
+ from .rgb565 import (
29
+ convert_art_for_ipod,
30
+ image_from_bytes,
31
+ rgb888_to_rgb565,
32
+ get_artwork_formats,
33
+ IPOD_CLASSIC_FORMATS,
34
+ IPOD_NANO_1G2G_FORMATS,
35
+ IPOD_PHOTO_FORMATS,
36
+ IPOD_VIDEO_FORMATS,
37
+ IPOD_NANO_4G_FORMATS,
38
+ IPOD_NANO_5G_FORMATS,
39
+ ALL_KNOWN_FORMATS,
40
+ )
41
+ # Re-export canonical format lookups from ipod_device
42
+ from ipod_device import ITHMB_FORMAT_MAP, ITHMB_SIZE_MAP, ithmb_formats_for_device
43
+
44
+ __all__ = [
45
+ 'write_artworkdb',
46
+ 'ArtworkEntry',
47
+ 'extract_art',
48
+ 'art_hash',
49
+ 'convert_art_for_ipod',
50
+ 'image_from_bytes',
51
+ 'rgb888_to_rgb565',
52
+ 'get_artwork_formats',
53
+ 'IPOD_CLASSIC_FORMATS',
54
+ 'IPOD_NANO_1G2G_FORMATS',
55
+ 'IPOD_PHOTO_FORMATS',
56
+ 'IPOD_VIDEO_FORMATS',
57
+ 'IPOD_NANO_4G_FORMATS',
58
+ 'IPOD_NANO_5G_FORMATS',
59
+ 'ALL_KNOWN_FORMATS',
60
+ 'ITHMB_FORMAT_MAP',
61
+ 'ITHMB_SIZE_MAP',
62
+ 'ithmb_formats_for_device',
63
+ ]