itunes.py 3.2 KB

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