__init__.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. # -*- coding: utf-8 -*-
  2. import datetime as dt
  3. import dateutil.tz
  4. import mutagen
  5. import os
  6. import re
  7. import symuid.tag_interface
  8. def _timestamp_to_utc_dt(ts):
  9. return dt.datetime.utcfromtimestamp(ts) \
  10. .replace(tzinfo=dateutil.tz.tzutc())
  11. assert _timestamp_to_utc_dt(1528795204) \
  12. == dt.datetime(2018, 6, 12, 9, 20, 4, tzinfo=dateutil.tz.tzutc())
  13. class PlayCount:
  14. def __init__(self, player, library_id, register_dt, count):
  15. self.player = player
  16. self.library_id = library_id
  17. assert isinstance(register_dt, dt.datetime), register_dt
  18. assert register_dt.tzinfo is not None, register_dt
  19. self.register_dt = register_dt
  20. assert isinstance(count, int) and count >= 0, count
  21. self.count = count
  22. class Track:
  23. PATH_DEFAULT_IGNORE_REGEX = r'\.(itdb|itc2|itl|jpg|midi?|plist|xml|zip)$'
  24. def __init__(self, path):
  25. mutagen_file = mutagen.File(filename=path)
  26. if mutagen_file is None:
  27. raise NotImplementedError(path)
  28. elif isinstance(mutagen_file.tags, mutagen.id3.ID3):
  29. self._iface = symuid.tag_interface.ID3(mutagen_file)
  30. elif isinstance(mutagen_file.tags, mutagen.mp4.MP4Tags):
  31. self._iface = symuid.tag_interface.MP4(mutagen_file)
  32. else:
  33. raise NotImplementedError((path, type(mutagen_file)))
  34. @property
  35. def path(self):
  36. return self._iface.track_path
  37. def get_uuid(self):
  38. return self._iface.get_track_uuid()
  39. def assign_uuid(self, uuid):
  40. if self.get_uuid():
  41. raise Exception("{!r} already has an uuid".format(self.path))
  42. self._iface.set_track_uuid(uuid)
  43. self._iface.save()
  44. def _get_play_counts(self, player=None, library_id=None):
  45. label = 'symuid:pcnt'
  46. assert library_id is None or player is not None
  47. if player:
  48. label += ':' + player
  49. if library_id:
  50. label += ':' + library_id
  51. elif library_id:
  52. raise Exception((player, library_id))
  53. for k, c in self._iface.get_free_ints(label):
  54. player, library_id, register_ts_dec = k.split(':')[2:]
  55. yield PlayCount(
  56. player=player,
  57. library_id=library_id,
  58. register_dt=_timestamp_to_utc_dt(int(register_ts_dec)),
  59. count=c,
  60. )
  61. def _get_latest_play_counts(self, player=None, library_id=None):
  62. latest = {}
  63. for count in self._get_play_counts(player, library_id):
  64. if not count.player in latest:
  65. latest[count.player] = {}
  66. if not count.library_id in latest[count.player] \
  67. or latest[count.player][count.library_id].register_dt < count.register_dt:
  68. latest[count.player][count.library_id] = count
  69. return [c for p in latest.values() for c in p.values()]
  70. def get_latest_play_count(self, player, library_id):
  71. assert player is not None, player
  72. assert library is not None, library_id
  73. counts = self._get_latest_play_counts(player, library_id)
  74. if len(counts) == 0:
  75. return None
  76. else:
  77. assert len(counts) == 1, counts
  78. return counts[0]
  79. def get_play_count_sum(self, player=None, library_id=None):
  80. return sum(c.count for c in self._get_latest_play_counts(player, library_id))
  81. def register_play_count(self, player, library_id, register_dt, play_count, tag_set_cb=None):
  82. # TODO accept PlayCount as param
  83. assert isinstance(register_dt, dt.datetime), register_dt
  84. assert isinstance(play_count, int), play_count
  85. tag_label = 'symuid:pcnt:{}:{}:{:.0f}'.format(
  86. player, library_id, register_dt.timestamp(),
  87. )
  88. current_count = self._iface.get_free_int(tag_label)
  89. if current_count is None:
  90. new_tag = self._iface.set_free_int(tag_label, play_count)
  91. self._iface.save()
  92. if tag_set_cb:
  93. tag_set_cb(self, new_tag)
  94. elif current_count != play_count:
  95. raise Exception((current_count, play_count))
  96. @classmethod
  97. def walk(cls, root_path, path_ignore_regex, ignored_cb=None, unsupported_cb=None):
  98. for dirpath, dirnames, filenames in os.walk(root_path):
  99. for filename in filenames:
  100. track_path = os.path.join(dirpath, filename)
  101. if path_ignore_regex.search(track_path):
  102. if ignored_cb is not None:
  103. ignored_cb(track_path)
  104. else:
  105. try:
  106. yield cls(track_path)
  107. except NotImplementedError as e:
  108. if unsupported_cb is not None:
  109. unsupported_cb(track_path, e)