Python Library Media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

metadata.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import media.CDDB
  2. import time
  3. import subprocess
  4. from media import common
  5. import math
  6. from PIL import Image
  7. import os
  8. import logging
  9. import sys
  10. try:
  11. from config import APP_NAME as ROOT_LOGGER_NAME
  12. except ImportError:
  13. ROOT_LOGGER_NAME = 'root'
  14. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  15. # make module usable without discid dependency
  16. try:
  17. import discid
  18. except ModuleNotFoundError:
  19. logger.warning("Python module discid not installed")
  20. except OSError:
  21. logger.exception("You might install python3-libdiscid")
  22. __KEY_CAMERA_VENDOR__ = 'camera_vendor'
  23. __KEY_CAMERA_MODEL__ = 'camera_model'
  24. def get_media_data(full_path, user_callback=None):
  25. #
  26. ft = common.get_filetype(full_path)
  27. #
  28. if ft == common.FILETYPE_AUDIO:
  29. return get_audio_data(full_path)
  30. elif ft == common.FILETYPE_IMAGE:
  31. return get_image_data(full_path)
  32. elif ft == common.FILETYPE_VIDEO:
  33. return get_video_data(full_path)
  34. elif ft == common.FILETYPE_DISC:
  35. return get_disc_data(full_path, user_callback)
  36. else:
  37. logger.warning('Filetype not known: %s', full_path)
  38. def get_audio_data(full_path):
  39. conv_key_dict = {}
  40. conv_key_dict['album'] = (str, common.KEY_ALBUM)
  41. conv_key_dict['TAG:album'] = (str, common.KEY_ALBUM)
  42. conv_key_dict['TAG:artist'] = (str, common.KEY_ARTIST)
  43. conv_key_dict['artist'] = (str, common.KEY_ARTIST)
  44. conv_key_dict['bit_rate'] = (__int_conv__, common.KEY_BITRATE)
  45. conv_key_dict['duration'] = (float, common.KEY_DURATION)
  46. conv_key_dict['TAG:genre'] = (str, common.KEY_GENRE)
  47. conv_key_dict['genre'] = (str, common.KEY_GENRE)
  48. conv_key_dict['TAG:title'] = (str, common.KEY_TITLE)
  49. conv_key_dict['title'] = (str, common.KEY_TITLE)
  50. conv_key_dict['TAG:track'] = (__int_conv__, common.KEY_TRACK)
  51. conv_key_dict['track'] = (__int_conv__, common.KEY_TRACK)
  52. conv_key_dict['TAG:date'] = (__int_conv__, common.KEY_YEAR)
  53. conv_key_dict['date'] = (__int_conv__, common.KEY_YEAR)
  54. return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
  55. def get_disc_data(full_path, user_callback):
  56. #
  57. # Read Information from CDDB database
  58. #
  59. did = media.CDDB.discid()
  60. if did is None:
  61. logger.error("Could not determine disc id...")
  62. sys.exit(1)
  63. q = media.CDDB.query(did)
  64. if q is None:
  65. data = {
  66. common.KEY_ARTIST: None,
  67. common.KEY_ALBUM: None,
  68. common.KEY_YEAR: None,
  69. common.KEY_GENRE: None
  70. }
  71. for i in range(0, int(did.split('+')[1])):
  72. data["track_%02d" % (i + 1)] = None
  73. data = user_callback(common.CALLBACK_MAN_INPUT, data)
  74. return media.CDDB.my_disc_metadata(**data)
  75. if len(q) == 1:
  76. # Only one entry
  77. did = tuple(q.keys())[0]
  78. else:
  79. # multiple entries (choose)
  80. if user_callback is None:
  81. logger.warning("No usercallback to handle multiple cddb choices...")
  82. sys.exit(1)
  83. did = user_callback(common.CALLBACK_CDDB_CHOICE, q)
  84. return media.CDDB.cddb(did)
  85. """
  86. musicbrainzngs.set_useragent("pyrip", "0.1", "your@mail")
  87. disc = discid.read()
  88. disc_id = disc.id
  89. disc_data = {}
  90. try:
  91. result = musicbrainzngs.get_releases_by_discid(disc_id, includes=["artists", "recordings"])
  92. except musicbrainzngs.ResponseError:
  93. logger.exception("disc not found or bad response")
  94. sys.exit(1)
  95. else:
  96. disc_data[common.KEY_ARTIST] = result["disc"]["release-list"][0]["artist-credit-phrase"]
  97. disc_data[common.KEY_ALBUM] = result["disc"]["release-list"][0]["title"]
  98. disc_data[common.KEY_YEAR] = int(result["disc"]["release-list"][0]["date"][:4])
  99. data_copy = dict(disc_data)
  100. disc_data["id"] = result["disc"]["release-list"][0]["id"]
  101. disc_data["tracks"] = []
  102. # get tracklist
  103. for entry in result["disc"]["release-list"][0]["medium-list"][0]["track-list"]:
  104. track = dict(data_copy)
  105. track[common.KEY_TITLE] = entry["recording"]["title"]
  106. track[common.KEY_TRACK] = int(entry['number'])
  107. disc_data["tracks"].append(track)
  108. return disc_data
  109. """
  110. def get_video_data(full_path):
  111. conv_key_dict = {}
  112. conv_key_dict['creation_time'] = (__vid_datetime_conv__, common.KEY_TIME)
  113. conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, common.KEY_TIME)
  114. conv_key_dict['bit_rate'] = (__int_conv__, common.KEY_BITRATE)
  115. conv_key_dict['duration'] = (float, common.KEY_DURATION)
  116. conv_key_dict['height'] = (__int_conv__, common.KEY_HEIGHT)
  117. conv_key_dict['width'] = (__int_conv__, common.KEY_WIDTH)
  118. conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, common.KEY_RATIO)
  119. return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
  120. def get_image_data(full_path):
  121. return __adapt__data__(__get_exif_data__(full_path), full_path)
  122. def __adapt__data__(data, full_path):
  123. data[common.KEY_SIZE] = os.path.getsize(full_path)
  124. # Join Camera Vendor and Camera Model
  125. if __KEY_CAMERA_MODEL__ in data and __KEY_CAMERA_VENDOR__ in data:
  126. model = data.pop(__KEY_CAMERA_MODEL__)
  127. vendor = data.pop(__KEY_CAMERA_VENDOR__)
  128. data[common.KEY_CAMERA] = '%s: %s' % (vendor, model)
  129. # Add time if not exists
  130. if common.KEY_TIME not in data:
  131. if common.KEY_YEAR in data and common.KEY_TRACK in data:
  132. if data[common.KEY_YEAR] != 0: # ignore year 0 - must be wrong
  133. # Use a date where track 1 is the newest in the given year
  134. minute = int(data[common.KEY_TRACK] / 60)
  135. second = (data[common.KEY_TRACK] - 60 * minute) % 60
  136. #
  137. data[common.KEY_TIME] = int(time.mktime((data[common.KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0)))
  138. data[common.KEY_TIME_IS_SUBSTITUTION] = True
  139. else:
  140. data[common.KEY_TIME] = int(os.path.getmtime(full_path))
  141. data[common.KEY_TIME_IS_SUBSTITUTION] = True
  142. return data
  143. def __get_xxprobe_data__(full_path, conv_key_dict):
  144. def _ffprobe_command(full_path):
  145. return ['ffprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
  146. def _avprobe_command(full_path):
  147. return ['avprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
  148. try:
  149. xxprobe_text = subprocess.check_output(_avprobe_command(full_path))
  150. except FileNotFoundError:
  151. try:
  152. xxprobe_text = subprocess.check_output(_ffprobe_command(full_path))
  153. except FileNotFoundError:
  154. logger.warning('ffprobe and avprobe seem to be not installed')
  155. return {}
  156. #
  157. rv = {}
  158. for line in xxprobe_text.decode('utf-8').splitlines():
  159. try:
  160. key, val = [snippet.strip() for snippet in line.split('=')]
  161. except ValueError:
  162. continue
  163. else:
  164. if key in conv_key_dict:
  165. tp, name = conv_key_dict[key]
  166. try:
  167. rv[name] = tp(val)
  168. except ValueError:
  169. logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, name)
  170. return rv
  171. def __get_exif_data__(full_path):
  172. rv = {}
  173. im = Image.open(full_path)
  174. try:
  175. exif = dict(im._getexif().items())
  176. except AttributeError:
  177. logger.debug('%s does not have any exif information', full_path)
  178. else:
  179. conv_key_dict = {}
  180. # IMAGE
  181. conv_key_dict[0x9003] = (__datetime_conv__, common.KEY_TIME)
  182. conv_key_dict[0x8822] = (__exposure_program_conv__, common.KEY_EXPOSURE_PROGRAM)
  183. conv_key_dict[0x829A] = (__num_denum_conv__, common.KEY_EXPOSURE_TIME)
  184. conv_key_dict[0x9209] = (__flash_conv__, common.KEY_FLASH)
  185. conv_key_dict[0x829D] = (__num_denum_conv__, common.KEY_APERTURE)
  186. conv_key_dict[0x920A] = (__num_denum_conv__, common.KEY_FOCAL_LENGTH)
  187. conv_key_dict[0x8825] = (__gps_conv__, common.KEY_GPS)
  188. conv_key_dict[0xA003] = (__int_conv__, common.KEY_HEIGHT)
  189. conv_key_dict[0x8827] = (__int_conv__, common.KEY_ISO)
  190. conv_key_dict[0x010F] = (str, __KEY_CAMERA_VENDOR__)
  191. conv_key_dict[0x0110] = (str, __KEY_CAMERA_MODEL__)
  192. conv_key_dict[0x0112] = (__int_conv__, common.KEY_ORIENTATION)
  193. conv_key_dict[0xA002] = (__int_conv__, common.KEY_WIDTH)
  194. for key in conv_key_dict:
  195. if key in exif:
  196. tp, name = conv_key_dict[key]
  197. raw_value = exif[key]
  198. logger.debug("Converting %s out of %s", name, repr(raw_value))
  199. value = tp(raw_value)
  200. if value is not None:
  201. rv[name] = value
  202. return rv
  203. # TODO: Join datetime converter __datetime_conv__ and __vid_datetime_conv_
  204. def __datetime_conv__(dt):
  205. format_string = "%Y:%m:%d %H:%M:%S"
  206. return int(time.mktime(time.strptime(dt, format_string)))
  207. def __vid_datetime_conv__(dt):
  208. try:
  209. dt = dt[:dt.index('.')]
  210. except ValueError:
  211. pass # time string seems to have no '.'
  212. dt = dt.replace('T', ' ').replace('/', '').replace('\\', '')
  213. if len(dt) == 16:
  214. dt += ':00'
  215. format_string = '%Y-%m-%d %H:%M:%S'
  216. return int(time.mktime(time.strptime(dt, format_string)))
  217. def __exposure_program_conv__(n):
  218. return {
  219. 0: 'Unidentified',
  220. 1: 'Manual',
  221. 2: 'Program Normal',
  222. 3: 'Aperture Priority',
  223. 4: 'Shutter Priority',
  224. 5: 'Program Creative',
  225. 6: 'Program Action',
  226. 7: 'Portrait Mode',
  227. 8: 'Landscape Mode'
  228. }.get(n, None)
  229. def __flash_conv__(n):
  230. return {
  231. 0: 'No',
  232. 1: 'Fired',
  233. 5: 'Fired (?)', # no return sensed
  234. 7: 'Fired (!)', # return sensed
  235. 9: 'Fill Fired',
  236. 13: 'Fill Fired (?)',
  237. 15: 'Fill Fired (!)',
  238. 16: 'Off',
  239. 24: 'Auto Off',
  240. 25: 'Auto Fired',
  241. 29: 'Auto Fired (?)',
  242. 31: 'Auto Fired (!)',
  243. 32: 'Not Available'
  244. }.get(n, None)
  245. def __int_conv__(value):
  246. try:
  247. return int(value)
  248. except ValueError:
  249. for c in ['.', '/', '-']:
  250. p = value.find(c)
  251. if p >= 0:
  252. value = value[:p]
  253. try:
  254. return int(value)
  255. except ValueError:
  256. return None
  257. def __num_denum_conv__(data):
  258. try:
  259. return float(data)
  260. except TypeError:
  261. num, denum = data
  262. return num / denum
  263. def __gps_conv__(data):
  264. def lat_lon_cal(lon_or_lat):
  265. lon_lat = 0.
  266. fac = 1.
  267. for data in lon_or_lat:
  268. try:
  269. lon_lat += float(data[0]) / float(data[1]) * fac
  270. except TypeError:
  271. lon_lat += data * fac
  272. except ZeroDivisionError:
  273. return 0.
  274. fac *= 1. / 60.
  275. if math.isnan(lon_lat):
  276. return 0.
  277. return lon_lat
  278. try:
  279. lon = lat_lon_cal(data[0x0004])
  280. lat = lat_lon_cal(data[0x0002])
  281. if lon != 0 or lat != 0: # do not use lon and lat equal 0, caused by motorola gps weakness
  282. return {'lon': lon, 'lat': lat}
  283. except KeyError:
  284. logger.warning('GPS data extraction failed for %s', repr(data))
  285. def __ratio_conv__(ratio):
  286. ratio = ratio.replace('\\', '')
  287. num, denum = ratio.split(':')
  288. return float(num) / float(denum)