symuid-import-itunes 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import datetime as dt
  4. import dateutil.parser
  5. import mutagen
  6. import mutagen.id3
  7. import mutagen.mp4
  8. import os
  9. import urllib.parse
  10. import xml.etree.ElementTree
  11. def generate_play_count_tag_label(player, library_id, reg_dt):
  12. return 'symuid:pcnt:{}:{}:{}'.format(player, library_id, int(reg_dt.timestamp()))
  13. def get_itunes_dict_value_node(dict_node, key):
  14. assert isinstance(dict_node, xml.etree.ElementTree.Element)
  15. assert isinstance(key, str)
  16. # WORKAROUND method getnext() is sadly not available
  17. for child_idx, child_node in enumerate(dict_node):
  18. if child_node.tag == 'key' and child_node.text == key:
  19. return dict_node[child_idx + 1]
  20. raise KeyError()
  21. def get_itunes_dict_value(dict_node, key):
  22. value_node = get_itunes_dict_value_node(dict_node, key)
  23. if value_node.tag == 'string':
  24. return value_node.text
  25. elif value_node.tag == 'integer':
  26. return int(value_node.text)
  27. elif value_node.tag == 'date':
  28. return dateutil.parser.parse(value_node.text)
  29. else:
  30. return value_node
  31. def set_play_count_tag(track_path, player, library_id, reg_dt, play_count):
  32. assert isinstance(reg_dt, dt.datetime), reg_dt
  33. tag_label = generate_play_count_tag_label(
  34. player=player,
  35. library_id=library_id,
  36. reg_dt=reg_dt,
  37. )
  38. track = mutagen.File(filename=track_path)
  39. if isinstance(track.tags, mutagen.id3.ID3):
  40. tag_label_id3 = 'TXXX:' + tag_label
  41. if not tag_label_id3 in track.tags:
  42. # mutagen.id3._specs.EncodedTextSpec.write encodes
  43. # 'desc' and 'text'
  44. tag = mutagen.id3.TXXX(
  45. encoding=mutagen.id3.Encoding.LATIN1,
  46. desc=tag_label,
  47. text=[str(play_count)],
  48. )
  49. track.tags.add(tag)
  50. track.save()
  51. print('{!r}: set ID3 tag {!r}'.format(track_path, tag))
  52. elif isinstance(track.tags, mutagen.mp4.MP4Tags):
  53. tag_label_mp4 = '----:' + tag_label
  54. if not tag_label_mp4 in track.tags:
  55. track.tags[tag_label_mp4] = tag = mutagen.mp4.MP4FreeForm(
  56. # "a signed big-endian integer with length one of { 1,2,3,4,8 } bytes"
  57. # TODO set byte length properly
  58. data=play_count.to_bytes(1, byteorder='big'),
  59. dataformat=mutagen.mp4.AtomDataType.INTEGER,
  60. )
  61. track.save()
  62. print('{!r}: set MP4 tag {!r}'.format(track_path, tag))
  63. else:
  64. raise Exception(track_path)
  65. def symuid_import_itunes(xml_library_path, root_url, root_path):
  66. root_path = os.path.expanduser(root_path)
  67. lib = xml.etree.ElementTree.parse(xml_library_path)
  68. # WORKAROUND find('.//key[.="Library Persistent ID"]')
  69. # -> SyntaxError: invalid predicate
  70. lib_root_dict = lib.find('./dict')
  71. lib_id = get_itunes_dict_value(lib_root_dict, 'Library Persistent ID')
  72. assert isinstance(lib_id, str) and len(lib_id) > 0, lib_id
  73. for track_node in get_itunes_dict_value(lib_root_dict, 'Tracks').iterfind('./dict'):
  74. try:
  75. track_url = get_itunes_dict_value(track_node, 'Location')
  76. except KeyError:
  77. track_url = None
  78. try:
  79. play_count = get_itunes_dict_value(track_node, 'Play Count')
  80. except KeyError:
  81. play_count = 0
  82. try:
  83. last_play_dt = get_itunes_dict_value(track_node, 'Play Date UTC')
  84. except KeyError:
  85. last_play_dt = None
  86. # TODO create tag if last_play_dt is None
  87. if last_play_dt and track_url and track_url.startswith(root_url):
  88. track_path = os.path.join(
  89. root_path,
  90. urllib.parse.unquote(track_url[len(root_url):]),
  91. )
  92. # TODO dt=dt.datetime.now()
  93. set_play_count_tag(
  94. track_path=track_path,
  95. player='itunes',
  96. library_id=lib_id,
  97. reg_dt=last_play_dt,
  98. play_count=play_count,
  99. )
  100. def _init_argparser():
  101. import argparse
  102. argparser = argparse.ArgumentParser(description=None)
  103. argparser.add_argument('xml_library_path')
  104. argparser.add_argument(
  105. '--root-url',
  106. default='file://localhost/',
  107. help='(default: %(default)r)',
  108. )
  109. argparser.add_argument(
  110. '--root-path',
  111. default='',
  112. help='(default: %(default)r)',
  113. )
  114. return argparser
  115. def main(argv):
  116. argparser = _init_argparser()
  117. args = argparser.parse_args(argv[1:])
  118. symuid_import_itunes(**vars(args))
  119. return 0
  120. if __name__ == "__main__":
  121. import sys
  122. sys.exit(main(sys.argv))