symuid-import-itunes 4.3 KB

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