cmus.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import re
  2. import sys
  3. def _int_from_bytes_sys(data_bytes):
  4. return int.from_bytes(data_bytes, byteorder=sys.byteorder)
  5. class Track:
  6. RESERVED_PAD_REGEX = rb'\xff{16,}'
  7. STRING_TERMINATOR = b'\x00'
  8. def __init__(self, cache_size, cache_bytes):
  9. """
  10. struct cache_entry {
  11. // size of this struct including size itself
  12. uint32_t size;
  13. int32_t play_count;
  14. int64_t mtime;
  15. int32_t duration;
  16. int32_t bitrate;
  17. int32_t bpm; // CACHE_VERSION 0x0d (commit 976c10d0e42c9ecd7389b28dd7c5b560a1702821)
  18. uint8_t _reserved[CACHE_ENTRY_RESERVED_SIZE];
  19. // filename, codec, codec_profile and N * (key, val)
  20. char strings[];
  21. };
  22. """
  23. assert len(cache_bytes) + 4 == cache_size
  24. self._play_count = _int_from_bytes_sys(cache_bytes[0:4])
  25. # self._mtime = _int_from_bytes_sys(cache_bytes[4:12])
  26. # self._duration_seconds = _int_from_bytes_sys(cache_bytes[12:16])
  27. # self._bitrate = _int_from_bytes_sys(cache_bytes[16:20])
  28. # self._bpm = _int_from_bytes_sys(cache_bytes[20:24])
  29. strings = re.split(self.RESERVED_PAD_REGEX, cache_bytes)[1] \
  30. .split(self.STRING_TERMINATOR)
  31. self._path = strings[0]
  32. @property
  33. def path(self):
  34. return self._path
  35. @property
  36. def play_count(self):
  37. return self._play_count
  38. class Cache:
  39. # pylint: disable=too-few-public-methods
  40. FILE_PREFIX = b'CTC'
  41. VERSION_LENGTH = 1
  42. SUPPORTED_VERSIONS = [b'\x0c', b'\x0d']
  43. # always big endian, see cache_init()
  44. FLAGS_BYTEORDER = 'big'
  45. FLAGS_LENGTH = 4
  46. FLAG_64_BIT = 0x01
  47. HEADER_LENGTH = len(FILE_PREFIX) + VERSION_LENGTH + FLAGS_LENGTH
  48. def __init__(self, path):
  49. self._path = path
  50. with open(self._path, 'rb') as stream:
  51. # see cache.c cache_init()
  52. assert stream.read(len(self.FILE_PREFIX)) == self.FILE_PREFIX
  53. cache_version = stream.read(self.VERSION_LENGTH)
  54. assert cache_version in self.SUPPORTED_VERSIONS, cache_version
  55. flags = int.from_bytes(
  56. stream.read(self.FLAGS_LENGTH),
  57. byteorder=self.FLAGS_BYTEORDER, # persistent
  58. )
  59. assert flags & self.FLAG_64_BIT
  60. # support no other flags
  61. assert flags & ~self.FLAG_64_BIT == 0, flags
  62. def get_tracks(self):
  63. with open(self._path, 'rb') as stream:
  64. stream.seek(self.HEADER_LENGTH)
  65. # size includes size itself
  66. while True:
  67. size = _int_from_bytes_sys(stream.read(4))
  68. if size == 0:
  69. break
  70. yield Track(size, stream.read(size - 4))
  71. # see cache.c write_ti ALIGN
  72. stream.read((-size) % 8)