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.
konversation/konversation/scripts/media

486 lines
16 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
# Copyright 2006 Eli J. MacKenzie
# Inspired by `media` (Copyright 2005 İsmail Dönmez)
# If you wish to customize the formatting strings, do so in this table.
# Do not change the numbers unless you're changing the logic.
# Title, artist, and album will be set once the player is queried.
# See Player.format() for how these are used.
#Changing these 3 values will likely cause the script to fail
Title =4
Artist=2
Album =1
#To disable self-titled (eponymous) checking, subtract 8
SelfTitled=11
outputFormat="/me $intro $info [$player]"
formatStrings = {
Title+SelfTitled : "$title by $artist (eponymous)",
SelfTitled : "${artist}'s self-titled album",
Title+Artist+Album : "$title by $artist on $album", #7,15
Title+Artist : "$title by $artist", #6,14
Title+Album : "$title from $album", #5,13
Album+Artist : "$album by $artist", #3,11
Title : "$title", #4,12
Artist : "$artist", #2,10
Album : "$album", #1,9
}
#Intro defaults to first type the player supports when a specific type was not demanded
formatVariables={'audio': 'is listening to', 'video': 'is watching'}
## Static player ranking list
## If you add a new player, you must add it here or it won't get checked when in audio-only or video-only modes.
playerRankings= {
'video' :['kaffeine','kmplayer', 'kplayer', 'noatun', 'kdetv'],
'audio' :['amarok', 'MPD' 'juk', 'noatun', 'kscd', 'kaffeine', 'kmplayer', 'Audacious', 'xmms', 'yammi']
}
## Title, album and artist fields to be quoted depending on contents
# List the possible trigger characters here.
# If you want a '-', it must be first. if you want a '^', it must be last.
SIMPLE_FIXUP = '' #I use ' '
# If you want to use a regex for the above, specify it here in which case it will be used
REGEX_FIXUP = ''
# Quote chars to use:
QUOTE_BEFORE = '"'
QUOTE_AFTER = '"'
###############################
## The Real work is done below
#############################
import os
import sys
import re
import string
from functools import reduce
try:
APP_ID = sys.argv[1]
IRC_SERVER = sys.argv[2]
TARGET = sys.argv[3]
except IndexError:
print("This script is intended to be run from within Konversation.", file=sys.stderr)
sys.exit(0)
if (sys.hexversion >> 16) < 0x0204:
err="The media script requires Python 2.4."
os.popen('dcop %s default error "%s"' %(APP_ID,err))
sys.exit(err)
import subprocess
# Python 2.5 has this ...
try:
any(())
except NameError:
def any(data):
"""Return true of any of the items in the sequence 'data' are true.
(ie non-zero or not empty)"""
try:
return reduce(lambda x,y: bool(x) or bool(y), data)
except TypeError:
return False
def tell(data, feedback='info'):
"""Report back to the user"""
l=['dcop', APP_ID, 'default', feedback]
if type(data) is type(''):
l.append(data)
else:
l.extend(data)
subprocess.Popen(l).communicate()
class Player(object):
def __init__(self, display_name, playerType=None):
if playerType is None:
self.type = "audio"
else:
self.type=playerType
self.displayName=display_name
self.running = False
d={}
d.update(formatVariables)
d['player']=self.displayName
self._format = d
def get(self, mode):
data=self.getData()
if any(data):
self._format['info']=self.format(*data)
if mode and mode != self.displayName:
self._format['intro']=self._format[mode]
else:
self._format['intro']=self._format[self.type.replace(',','').split()[0]]
return string.Template(outputFormat).safe_substitute(self._format)
return ''
def format(self, title='', artist='', album=''):
"""Return a 'pretty-printed' info string for the track.
Uses formatStrings from above."""
#Update args last to prevent non-sensical override in formatVariables
x={'title':title, 'artist':artist, 'album':album}
if FIXUP:
for i,j in list(x.items()):
if re.search(FIXUP,j):
x[i]='%s%s%s'%(QUOTE_BEFORE,j,QUOTE_AFTER)
self._format.update(x)
n=0
if title:
n|=4 #Still binary to make you read the code ;p
if artist:
if artist == album:
n|=SelfTitled
else:
n|=2
if album:
n|=1
if n:
return string.Template(formatStrings[n]).safe_substitute(self._format)
return ''
def getData(self):
"""Implement this to do the work"""
return ''
def reEncodeString(self, input):
if input:
try:
input = input.decode('utf-8')
except UnicodeError:
try:
input = input.decode('latin-1')
except UnicodeError:
input = input.decode('ascii', 'replace')
except NameError:
pass
return input.encode('utf-8')
def test_format(self, title='', artist='', album=''):
s=[]
l=["to","by","on"]
if title:
s.append(title)
else:
album,artist=artist,album
l.pop()
if artist:
s.append(artist)
else:
del l[1]
if album:
s.append(album)
else:
l.pop()
t=["is listening"]
while l:
t.append(l.pop(0))
t.append(s.pop(0))
return ' '.join(t)
def isRunning(self):
return self.running
class DCOPPlayer(Player):
def __init__(self, display_name, service_name, getTitle='', getArtist='', getAlbum='',playerType=None):
Player.__init__(self, display_name, playerType)
self.serviceName=service_name
self._title=getTitle
self._artist=getArtist
self._album=getAlbum
self.DCOP=""
def getData(self):
self.getService()
return (self.grab(self._title), self.grab(self._artist), self.grab(self._album))
def getService(self):
if self.DCOP:
return self.DCOP
running = re.findall('^' + self.serviceName + "(?:-\\d*)?$", DCOP_ITEMS, re.M)
if type(running) is list:
try:
running=running[0]
except IndexError:
running=''
self.DCOP=running.strip()
self.running=bool(self.DCOP)
return self.DCOP
def grab(self, item):
if item and self.isRunning():
return self.reEncodeString(os.popen("dcop %s %s"%(self.DCOP, item)).readline().rstrip('\n'))
return ''
def isRunning(self):
self.getService()
return self.running
class AmarokPlayer(DCOPPlayer):
def __init__(self):
DCOPPlayer.__init__(self,'Amarok','amarok','player title','player artist','player album')
def getData(self):
data=DCOPPlayer.getData(self)
if not any(data):
data=(self.grab('player nowPlaying'),'','')
if not data[0]:
return ''
return data
#class Amarok2Player(Player):
# def __init__(self):
# Player.__init__(self, 'Amarok2', 'audio')
# self.isRunning()
#
# def getData(self):
# playing=os.popen("qdbus org.mpris.amarok /Player PositionGet").readline().strip() != "0"
# if playing and self.isRunning():
# for line in os.popen("qdbus org.mpris.amarok /Player GetMetadata").readlines():
# if re.match("^title", line):
# title=self.reEncodeString(line.strip().split(None,1)[1])
# if re.match("^artist", line):
# artist=self.reEncodeString(line.strip().split(None,1)[1])
# if re.match("^album", line):
# album=self.reEncodeString(line.strip().split(None,1)[1])
# return (title, artist, album)
# else:
# return ''
#
# def isRunning(self):
# qdbus_items=subprocess.Popen(['qdbus'], stdout=subprocess.PIPE).communicate()[0]
# running=re.findall('^ org.mpris.amarok$', qdbus_items, re.M)
# if type(running) is list:
# try:
# running=running[0]
# except IndexError:
# running=''
# self.running=bool(running.strip())
# return self.running
import socket
class MPD(Player):
def __init__(self, display_name):
Player.__init__(self, display_name)
self.host = "localhost"
self.port = 6600
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(0.5)
try:
self.sock.connect((self.host, self.port))
# just welcome message, we don't need it
self.sock.recv(1024)
self.running = True
except socket.error:
self.running = False
def getData(self):
if not self.running:
return ''
try:
self.sock.send("currentsong\n")
data = self.sock.recv(1024)
except socket.error:
return ''
# mpd sends OK always, so if nothing to show, we should seek for at least 3 chars
if len(data) < 4:
return ''
else:
# if there is Artist, Title and Album, get it
data=data.splitlines()
d={}
for i in data:
if ':' not in i:
continue
k,v=i.split(':',1)
d[k.lower()]=self.reEncodeString(v.strip())
data=(d.get('title',''),d.get('artist',''),d.get('album',''))
if not any(data):
return d.get('file','')
return data
class StupidPlayer(DCOPPlayer):
def getData(self):
data=DCOPPlayer.getData(self)[0]
if data:
if data.startswith('URL'):
# KMPlayer window titles in the form of "URL - file:///path/to/<media file> - KMPlayer"
data=data.split(None,2)[2].rsplit(None,2)[0].rsplit('/')[-1]
else:
# KPlayer window titles in the form of "<media file> - KPlayer"
data=data.rsplit(None,2)[0]
return (data,'','')
return ''
try:
import xmms.common
class XmmsPlayer(Player):
def __init__(self, display_name):
Player.__init__(self, display_name)
def isRunning(self):
self.running = xmms.control.is_running()
return self.running
def getData(self):
if self.isRunning() and xmms.control.is_playing():
# get the position in the playlist for current playing track
index = xmms.control.get_playlist_pos();
# get the title of the currently playing track
return (self.reEncodeString(xmms.control.get_playlist_title(index)),'','')
return ''
except ImportError:
XmmsPlayer=Player
class AudaciousPlayer(Player):
def __init__(self, display_name):
Player.__init__(self, display_name)
def isRunning(self):
self.running = not os.system('audtool current-song')
return self.running
def getData(self):
if self.isRunning() and not os.system('audtool playback-playing'):
# get the title of the currently playing track
data = os.popen('audtool current-song').read().strip()
data_list = data.split(' - ')
list_length = len(data_list)
if list_length == 1:
return (self.reEncodeString(data_list[0]),'','')
elif list_length == 3:
return (self.reEncodeString(data_list[-1]),data_list[0],data_list[1])
else:
return (self.reEncodeString(data),'','')
else:
return ''
def playing(playerList, mode=None):
for i in playerList:
s=i.get(mode)
if s:
tell([IRC_SERVER, TARGET, s], 'say' )
return 1
return 0
def handleErrors(playerList, kind):
if kind:
kind=kind.strip()
kind=kind.center(len(kind)+2)
else:
kind= ' supported '
x=any([i.running for i in playerList])
if x:
l=[i.displayName for i in playerList if i.isRunning()]
err= "Nothing is playing in %s."%(', '.join(l))
else:
err= "No%splayers are running."%(kind,)
tell(err,'error')
def run(kind):
if not kind:
kind = ''
play=PLAYERS
else:
if kind in ['audio', 'video']:
unsorted=dict([(i.displayName.lower(),i) for i in PLAYERS if kind in i.type])
play=[unsorted.pop(i.lower(),Player("ImproperlySupported")) for i in playerRankings[kind]]
if len(unsorted):
play.extend(list(unsorted.values()))
else:
play=[i for i in PLAYERS if i.displayName.lower() == kind]
try:
kind=play[0].displayName
except IndexError:
tell("%s is not a supported player."%(kind,),'error')
sys.exit(0)
if not playing(play, kind):
handleErrors(play, kind)
#It would be so nice to just keep this pipe open and use it for all the dcop action,
#but of course you're supposed to use the big iron (language bindings) instead of
#the command line tools. One could consider `dcop` the bash dcop language binding,
#but of course when using shell you don't need to be efficient at all, right?
DCOP_ITEMS=subprocess.Popen(['dcop'], stdout=subprocess.PIPE).communicate()[0] #re.findall("^amarok(?:-\\d*)?$",l,re.M)
# Add your new players here. No more faulty logic due to copy+paste.
PLAYERS = [
AmarokPlayer(),
DCOPPlayer("JuK","juk","Player trackProperty Title","Player trackProperty Artist","Player trackProperty Album"),
DCOPPlayer("Noatun",'noatun',"Noatun title",playerType='audio, video'),
DCOPPlayer("Kaffeine","kaffeine","KaffeineIface title","KaffeineIface artist","KaffeineIface album",playerType='video, audio'),
StupidPlayer("KMPlayer","kmplayer","kmplayer-mainwindow#1 caption",playerType="video audio"),
StupidPlayer("KPlayer","kplayer","kplayer-mainwindow#1 caption",playerType="video audio"),
DCOPPlayer("KsCD","kscd","CDPlayer currentTrackTitle","CDPlayer currentArtist","CDPlayer currentAlbum"),
DCOPPlayer("kdetv","kdetv","KdetvIface channelName",playerType='video'),
AudaciousPlayer('Audacious'), XmmsPlayer('XMMS'),
DCOPPlayer("Yammi","yammi","YammiPlayer songTitle","YammiPlayer songArtist","YammiPlayer songAlbum"),
MPD('MPD')
]
# Get rid of players that didn't get subclassed so they don't appear in the available players list
for i in PLAYERS[:]:
if type(i) is Player:
PLAYERS.remove(i)
if REGEX_FIXUP:
FIXUP=REGEX_FIXUP
elif SIMPLE_FIXUP:
FIXUP="[%s]"%(SIMPLE_FIXUP)
else:
FIXUP=''
# It all comes together right here
if __name__=="__main__":
if not TARGET:
s="""media v2.0.1 for Konversation 1.0. One media command to rule them all, inspired from Kopete's now listening plugin.
Usage:
"\00312/media\017" - report what the first player found is playing
"\00312/media\017 [ '\00312audio\017' | '\00312video\017' ]" - report what is playing in a supported audio or video player
"\00312/media\017 { \00312Player\017 }" - report what is playing in \00312Player\017 if it is supported
Available players are:
""" + ', '.join([("%s (%s)"%(i.displayName,i.type)) for i in PLAYERS])
for i in s.splitlines():
tell(i)
#tell("%s"%(len(s.splitlines()),))
# called from the server tab
pass
else:
try:
kind = sys.argv[4].lower()
except IndexError:
kind = None
run(kind)