Browse Source

symuid-import-cmus: register play count if play count > 0 and different from latest

Fabian Peter Hammerle 6 years ago
parent
commit
d2448ec506
3 changed files with 102 additions and 12 deletions
  1. 14 6
      symuid-import-cmus
  2. 58 0
      symuid/__init__.py
  3. 30 6
      symuid/tag_interface.py

+ 14 - 6
symuid-import-cmus

@@ -6,6 +6,7 @@ import os
 import symuid
 import symuid.library.cmus
 
+PLAYER_NAME = 'cmus'
 LIBRARY_ID_LENGTH_MIN = 8
 
 
@@ -19,14 +20,21 @@ def symuid_import_cmus(library_id, cache_path):
             sys.stderr.write('{!r}: not found\n'.format(cmus_track.path))
         elif cmus_track.play_count > 0:  # TODO play_count = 0
             symuid_track = symuid.Track(path=cmus_track.path.decode())
-            symuid_track.register_play_count(
-                player='cmus',
+            last_count = symuid_track.get_latest_play_count(
+                player=PLAYER_NAME,
                 library_id=library_id,
-                register_dt=dt.datetime.now(),
-                play_count=cmus_track.play_count,
-                tag_set_cb=lambda tr, tag:
-                    print('{!r}: set tag {!r}'.format(tr.path, tag)),
             )
+            assert last_count is None or last_count.count <= cmus_track.play_count, \
+                (symuid_track.path, last_count, cmus_track.play_count)
+            if last_count is None or last_count.count != cmus_track.play_count:
+                symuid_track.register_play_count(
+                    player=PLAYER_NAME,
+                    library_id=library_id,
+                    register_dt=dt.datetime.now(),
+                    play_count=cmus_track.play_count,
+                    tag_set_cb=lambda tr, tag:
+                        print('{!r}: set tag {!r}'.format(tr.path, tag)),
+                )
 
 
 def _init_argparser():

+ 58 - 0
symuid/__init__.py

@@ -1,11 +1,32 @@
 # -*- coding: utf-8 -*-
 
 import datetime as dt
+import dateutil.tz
 import mutagen
 
 import symuid.tag_interface
 
 
+def _timestamp_to_utc_dt(ts):
+    return dt.datetime.utcfromtimestamp(ts) \
+        .replace(tzinfo=dateutil.tz.tzutc())
+
+assert _timestamp_to_utc_dt(1528795204) \
+    == dt.datetime(2018, 6, 12, 9, 20, 4, tzinfo=dateutil.tz.tzutc())
+
+
+class PlayCount:
+
+    def __init__(self, player, library_id, register_dt, count):
+        self.player = player
+        self.library_id = library_id
+        assert isinstance(register_dt, dt.datetime), register_dt
+        assert register_dt.tzinfo is not None, register_dt
+        self.register_dt = register_dt
+        assert isinstance(count, int) and count >= 0, count
+        self.count = count
+
+
 class Track:
 
     def __init__(self, path):
@@ -21,7 +42,44 @@ class Track:
     def path(self):
         return self._iface.track_path
 
+    def _get_play_counts(self, player=None, library_id=None):
+        label = 'symuid:pcnt'
+        assert library_id is None or player is not None
+        if player:
+            label += ':' + player
+            if library_id:
+                label += ':' + library_id
+        for k, c in self._iface.get_free_ints(label):
+            player, library_id, register_ts_dec = k.split(':')[2:]
+            yield PlayCount(
+                player=player,
+                library_id=library_id,
+                register_dt=_timestamp_to_utc_dt(int(register_ts_dec)),
+                count=c,
+            )
+
+    def _get_latest_play_counts(self, player=None, library_id=None):
+        latest = {}
+        for count in self._get_play_counts(player, library_id):
+            if not count.player in latest:
+                latest[count.player] = {}
+            if not count.library_id in latest[count.player] \
+                    or latest[count.player][count.library_id].register_dt < count.register_dt:
+                latest[count.player][count.library_id] = count
+        return [c for p in latest.values() for c in p.values()]
+
+    def get_latest_play_count(self, player, library_id):
+        assert player is not None, player
+        assert library is not None, library_id
+        counts = self._get_latest_play_counts(player, library_id)
+        if len(counts) == 0:
+            return None
+        else:
+            assert len(counts) == 1, counts
+            return counts[0]
+
     def register_play_count(self, player, library_id, register_dt, play_count, tag_set_cb=None):
+        # TODO accept PlayCount as param
         assert isinstance(register_dt, dt.datetime), register_dt
         assert isinstance(play_count, int), play_count
         tag_label = 'symuid:pcnt:{}:{}:{:.0f}'.format(

+ 30 - 6
symuid/tag_interface.py

@@ -2,6 +2,7 @@
 
 import mutagen.id3
 import mutagen.mp4
+import re
 
 
 class _mutagen:
@@ -22,10 +23,19 @@ class ID3(_mutagen):
             mutagen_file.tags
         self._mutagen_file = mutagen_file
 
+    def get_free_ints(self, tag_label_prefix):
+        for t in self._mutagen_file.tags.getall('TXXX:' + tag_label_prefix):
+            assert len(t.text) == 1, t
+            yield (t.desc, int(t.text[0]))
+
     def get_free_int(self, tag_label):
-        values = self._mutagen_file.tags['TXXX:' + tag_label].text
-        assert len(values) == 1, values
-        return int(values[0])
+        tags = [t for t in self.get_free_ints(tag_label)]
+        if len(tags) == 0:
+            raise KeyError(tag_label)
+        else:
+            assert len(tags) == 1, tags
+            assert tags[0][0] == tag_label, tag
+            return tags[0][1]
 
     def set_free_int(self, tag_label, data):
         # mutagen.id3._specs.EncodedTextSpec.write encodes 'desc' and 'text'
@@ -46,11 +56,25 @@ class MP4(_mutagen):
             mutagen_file.tags
         self._mutagen_file = mutagen_file
 
+    @staticmethod
+    def _freeform_to_int(freeform):
+        # "a signed big-endian integer with length one of { 1,2,3,4,8 } bytes"
+        assert freeform.dataformat == mutagen.mp4.AtomDataType.INTEGER, freeform
+        return int.from_bytes(freeform, byteorder='big', signed=True)
+
+    def get_free_ints(self, tag_label_prefix):
+        label_pattern = re.compile(r'^----:{}(:|$)'.format(
+            re.escape(tag_label_prefix),
+        ))
+        for label, values in self._mutagen_file.tags.items():
+            if label_pattern.match(label):
+                assert len(values) == 1, (label, values)
+                value = MP4._freeform_to_int(values[0])
+                yield (re.sub(r'^----:', '', label), value)
+
     def get_free_int(self, tag_label):
         tag, = self._mutagen_file.tags['----:' + tag_label]
-        # "a signed big-endian integer with length one of { 1,2,3,4,8 } bytes"
-        assert tag.dataformat == mutagen.mp4.AtomDataType.INTEGER, tag.dataformat
-        return int.from_bytes(tag, byteorder='big', signed=True)
+        return MP4._freeform_to_int(tag)
 
     def set_free_int(self, tag_label, data):
         assert isinstance(data, int)