#!/usr/bin/python # -*- coding: utf-8 -*- import os, sys import dbus import dbus.service from dbus.mainloop.glib import DBusGMainLoop class StreamDBus(dbus.service.Object): def __init__(self, bus_name, player): dbus.service.Object.__init__(self, bus_name, '/org/freedesktop/StreamFM') self.player = player self.player.title_handler = self.song @dbus.service.method('org.freedesktop.StreamFMIFace') def tune(self, uri): if self.player.now or self.player.dontscrobble: self.player['button1'].clicked() self.player['comboboxentry1'].child.set_text(uri) self.player['window1'].show() self.player['window1'].present() self.player['button2'].clicked() @dbus.service.method('org.freedesktop.StreamFMIFace') def show(self): self.player['window1'].show() self.player['window1'].present() @dbus.service.signal('org.freedesktop.StreamFMIFace') def song(self, title): pass session_bus = dbus.SessionBus(mainloop=DBusGMainLoop(set_as_default=True)) try: dbus_proxy = session_bus.get_object('org.freedesktop.StreamFM', '/org/freedesktop/StreamFM') except dbus.exceptions.DBusException: pass else: # Другая запущенная копия детектед interface = dbus.Interface(dbus_proxy, 'org.freedesktop.StreamFMIFace') if len(sys.argv)==2: interface.tune(sys.argv[1]) else: interface.show() sys.exit() import gtk, gobject import pygst pygst.require("0.10") import gst from xml.sax.saxutils import escape import re try: import pynotify except ImportError: pynotify = None import time import urllib, urllib2 from hashlib import md5 try: import musicbrainz2.webservice as musicbrainz except ImportError: musicbrainz = None from cStringIO import StringIO import traceback from config import get_config, set_config from playlist import parse_playlist import glib SVN_REV = int(open('svn-rev').read()) n = get_config('svn-rev',0) if n < SVN_REV: us = map(int, filter(lambda x: x.isdigit(), os.listdir('update'))) us = filter(lambda x: x > n , us) us.sort() for u in us: os.system('python update/%s'%u) set_config('svn-rev',SVN_REV) del n #import psyco #psyco.full() #from psyco.classes import * def dict_to_params(d): return '&'.join( map( '='.join, map( lambda x: map( lambda y: urllib.quote(y, ''), x), d.items()))) class Submissions: appid = 'str' appversion = '1.0' sessionid = None url = 'http://post.audioscrobbler.com/' def request_get(self, url, d): url = '?'.join(( url, dict_to_params(d))) return urllib2.urlopen(url).read().strip() def request_post(self, url, d): return urllib2.urlopen(url, dict_to_params(d)).read().strip() def init(self, user, password): timestamp = str(int(time.time())) response = self.request_get(self.url, { 'hs': 'true', 'p': '1.2.1', 'c': self.appid, 'v': self.appversion, 'u': user, 't': timestamp, 'a': md5(md5(password).hexdigest()+timestamp).hexdigest() }).split('\n') if response[0]=='OK': self.sessionid, self.nowurl, self.suburl = response[1:] return True def submit(self, scrobbles): d = {'s': self.sessionid} for i in enumerate(scrobbles): artist, title, time = i[1] for j in (('a', artist), ('t', title), ('i', str(time)), ('o', 'R'))+tuple(map(lambda x: (x, ''),'rlbnm')): d['%s[%s]'%(j[0], i[0])] = j[1] response = self.request_post(self.suburl, d) if response=='OK': return True if response=='BADSESSION': self.sessionid = None print response def now(self, title, artist='[unknown]'): response = self.request_post(self.nowurl, { 's': self.sessionid, 'a': artist, 't': title, 'b': '', 'n': '', 'm': '', 'l': '300' # Да, это неправда }) if response=='OK': return True if response=='BADSESSION': self.sessionid = None def fill_columns(treeview,cols): # cols is ('name', type, edited handler[, completion liststore]) for i in enumerate(cols): c = i[1] col = gtk.TreeViewColumn(c[0]) treeview.append_column(col) col.set_sort_column_id(i[0]) if c[1]==gobject.TYPE_BOOLEAN: cell = gtk.CellRendererToggle() col.pack_start(cell, True) col.add_attribute(cell, 'active', i[0]) col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) col.set_fixed_width(64) if c[2]: cell.connect("toggled", c[2]) elif c[1]==gobject.TYPE_STRING: if len(c)==4: cell = gtk.CellRendererCombo() cell.set_property('model',c[3]) cell.set_property('text-column',0) else: cell = gtk.CellRendererText() elif c[1]==gobject.TYPE_INT: cell = gtk.CellRendererSpin() if c[1] in (gobject.TYPE_INT, gobject.TYPE_STRING): col.pack_start(cell, True) col.add_attribute(cell, 'text', i[0]) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) col.set_resizable(True) if c[2]: cell.set_property('editable',True) cell.connect("edited", c[2]) cell.set_data('column',i[0]) def get_selected_iter(treeview): return treeview.get_selection().get_selected()[1] def on_edit_cell(liststore, data): def handler(cell, path, new=None): path = int(path) iter = liststore.get_iter(path) col = cell.get_data('column') if new is None: # CellRendererToggle new = not liststore.get_value(iter, col) # else CellRendererText liststore.set(iter, col, new) data[path][col] = new return handler def tell_error(s,extra=''): m = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_CLOSE, message_format=s) if extra: e = gtk.Expander('Дополнительно') m.vbox.pack_start(e, True, True) e.show() s = gtk.ScrolledWindow() s.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) s.show() e.add(s) t = gtk.TextView() t.set_wrap_mode(gtk.WRAP_WORD) t.set_editable(False) t.get_buffer().set_text(extra) s.add(t) t.show() m.run() m.hide() def excepthook(et,v,tb): error = 'svn revision %s\nPython %s on %s\nPyGTK %s\nGTK %s\nPyGSt %s\nGStreamer %s\n\n'%tuple([ SVN_REV, sys.version, sys.platform ]+map(lambda x: '.'.join(map(str, x)),[ gtk.pygtk_version, gtk.gtk_version, gst.pygst_version, gst.gst_version ])) s = StringIO() traceback.print_exception(et,v,tb,None,s) error += s.getvalue() tell_error('Во время работы произошла ошибка. Если Вы используете последнюю версию приложения, попробуйте завершить его, запустить заново и повторить действия, которые привели к возникновению ошибки. Если она повторилась, зайдите на http://shamangrad.net/report.php?act=list&prj=streamfm, убедитесь, что данная ошибка ещё не описана в BTS, опишите действия, которые привели к возникновению ошибки, и приведите дополнительные данные, указанные в блоке «Дополнительно».',error) sys.excepthook = excepthook class StreamFM: def gtk_main_quit(self, w, d=None): if self.now or self.dontscrobble: self['button1'].clicked() gtk.main_quit() def __init__(self): self.player = gst.element_factory_make("playbin2", "player") fakesink = gst.element_factory_make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink) # А может, это всё-таки реально нагло подружить с icydemux? :-| bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) if pynotify: pynotify.init('StreamFM') builder = gtk.Builder() builder.add_from_file("streamfm.xml") builder.connect_signals(self) self.__getitem__ = lambda x: builder.get_object(x) self.status = gtk.StatusIcon() self.status.connect('activate', lambda w, d=None: self.visibility_toggle()) #self.status.connect('popup-menu',) self.status.set_from_file('streamfm.svg') self['window1'].show() if musicbrainz: self['button9'].show() self.update_recent_streams() login, password, browser, scrobble_non_full, buffer_size, submit_current = get_config('config', ['', '', 'firefox "%s"', False, -1, False]) self['entry1'].set_text(login) self['entry2'].set_text(password) self['entry3'].set_text(browser) self['checkbutton1'].set_active(scrobble_non_full) self['checkbutton2'].set_active(submit_current) self['spinbutton1'].set_value(buffer_size) gtk.link_button_set_uri_hook(lambda w, u: os.system((self['entry3'].get_text()+'&')%u)) self['comboboxentry1'].child.connect('activate', lambda w, d=None: self['button2'].clicked()) self['comboboxentry1'].set_text_column(0) fill_columns(self['treeview1'], ( ('Регулярное выражение?', gobject.TYPE_BOOLEAN, self.filter_changed), ('Не скробблить', gobject.TYPE_BOOLEAN, self.filter_changed), ('Не уведомлять', gobject.TYPE_BOOLEAN, self.filter_changed), ('Фильтр', gobject.TYPE_STRING, self.filter_changed), ('Радио', gobject.TYPE_STRING, self.filter_changed, self['liststore2']))) fill_columns(self['treeview2'], ( ('Удалить', gobject.TYPE_BOOLEAN, self.scrobble_changed), ('Время', gobject.TYPE_STRING, None), ('Исполнитель', gobject.TYPE_STRING, self.scrobble_changed), ('Название', gobject.TYPE_STRING, self.scrobble_changed), ('Информация', gobject.TYPE_STRING, None))) fill_columns(self['treeview3'], ( ('', gobject.TYPE_STRING, None), ('', gobject.TYPE_STRING, None))) fill_columns(self['treeview4'], ( ('Score', gobject.TYPE_INT, None), ('Исполнитель', gobject.TYPE_STRING, None), ('Название', gobject.TYPE_STRING, None))) fill_columns(self['treeview5'], ( ('URL', gobject.TYPE_STRING, None), ('Название', gobject.TYPE_STRING, None))) # Ах, Lisp! Как же красиво выглядит код с кучей скобок в конце каждого выражения :) self.filters = get_config('filters',[]) for i in self.filters: self['liststore3'].append(i) self.queue = get_config('queue', []) self.show_queue() self.update_title('Простаиваю :-(') self.tags = {} self.now = None self.dontscrobble = None self.playlist = [] self.submissions = Submissions() self.title_handler = None if len(sys.argv)==2: self['comboboxentry1'].child.set_text(sys.argv[1]) self['button2'].clicked() def show_queue(self): self['liststore1'].clear() for i in self.queue: e = i[:] t = list(e[1][3:5]) t[0] %= 24 e[1] = ':'.join(map(lambda x: '%.2d'%x, t)) self['liststore1'].append(e) #gtk.main_iteration() def filter_changed(self, *a): on_edit_cell(self['liststore3'], self.filters)(*a) set_config('filters', self.filters) def scrobble_changed(self, *a): on_edit_cell(self['liststore1'], self.queue)(*a) set_config('queue', self.queue) def filter_match(self, s, filnum): # Обратите внимание: фильтры регистронезависимы filters = filter(lambda x: x[filnum], self.filters) filters = filter(lambda x: x[4] in ('', self.uri), self.filters) for f in filter(lambda x: not x[0], filters): # non-regexp if s.lower().find(f[3].lower()) != -1: return True for f in filter(lambda x: x[0], filters): if re.match(f[3], s, re.I): return True return False def update_recent_streams(self): self['liststore2'].clear() for i in filter(None, get_config('recent_streams', [])): self['liststore2'].append([i]) def update_config(self): c = [] for i in '123': c.append(self['entry%s'%i].get_text()) c.append(self['checkbutton1'].get_active()) c.append(self['spinbutton1'].get_value()) c.append(self['checkbutton2'].get_active()) set_config('config', c) def visibility_toggle(self): if self['window1'].get_property("visible"): self['window1'].hide() else: self['window1'].show() self['window1'].present() def update_title(self, t): self['label1'].set_markup('%s'%escape(t)) self['window1'].set_title('StreamFM [%s]'%t) self.status.set_tooltip(t) def show_tip(self, t, err=False): if not self.filter_match(t, 2): # песни нет в списке игнорируемых if err: t = 'Ошибка: '+t else: if self.lastfm_init() and self['checkbutton2'].get_active(): ts = t.split(' - ') if len(t)>1: self.submissions.now('-'.join(ts[1:]), ts[0]) else: self.submissions.now(ts[0]) if self.title_handler: self.title_handler(t) if pynotify: n = pynotify.Notification(t) n.set_urgency(pynotify.URGENCY_LOW) n.set_timeout(5000) n.attach_to_status_icon(self.status) try: n.show() except glib.GError: pass # сделайте меня пофиксить это! def lastfm_init(self, show_error=True): if self.submissions.sessionid: return True try: if not self.submissions.init(self['entry1'].get_text(), self['entry2'].get_text()): if show_error: tell_error('Не удалось залогиниться на last.fm. Проверьте правильность логина и пароля, а также не забудьте выставить вменяемые дату, время и часовой пояс.') return except IOError: if show_error: tell_error('Last.fm временно недоступен.') return return True def button6_clicked_cb(self, w, d=None): if not self.lastfm_init(): return kick_from = time.localtime(time.time()-60*15) allowed = filter(lambda x: x[2] and x[3] and (kick_from > x[1]), self.queue) # шерстим все скробблы, кроме тех, что были за последние 15 минут и что с заполненными тегами scrollable = filter(lambda x: not x[0], allowed) # скробблим допущенное юзером scrobbles = map(lambda x: (x[2], x[3], int(time.mktime(x[1]))), scrollable) # форматируем список скробблов if not scrobbles: tell_error('Нечего скробблить. Если Вы считаете, что верно обратное, подождите 15 минут и попробуйте снова.') return try: done = self.submissions.submit(scrobbles) # если отправка данных прошла успешно, done = True except IOError: done = False if done: for i in allowed: self.queue.remove(i) set_config('queue', self.queue) self.show_queue() def button9_clicked_cb(self, w, d=None): iter = get_selected_iter(self['treeview2']) if not iter: tell_error('Для кого теги править будем?') return pos = self['liststore1'].get_path(iter)[0] artist = self['liststore1'].get_value(iter, 2) title = self['liststore1'].get_value(iter, 3) self['entry4'].set_text(artist) self['entry5'].set_text(title) self['button10'].clicked() if self['dialog1'].run()==0: iter = get_selected_iter(self['treeview4']) if iter: self.queue[pos][2] = self['liststore6'].get_value(iter, 1) self.queue[pos][3] = self['liststore6'].get_value(iter, 2) set_config('queue', self.queue) self.show_queue() self['dialog1'].hide() def treeview4_row_activated_cb(self, w, path, view_column): self['button7'].clicked() def button10_clicked_cb(self, w, d=None): q = musicbrainz.Query() self['liststore6'].clear() try: f = musicbrainz.TrackFilter(title=self['entry5'].get_text(), artistName=self['entry4'].get_text(), limit=100) results = q.getTracks(f) except musicbrainz.WebServiceError: tell_error('Не удалось получить информацию с сервера MusicBrainz') return found = [] for i in results: r = (i.track.artist.name, i.track.title) if r not in found: self['liststore6'].append((i.score, i.track.artist.name, i.track.title)) found.append(r) def scrobble(self): # «скробблим» текущую песню # TODO: преобразование по пользовательскому шаблону if self.now: t, d = self.now, False elif self.dontscrobble: t, d = self.dontscrobble, True else: return # «Это радио» try: artist, title = re.match('^(.*?) - (.*?)(?: \| .*)?$', t).groups() except AttributeError: # Они присылают всякий бред! Я не заскробблю это просто так! artist, title = '', t dt = time.localtime() if time.mktime(dt)-(time.mktime(self.queue[-1][1]) if self.queue else 0)>30: # А прошло ли полминуты, детка? self.queue.append([d, dt, artist, title, t]) set_config('queue', self.queue) self.show_queue() def on_message(self, bus, message): t = message.type if t == gst.MESSAGE_EOS: # Радио кончилось о_О self.player.set_state(gst.STATE_NULL) self.update_title('Радио закончилось!') self.show_tip('Радио закончилось!') elif t == gst.MESSAGE_STATE_CHANGED: if message.parse_state_changed()[1] == gst.STATE_PLAYING and not self.now and not self.dontscrobble: self.update_title('Это радио') elif t == gst.MESSAGE_ERROR: self.player.set_state(gst.STATE_NULL) err, debug = message.parse_error() print debug self.update_title('Ошибка: %s'%err) self.show_tip(str(err), True) self.scrobble() self.playlist_current += 1 self.playlist_current %= len(self.playlist) if self.playlist_current != self.playlist_last_manual_choose: self.play(self.playlist_current) elif t == gst.MESSAGE_BUFFERING: p = message.parse_buffering() if p==100: self['progressbar1'].hide() else: self['progressbar1'].show() self['progressbar1'].set_fraction(p/100.0) self['progressbar1'].set_text('Буферизация: %d%%'%p) elif t == gst.MESSAGE_TAG: for i in message.structure.keys(): m = str(message.structure[i]).strip() self.tags[i] = m self['liststore4'].clear() for j in self.tags.items(): self['liststore4'].append(j) if i=='title': self['button3'].show() if m not in (self.now, self.dontscrobble): # если тег сменился self.scrobble() self.now = m self.dontscrobble = None self['button3'].set_label('Не скробблить') self['image1'].set_from_stock('gtk-no', gtk.ICON_SIZE_LARGE_TOOLBAR) # Привет аниматорам и фанатом кнопки «Стоп» при допущении скробблинга недослушанного: # если песня уже попадала в очередь скробблов за последние 15 минут — удаляем last_kicked_scrobble = None kick_from = time.localtime(time.time()-60*15) for i in filter(lambda x: kick_from < x[1] and x[4]==m, self.queue): last_kicked_scrobble = i self.queue.remove(i) set_config('queue', self.queue) # если песня в чёрном списке или уже игнорировалась за последние 15 минут — нажимаем кнопку «Не скробблить» сами :) if self.filter_match(m, 1) or (last_kicked_scrobble[0] if last_kicked_scrobble else False): self['button3'].clicked() self.show_queue() self.update_title(m) if not last_kicked_scrobble: self.show_tip(m) # не показывать, если уже показывали elif i=='organization': if len(m)>64: m = m[:64]+'…' self['linkbutton1'].set_label(m) elif i=='location': self['linkbutton1'].set_uri(m) self['linkbutton1'].show() def window1_window_state_event_cb(self, w, d=None): if (d.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED) and (d.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED): self.visibility_toggle() # Не уверен, что GtkBuildable схавает лямбды, объявленные в __init__ после инициализации, а экспериментировать сейчас лень def entry1_changed_cb(self, w, d=None): self.update_config() def entry2_changed_cb(self, w, d=None): self.update_config() def entry3_changed_cb(self, w, d=None): self.update_config() def checkbutton1_toggled_cb(self, w, d=None): self.update_config() def checkbutton2_toggled_cb(self, w, d=None): self.update_config() def spinbutton1_value_changed_cb(self, w, d=None): self.update_config() try: self.player.set_property('buffer-size', int(w.get_value())) except TypeError: w.set_sensitive(False) print 'Ваш GStreamer немного устарел, поскольку GstPlayBin2 не поддерживает манипуляции с буфером' def expander1_activate_cb(self, w, d=None): self['window1'].set_resizable(not self['expander1'].get_property('expanded')) def button4_clicked_cb(self, w, d=None): new = [False, False, False, '', ''] self.filters.append(new) self['liststore3'].append(new) def button5_clicked_cb(self, w, d=None): iter = self['treeview1'].get_selection().get_selected()[1] if iter: path = self['liststore3'].get_path(iter)[0] self['liststore3'].remove(iter) del self.filters[path] set_config('filters', self.filters) def button3_clicked_cb(self, w, d=None): if self.now: self.dontscrobble = self.now self.now = None self['button3'].set_label('Я передумал!') self['image1'].set_from_stock('gtk-yes', gtk.ICON_SIZE_LARGE_TOOLBAR) else: self.now = self.dontscrobble self.dontscrobble = None self['button3'].set_label('Не скробблить') self['image1'].set_from_stock('gtk-no', gtk.ICON_SIZE_LARGE_TOOLBAR) def button1_clicked_cb(self, w, d=None): self.play(-1) def treeview5_row_activated_cb(self, w, path, view_column): self.playlist_last_manual_choose = path[0] self.playlist_current = path[0] self.play(path[0]) def play(self, pos=0): # Остановись, о радио, что играет сейчас! self.player.set_state(gst.STATE_NULL) self.update_title('Простаиваю :-(') if self['checkbutton2'].get_active(): # скробблить недослушанное self.scrobble() self.now = None self.dontscrobble = None self['button3'].hide() self['progressbar1'].hide() self['table2'].hide() self['hbox2'].show() self['linkbutton1'].hide() self['linkbutton1'].set_label('Radio') if pos!=-1: # Если это не -1, то нам надо проиграть что-то ещё self['treeview5'].set_cursor(pos) self['hbox2'].hide() self['table2'].show() uri = self.playlist[pos][1] self.tags = {} self.player.set_property("uri", uri) self.player.set_state(gst.STATE_PLAYING) self.update_title('Соединяемся…') def show_playlist(self): self['liststore5'].clear() for i in self.playlist: self['liststore5'].append(i[::-1]) def button2_clicked_cb(self, w, d=None): history = get_config('recent_streams', []) self.uri = self['comboboxentry1'].child.get_text() if not self.uri: # если URL не введён, включаем последнюю игравшую станцию try: self.uri = history[0] except IndexError: tell_error('Введите любую ссылку — у вас пустая история.') return if self.uri[0]=='/': self.uri = 'file://'+self.uri try: # убивать надо за репозитории гниющего софта if sys.hexversion >= 0x020600F0: f = urllib2.urlopen(self.uri, timeout=5) else: # Если ресурс недоступен, приложение повиснет, а виноваты будут в этом нерасторопные контрибьюторы Вашего дистрибутива. f = urllib2.urlopen(self.uri) except (IOError, ValueError): # Или ресурс недоступен, или это какой-то экзотический протокол вроде mmc: или icyx:, иди юзер ввёл белиберду l = [] else: fc = f.read(10240) # надеюсь, безумцев с плейлистами больше 10 кб нет f.close() l = parse_playlist(fc) if l: self.playlist = l # это плейлист else: self.playlist = [['', self.uri]] self.show_playlist() self.playlist_last_manual_choose = 0 self.playlist_current = 0 self.play() try: history.remove(self.uri) except ValueError: pass set_config('recent_streams', [self.uri]+history) self['comboboxentry1'].child.set_text('') self.update_recent_streams() gtk.gdk.threads_init() player = StreamFM() bus = StreamDBus(dbus.service.BusName('org.freedesktop.StreamFM', bus=session_bus), player) gtk.main()