itunes.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import os
  2. import urllib.parse
  3. import xml.etree.ElementTree
  4. import dateutil.parser
  5. class XmlDict:
  6. def __init__(self, node):
  7. assert isinstance(node, xml.etree.ElementTree.Element), node
  8. assert node.tag == "dict", node.tag
  9. self._node = node
  10. def _get_value_node(self, key):
  11. # WORKAROUND method getnext() is sadly not available
  12. for child_idx, child_node in enumerate(self._node):
  13. if child_node.tag == "key" and child_node.text == key:
  14. return self._node[child_idx + 1]
  15. raise KeyError(key)
  16. @staticmethod
  17. def _parse_value_node(value_node):
  18. if value_node.tag == "string":
  19. return value_node.text
  20. if value_node.tag == "integer":
  21. return int(value_node.text)
  22. if value_node.tag == "date":
  23. return dateutil.parser.parse(value_node.text)
  24. if value_node.tag == "dict":
  25. return XmlDict(value_node)
  26. raise ValueError(value_node.tag)
  27. def __getitem__(self, key):
  28. value_node = self._get_value_node(key)
  29. return XmlDict._parse_value_node(value_node)
  30. def get(self, key, default):
  31. try:
  32. return self[key]
  33. except KeyError:
  34. return default
  35. def items(self):
  36. for key_node, value_node in zip(*[iter(self._node)] * 2):
  37. assert key_node.tag == "key"
  38. key = key_node.text
  39. value = XmlDict._parse_value_node(value_node)
  40. yield (key, value)
  41. def values(self):
  42. for _, value in self.items():
  43. yield value
  44. class Track:
  45. LOCAL_LOCATION_URL_PREFIX = "file://localhost/"
  46. def __init__(self, track_dict):
  47. assert isinstance(track_dict, XmlDict)
  48. self._dict = track_dict
  49. self._id = self._dict["Track ID"]
  50. assert isinstance(self._id, int)
  51. self._location_url = self._dict.get("Location", None)
  52. self._play_count = self._dict.get("Play Count", 0)
  53. assert isinstance(self._play_count, int)
  54. self._last_play_dt = self._dict.get("Play Date UTC", None)
  55. @property
  56. def track_id(self):
  57. return self._id
  58. @property
  59. def location_url(self):
  60. return self._location_url
  61. @property
  62. def local(self):
  63. return self.location_url is not None and self.location_url.startswith(
  64. self.LOCAL_LOCATION_URL_PREFIX
  65. )
  66. @property
  67. def local_path(self):
  68. if self.local:
  69. return os.path.sep + urllib.parse.unquote(
  70. self.location_url[len(self.LOCAL_LOCATION_URL_PREFIX) :],
  71. )
  72. raise ValueError(self.location_url)
  73. @property
  74. def play_count(self):
  75. return self._play_count
  76. @property
  77. def last_play_dt(self):
  78. return self._last_play_dt
  79. class XmlLibrary:
  80. def __init__(self, path):
  81. self._tree = xml.etree.ElementTree.parse(path)
  82. self._root_dict = XmlDict(self._tree.find("./dict"))
  83. self._id = self._root_dict["Library Persistent ID"]
  84. assert isinstance(self._id, str), self._id
  85. assert len(self._id) > 4
  86. @property
  87. def library_id(self):
  88. return self._id
  89. @property
  90. def tracks(self):
  91. for track_dict in self._root_dict["Tracks"].values():
  92. yield Track(track_dict)