/sonata/scrobbler.py

https://github.com/andersbll/sonata
Python | 222 lines | 164 code | 26 blank | 32 comment | 58 complexity | f047c18fdaf7fa4192fe84a8434b012b MD5 | raw file
  1. """
  2. This module makes Sonata submit the songs played to a Last.fm account.
  3. Example usage:
  4. import scrobbler
  5. self.scrobbler = scrobbler.Scrobbler(self.config)
  6. self.scrobbler.import_module()
  7. self.scrobbler.init()
  8. ...
  9. self.scrobbler.handle_change_status(False, self.prevsonginfo)
  10. """
  11. import logging
  12. import os
  13. import sys
  14. import threading # init, np, post start threads init_thread, do_np, do_post
  15. import time
  16. audioscrobbler = None # imported when first needed
  17. from sonata import mpdhelper as mpdh
  18. class Scrobbler:
  19. def __init__(self, config):
  20. self.logger = logging.getLogger(__name__)
  21. self.config = config
  22. self.scrob = None
  23. self.scrob_post = None
  24. self.scrob_start_time = ""
  25. self.scrob_playing_duration = 0
  26. self.scrob_last_prepared = ""
  27. self.elapsed_now = None
  28. def import_module(self, _show_error=False):
  29. """Import the audioscrobbler module"""
  30. # We need to try to import audioscrobbler either when the app starts
  31. # (if as_enabled=True) or if the user enables it in prefs.
  32. global audioscrobbler
  33. if audioscrobbler is None:
  34. from sonata import audioscrobbler
  35. def imported(self):
  36. """Return True if the audioscrobbler module has been imported"""
  37. return audioscrobbler is not None
  38. def init(self):
  39. """Initialize the Audioscrobbler support if enabled and configured"""
  40. if audioscrobbler is not None and self.config.as_enabled and \
  41. len(self.config.as_username) > 0 and \
  42. len(self.config.as_password_md5) > 0:
  43. thread = threading.Thread(target=self.init_thread)
  44. thread.daemon = True
  45. thread.start()
  46. def init_thread(self):
  47. if self.scrob is None:
  48. self.scrob = audioscrobbler.AudioScrobbler()
  49. if self.scrob_post is None:
  50. self.scrob_post = self.scrob.post(self.config.as_username,
  51. self.config.as_password_md5,
  52. verbose=True)
  53. else:
  54. if self.scrob_post.authenticated:
  55. return # We are authenticated
  56. else:
  57. self.scrob_post = self.scrob.post(self.config.as_username,
  58. self.config.as_password_md5,
  59. verbose=True)
  60. try:
  61. self.scrob_post.auth()
  62. except Exception as e:
  63. self.logger.error("Error authenticating audioscrobbler: %r", e)
  64. self.scrob_post = None
  65. if self.scrob_post:
  66. self.retrieve_cache()
  67. def handle_change_status(self, state, prevstate, prevsonginfo,
  68. songinfo=None, mpd_time_now=None):
  69. """Handle changes to play status, submitting info as appropriate"""
  70. if prevsonginfo and 'time' in prevsonginfo:
  71. prevsong_time = prevsonginfo.time
  72. else:
  73. prevsong_time = None
  74. if state in ('play', 'pause'):
  75. elapsed_prev = self.elapsed_now
  76. self.elapsed_now, length = [float(c) for c in
  77. mpd_time_now.split(':')]
  78. current_file = songinfo.file
  79. if prevstate == 'stop':
  80. # Switched from stop to play, prepare current track:
  81. self.prepare(songinfo)
  82. elif (prevsong_time and
  83. (self.scrob_last_prepared != current_file or
  84. (self.scrob_last_prepared == current_file and
  85. elapsed_prev and self.elapsed_now <= 1 and
  86. self.elapsed_now < elapsed_prev and length > 0))):
  87. # New song is playing, post previous track if time criteria is
  88. # met. In order to account for the situation where the same
  89. # song is played twice in a row, we will check if previous
  90. # elapsed time was larger than current and we're at the
  91. # beginning of the same song now
  92. if self.scrob_playing_duration > 4 * 60 or \
  93. self.scrob_playing_duration > int(prevsong_time) / 2:
  94. if self.scrob_start_time != "":
  95. self.post(prevsonginfo)
  96. # Prepare current track:
  97. self.prepare(songinfo)
  98. # Keep track of the total amount of time that the current song
  99. # has been playing:
  100. now = time.time()
  101. if prevstate != 'pause':
  102. self.scrob_playing_duration += now - self.scrob_prev_time
  103. self.scrob_prev_time = now
  104. else: # stopped:
  105. self.elapsed_now = 0
  106. if prevsong_time:
  107. if self.scrob_playing_duration > 4 * 60 or \
  108. self.scrob_playing_duration > int(prevsong_time) / 2:
  109. # User stopped the client, post previous track if time
  110. # criteria is met:
  111. if self.scrob_start_time != "":
  112. self.post(prevsonginfo)
  113. def auth_changed(self):
  114. """Try to re-authenticate"""
  115. if self.scrob_post:
  116. if self.scrob_post.authenticated:
  117. self.scrob_post = None
  118. def prepare(self, songinfo):
  119. if audioscrobbler is not None:
  120. self.scrob_start_time = ""
  121. self.scrob_last_prepared = ""
  122. self.scrob_playing_duration = 0
  123. self.scrob_prev_time = time.time()
  124. if self.config.as_enabled and songinfo:
  125. # No need to check if the song is 30 seconds or longer,
  126. # audioscrobbler.py takes care of that.
  127. if 'time' in songinfo:
  128. self.np(songinfo)
  129. self.scrob_start_time = str(int(time.time()))
  130. self.scrob_last_prepared = songinfo.file
  131. def np(self, songinfo):
  132. thread = threading.Thread(target=self.do_np, args=(songinfo,))
  133. thread.daemon = True
  134. thread.start()
  135. def do_np(self, songinfo):
  136. self.init()
  137. if self.config.as_enabled and self.scrob_post and songinfo:
  138. if 'artist' in songinfo and \
  139. 'title' in songinfo and \
  140. 'time' in songinfo:
  141. album = songinfo.get('album', '')
  142. tracknumber = songinfo.get('track', '')
  143. try:
  144. self.scrob_post.nowplaying(songinfo.artist,
  145. songinfo.title,
  146. songinfo.time,
  147. tracknumber,
  148. album)
  149. except:
  150. self.logger.exception(
  151. "Unable to send 'now playing' data to the scrobbler")
  152. time.sleep(10)
  153. def post(self, prevsonginfo):
  154. self.init()
  155. if self.config.as_enabled and self.scrob_post and prevsonginfo:
  156. if 'artist' in prevsonginfo and \
  157. 'title' in prevsonginfo and \
  158. 'time' in prevsonginfo:
  159. album = prevsonginfo.get('album', '')
  160. tracknumber = prevsonginfo.get('track', '')
  161. try:
  162. self.scrob_post.addtrack(
  163. prevsonginfo.artist,
  164. prevsonginfo.title,
  165. prevsonginfo.time,
  166. self.scrob_start_time,
  167. tracknumber,
  168. album)
  169. except:
  170. self.logger.critical("Unable to add track to scrobbler")
  171. thread = threading.Thread(target=self.do_post)
  172. thread.daemon = True
  173. thread.start()
  174. self.scrob_start_time = ""
  175. def do_post(self):
  176. for _i in range(0, 3):
  177. if not self.scrob_post:
  178. return
  179. if len(self.scrob_post.cache) == 0:
  180. return
  181. try:
  182. self.scrob_post.post()
  183. except audioscrobbler.AudioScrobblerConnectionError as e:
  184. self.logger.exception(
  185. "Error while posting data to the scrobbler")
  186. time.sleep(10)
  187. def save_cache(self):
  188. """Save the cache in a file"""
  189. filename = os.path.expanduser('~/.config/sonata/ascache')
  190. if self.scrob_post:
  191. self.scrob_post.savecache(filename)
  192. def retrieve_cache(self):
  193. filename = os.path.expanduser('~/.config/sonata/ascache')
  194. if self.scrob_post:
  195. self.scrob_post.retrievecache(filename)