process.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. # -*- coding: utf-8 -*-
  2. from contextlib import contextmanager
  3. from ..monitor.pure import truncate_to_value, truncate_to_length, toposort
  4. from os.path import normpath
  5. from ..monitor.exceptions import BadSongFile
  6. from ..monitor.log import Loggable
  7. from ..monitor import pure as mmp
  8. from collections import namedtuple
  9. import mutagen
  10. import subprocess
  11. import json
  12. import logging
  13. class FakeMutagen(dict):
  14. """
  15. Need this fake mutagen object so that airtime_special functions
  16. return a proper default value instead of throwing an exceptions for
  17. files that mutagen doesn't recognize
  18. """
  19. FakeInfo = namedtuple('FakeInfo','length bitrate')
  20. def __init__(self,path):
  21. self.path = path
  22. self.mime = ['audio/wav']
  23. self.info = FakeMutagen.FakeInfo(0.0, '')
  24. dict.__init__(self)
  25. def set_length(self,l):
  26. old_bitrate = self.info.bitrate
  27. self.info = FakeMutagen.FakeInfo(l, old_bitrate)
  28. class MetadataAbsent(Exception):
  29. def __init__(self, name): self.name = name
  30. def __str__(self): return "Could not obtain element '%s'" % self.name
  31. class MetadataElement(Loggable):
  32. def __init__(self,name):
  33. self.name = name
  34. # "Sane" defaults
  35. self.__deps = set()
  36. self.__normalizer = lambda x: x
  37. self.__optional = True
  38. self.__default = None
  39. self.__is_normalized = lambda _ : True
  40. self.__max_length = -1
  41. self.__max_value = -1
  42. self.__translator = None
  43. def max_length(self,l):
  44. self.__max_length = l
  45. def max_value(self,v):
  46. self.__max_value = v
  47. def optional(self, setting):
  48. self.__optional = setting
  49. def is_optional(self):
  50. return self.__optional
  51. def depends(self, *deps):
  52. self.__deps = set(deps)
  53. def dependencies(self):
  54. return self.__deps
  55. def translate(self, f):
  56. self.__translator = f
  57. def is_normalized(self, f):
  58. self.__is_normalized = f
  59. def normalize(self, f):
  60. self.__normalizer = f
  61. def default(self,v):
  62. self.__default = v
  63. def get_default(self):
  64. if hasattr(self.__default, '__call__'): return self.__default()
  65. else: return self.__default
  66. def has_default(self):
  67. return self.__default is not None
  68. def path(self):
  69. return self.__path
  70. def __slice_deps(self, d):
  71. """
  72. returns a dictionary of all the key value pairs in d that are also
  73. present in self.__deps
  74. """
  75. return dict( (k,v) for k,v in d.iteritems() if k in self.__deps)
  76. def __str__(self):
  77. return "%s(%s)" % (self.name, ' '.join(list(self.__deps)))
  78. def read_value(self, path, original, running={}):
  79. # If value is present and normalized then we only check if it's
  80. # normalized or not. We normalize if it's not normalized already
  81. if self.name in original:
  82. v = original[self.name]
  83. if self.__is_normalized(v): return v
  84. else: return self.__normalizer(v)
  85. # We slice out only the dependencies that are required for the metadata
  86. # element.
  87. dep_slice_orig = self.__slice_deps(original)
  88. dep_slice_running = self.__slice_deps(running)
  89. # TODO : remove this later
  90. dep_slice_special = self.__slice_deps({'path' : path})
  91. # We combine all required dependencies into a single dictionary
  92. # that we will pass to the translator
  93. full_deps = dict( dep_slice_orig.items()
  94. + dep_slice_running.items()
  95. + dep_slice_special.items())
  96. # check if any dependencies are absent
  97. # note: there is no point checking the case that len(full_deps) >
  98. # len(self.__deps) because we make sure to "slice out" any supefluous
  99. # dependencies above.
  100. if len(full_deps) != len(self.dependencies()) or \
  101. len(self.dependencies()) == 0:
  102. # If we have a default value then use that. Otherwise throw an
  103. # exception
  104. if self.has_default(): return self.get_default()
  105. else: raise MetadataAbsent(self.name)
  106. # We have all dependencies. Now for actual for parsing
  107. def def_translate(dep):
  108. def wrap(k):
  109. e = [ x for x in dep ][0]
  110. return k[e]
  111. return wrap
  112. # Only case where we can select a default translator
  113. if self.__translator is None:
  114. self.translate(def_translate(self.dependencies()))
  115. if len(self.dependencies()) > 2: # dependencies include themselves
  116. self.logger.info("Ignoring some dependencies in translate %s"
  117. % self.name)
  118. self.logger.info(self.dependencies())
  119. r = self.__normalizer( self.__translator(full_deps) )
  120. if self.__max_length != -1:
  121. r = truncate_to_length(r, self.__max_length)
  122. if self.__max_value != -1:
  123. try: r = truncate_to_value(r, self.__max_value)
  124. except ValueError, e: r = ''
  125. return r
  126. def normalize_mutagen(path):
  127. """
  128. Consumes a path and reads the metadata using mutagen. normalizes some of
  129. the metadata that isn't read through the mutagen hash
  130. """
  131. if not mmp.file_playable(path): raise BadSongFile(path)
  132. try : m = mutagen.File(path, easy=True)
  133. except Exception : raise BadSongFile(path)
  134. if m is None: m = FakeMutagen(path)
  135. try:
  136. if mmp.extension(path) == 'wav':
  137. m.set_length(mmp.read_wave_duration(path))
  138. except Exception: raise BadSongFile(path)
  139. md = {}
  140. for k,v in m.iteritems():
  141. if type(v) is list:
  142. if len(v) > 0: md[k] = v[0]
  143. else: md[k] = v
  144. # populate special metadata values
  145. md['length'] = getattr(m.info, 'length', 0.0)
  146. md['bitrate'] = getattr(m.info, 'bitrate', u'')
  147. md['sample_rate'] = getattr(m.info, 'sample_rate', 0)
  148. md['mime'] = m.mime[0] if len(m.mime) > 0 else u''
  149. md['path'] = normpath(path)
  150. # silence detect(set default cue in and out)
  151. #try:
  152. #command = ['silan', '-b', '-f', 'JSON', md['path']]
  153. #proc = subprocess.Popen(command, stdout=subprocess.PIPE)
  154. #out = proc.communicate()[0].strip('\r\n')
  155. #info = json.loads(out)
  156. #md['cuein'] = info['sound'][0][0]
  157. #md['cueout'] = info['sound'][0][1]
  158. #except Exception:
  159. #self.logger.debug('silan is missing')
  160. if 'title' not in md: md['title'] = u''
  161. return md
  162. class OverwriteMetadataElement(Exception):
  163. def __init__(self, m): self.m = m
  164. def __str__(self): return "Trying to overwrite: %s" % self.m
  165. class MetadataReader(object):
  166. def __init__(self):
  167. self.clear()
  168. def register_metadata(self,m):
  169. if m in self.__mdata_name_map:
  170. raise OverwriteMetadataElement(m)
  171. self.__mdata_name_map[m.name] = m
  172. d = dict( (name,m.dependencies()) for name,m in
  173. self.__mdata_name_map.iteritems() )
  174. new_list = list( toposort(d) )
  175. self.__metadata = [ self.__mdata_name_map[name] for name in new_list
  176. if name in self.__mdata_name_map]
  177. def clear(self):
  178. self.__mdata_name_map = {}
  179. self.__metadata = []
  180. def read(self, path, muta_hash):
  181. normalized_metadata = {}
  182. for mdata in self.__metadata:
  183. try:
  184. normalized_metadata[mdata.name] = mdata.read_value(
  185. path, muta_hash, normalized_metadata)
  186. except MetadataAbsent:
  187. if not mdata.is_optional(): raise
  188. return normalized_metadata
  189. def read_mutagen(self, path):
  190. return self.read(path, normalize_mutagen(path))
  191. global_reader = MetadataReader()
  192. @contextmanager
  193. def metadata(name):
  194. t = MetadataElement(name)
  195. yield t
  196. global_reader.register_metadata(t)