Advertisement
Guest User

mpdlfm.py

a guest
Jun 8th, 2010
306
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.64 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. #!/usr/bin/env python
  3. import mpd
  4. import socket
  5. import logging
  6. import sys
  7. import time
  8. import os
  9. import re
  10.  
  11. APPNAME = 'MPD-Last.FM'
  12. SLEEP_LARGE = 2
  13. SLEEP_SMALL = .5
  14. NOSCROBBLE = re.compile('^(https?|mms|rtsp)://.+', re.I)
  15. LFMCLIENT_PORT = 33367
  16.  
  17. # Где искать конфиги
  18.  
  19. CONFIGFILES = [
  20.     '/etc/mpdlastfm.conf',
  21.     os.path.join( os.environ.get('HOME', ''), '.mpdlastfm.conf' )
  22. ]
  23.  
  24. # Конфиг по умолчанию
  25.  
  26. config = {
  27.     # параметры подключения к mpd
  28.     'mpd_host' : 'localhost',
  29.     'mpd_port' : 6600,
  30.     'mpd_pass' : '',
  31.     # путь до корня с музыкой. Клиент Last.FM не прочь получить полный путь
  32.     'mpd_root' : os.path.join( os.environ['HOME'], '.music' ),
  33.     # файл с логом. ротации нет
  34.     'log_file' : '/var/log/mpdlfm/mdlfm.log',
  35. }
  36.  
  37. # Простая читалка конфигов
  38.  
  39. for cfile in CONFIGFILES:
  40.     try:
  41.         for line in file(cfile).xreadlines():
  42.             line.strip()
  43.             if line[0] == '#': continue
  44.             try:
  45.                 key, value = [x.strip() for x in line.split('=')]
  46.                 if key in config:
  47.                     config[key] = value
  48.             except ValueError:
  49.                 continue
  50.     except:
  51.         pass
  52.  
  53. config['mpd_port'] = int( config['mpd_port'] )
  54.  
  55. # Настраиваем лог
  56.  
  57. log_dir = os.path.dirname( config['log_file'] )
  58. if not os.path.exists(log_dir):
  59.     os.makedirs(log_dir)
  60.  
  61. logging.basicConfig(level=logging.DEBUG,
  62.                     format='%(asctime)s %(levelname)-8s %(message)s',
  63.                     datefmt='%d/%m/%Y %H:%M:%S',
  64.                     filename=config['log_file'],
  65.                     filemode='w')
  66.  
  67. console = logging.StreamHandler()
  68. logging.getLogger('').addHandler(console)
  69. formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
  70. console.setFormatter(formatter)
  71. logger = logging.getLogger(APPNAME)
  72.  
  73. logger.info(u'Начинаем')
  74.  
  75. class LFMClient():
  76.     def __init__(self, host = 'localhost',
  77.             port = LFMCLIENT_PORT, timeout = 5.0):
  78.         self.id = "mdc" # Это ID от mpdscribble
  79.         self.host = host
  80.         self.port = port
  81.         self.timeout = timeout
  82.  
  83.     def _build_command(self, command, **kwargs):
  84.         kwargs['c'] = self.id
  85.  
  86.         # ключ=значение
  87.         # объединяем при помощи &
  88.         # & в значениях экранируем им же: &&
  89.         command += ' '
  90.         for key, value in kwargs.items():
  91.             command += '%s=%s&' % (key, value.replace('&', '&&'))
  92.  
  93.         # не забываем про перевод строки
  94.         command = command[:-1] + '\n'
  95.  
  96.         return command
  97.  
  98.     def send_command(self, command, **kwargs):
  99.         command = self._build_command(command, **kwargs)
  100.  
  101.         try:
  102.             # Клиент закрывает соединение после каждой (?) комманды
  103.             # Так что не надо заморачиваться с постоянным соединением
  104.             _socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
  105.             _socket.connect(( self.host, self.port ))
  106.             _socket.settimeout( self.timeout )
  107.  
  108.             command_bytes = command.encode('utf-8')
  109.             sent = _socket.send( command_bytes )
  110.  
  111.             if sent == len( command_bytes ):
  112.                 logger.debug(u"Отправлено: %s" % command.strip())
  113.             else:
  114.                 logger.error( u"Не удалось отправить комманду клиенту" )
  115.  
  116.             _socket.shutdown(socket.SHUT_RDWR)
  117.             _socket.close()
  118.         except socket.error, e:
  119.             logger.error('Нет соединения с Last.FM клиентом: %s' % e[1] )
  120.  
  121.     def track_changed(self, trackinfo):
  122.         logger.info(u'Начинается воспроизведение нового трека)')
  123.  
  124.         try:
  125.             # Надеюсь, что MPD всегда отдаёт инфу в UTF-8
  126.             artist      = trackinfo.get('artist', '').decode('utf-8')
  127.             title       = trackinfo.get('title', '').decode('utf-8')
  128.             filename    = trackinfo.get('file', '').decode('utf-8')
  129.             album       = trackinfo.get('album', '').decode('utf-8')
  130.         except UnicodeDecodeError, e:
  131.             logger.error(u'Неверная последовательность символов в значении тега: %s'
  132.                 % e)
  133.             self.send_command("STOP")
  134.             return
  135.  
  136.         length = int( trackinfo.get('time', 0) )
  137.  
  138.         if artist and title and length:
  139.             logger.info(u'Трек: %s - %s (%s)' % (artist, title, filename))
  140.         else:
  141.             # Пишем предупреждение в лог, если надо
  142.             # Но клиенту всё равно отправим потом
  143.             # Пусть сам решает, что делать с коротким или с неправильными
  144.             # тегами файлом
  145.             logger.warn(u'В файле отсутствуют теги или он слишком короткий. Скорее всего он не будет заскробблен')
  146.  
  147.         mbId = ''
  148.  
  149.         self.send_command("START", a = artist, t = title, b = album, m = mbId,
  150.                 l = str(length), p = filename)
  151.  
  152.     def state_changed(self, oldstate, newstate):
  153.         if newstate == 'play':
  154.             logger.info(u'Воспроизведение возобновлено')
  155.             self.send_command('RESUME')
  156.             pass
  157.         elif newstate == 'stop':
  158.             logger.info(u'Воспроизведение остановлено')
  159.             self.send_command('STOP')
  160.             pass
  161.         elif newstate == 'pause':
  162.             logger.info(u'Воспроизведение поставлено на паузу')
  163.             self.send_command('PAUSE')
  164.             pass
  165.         else:
  166.             logger.warn(u'MPD находится в неизвестном состоянии: %s'
  167.                 % newstate)
  168.  
  169. class MPDHelper():
  170.     def __init__(self, host, port, password,
  171.             root, state_changed, track_changed):
  172.         self._mpd = mpd.MPDClient()
  173.  
  174.         self.host = host
  175.         self.port = port
  176.         self.password = password
  177.         self.root = root
  178.  
  179.         self.state_changed = state_changed
  180.         self.track_changed = track_changed
  181.  
  182.         self._servername = '%s:%d' % (host, port)
  183.         if password:
  184.             self._servername = '******@' + self._servername
  185.  
  186.         self.connection_status = None
  187.         self.last_file_played = None
  188.         self.last_state = None
  189.  
  190.     def _connect(self):
  191.         # Пингуем, коннектимся, реконнектимся
  192.         # «Умное» постоянное соединение
  193.         try:
  194.             self._mpd.ping()
  195.         except socket.error:
  196.             pass
  197.         except mpd.ConnectionError:
  198.             try:
  199.                 self._mpd.disconnect()
  200.             except socket.error:
  201.                 pass
  202.             except mpd.ConnectionError:
  203.                 pass
  204.  
  205.             try:
  206.                 self._mpd.connect(self.host, self.port)
  207.                 self._mpd.ping()
  208.  
  209.                 if self.password:
  210.                     self._mpd.password(self.password)
  211.  
  212.                 if self.connection_status is None:
  213.                     logger.info(u"Установленно соединение с MPD (%s)"
  214.                         % (self._servername,))
  215.                 else:
  216.                     logger.warn(u"Переподключение к MPD (%s)"
  217.                         % (self._servername,))
  218.  
  219.                 self.connection_status = True
  220.             except (mpd.ConnectionError, mpd.CommandError, socket.error), e:
  221.                 try:
  222.                     # На всякий случай отключаемся
  223.                     self._mpd.disconnect()
  224.                 except socket.error:
  225.                     pass
  226.                 except mpd.ConnectionError:
  227.                     pass
  228.  
  229.                 error_message = e[1] if isinstance(e, socket.error) else e
  230.  
  231.                 if self.connection_status:
  232.                     logger.error(u"Потеряно подключение к MPD (%s): %s"
  233.                         % (self._servername, error_message))
  234.                 else:
  235.                     logger.error(u"Не удается подключиться к MPD (%s): %s. Проверьте адрес сервера и пароль в настройках"
  236.                         % (self._servername, error_message))
  237.  
  238.                 self.connection_status = False
  239.  
  240.         return self.connection_status
  241.  
  242.     def poll(self):
  243.         try:
  244.             currentsong, status = self._mpd.currentsong(), self._mpd.status()
  245.  
  246.             # Пытаемся получить полное имя файла на диске, если возможно
  247.             file = currentsong.get('file')
  248.             if self.root and file:
  249.                 if not NOSCROBBLE.match(file):
  250.                     if file.startswith('file://'):
  251.                         file = file[7:]
  252.                     fullpath = os.path.join(self.root, file)
  253.                     if os.path.exists(fullpath):
  254.                         currentsong['file'] = os.path.join(self.root, file)
  255.  
  256.         except (mpd.ConnectionError, mpd.CommandError, socket.error), e:
  257.             error_message = e[1] if isinstance(e, socket.error) else e
  258.             logger.warn(u'Невозможно получить информацию о текущей композиции: %s'
  259.                 % error_message)
  260.             return False
  261.  
  262.         new_state = status.get('state')
  263.  
  264.         # Если изменилось состояние, то сообщаем клиенту об этом
  265.         # Надо учесть, что при запуске скрипта MPD уже может работать, но при
  266.         # этом стоять на паузе и пр. Об этом сообщать не будем
  267.  
  268.         if new_state != self.last_state:
  269.             if self.state_changed and self.last_state is not None:
  270.                 self.state_changed(self.last_state, new_state)
  271.  
  272.         # Если изменилось имя файла
  273.         # Если мы вышли из состояния ОСТАНОВЛЕННО
  274.         # ... и при этом сейчас находимся в состоянии ВОСПРОИЗВЕДЕНИЕ
  275.         # То сообщаем о новом треке
  276.         if (
  277.                 (
  278.                     currentsong.get('file') != self.last_file_played or
  279.                     self.last_state == 'stop'
  280.                 ) and
  281.                 new_state == 'play'
  282.             ):
  283.             self.track_changed( currentsong )
  284.  
  285.         # Сохраняем состояние и имя проигрываемого файла
  286.         self.last_file_played = currentsong.get('file')
  287.         self.last_state = new_state
  288.  
  289.         return True
  290.  
  291. lfm = LFMClient()
  292. mpdpoll = MPDHelper(config['mpd_host'], config['mpd_port'], config['mpd_pass'], config['mpd_root'], lfm.state_changed, lfm.track_changed)
  293.  
  294. # Пробуем запустить Last.FM клиента сами
  295. # Если он уже работает, то вторая копия не запустится
  296. # Так что дополнительно проверять не нужно
  297. from subprocess import Popen
  298. for procname in ['last.fm', 'lastfm']:
  299.     try:
  300.         logger.debug(u'Попытка запустить процесс %s' % procname)
  301.         stdin = open(os.devnull, 'r')
  302.         stdout = open(os.devnull, 'a+')
  303.         stderr = open(os.devnull, 'a+')
  304.  
  305.         Popen([procname, "-tray"], stdin = stdin, stdout = stdout, stderr = stderr).pid
  306.         logger.debug(u'Процесс запущен. Немного подождем')
  307.         time.sleep(3)
  308.         break
  309.     except:
  310.         logger.debug(u'Не удалось запустить процесс: %s' % sys.exc_info()[1])
  311.  
  312. # Пока живы опрашиваем MPD
  313. while True:
  314.     # «Умный» connect
  315.     mpdpoll._connect()
  316.  
  317.     # Если нет соединения с MPD, то выдержим паузу побольше
  318.     if not mpdpoll.connection_status:
  319.         time.sleep(SLEEP_LARGE)
  320.         continue
  321.  
  322.     # Если клиент отвечает, то опрашиваем
  323.     mpdpoll.poll()
  324.     # и выдерживаем короткую паузу, перед следущей попыткой
  325.     time.sleep(SLEEP_SMALL)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement