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.
428 lines
10 KiB
428 lines
10 KiB
#!/usr/bin/python |
|
|
|
import threading |
|
import random |
|
import os.path |
|
import os |
|
import time |
|
import socket |
|
import asyncore |
|
import asynchat |
|
import cmd |
|
import ConfigParser |
|
import sys |
|
import json |
|
|
|
import blup.animation as Animation |
|
import blup.output as BlupOutput |
|
import blup.frame as Frame |
|
|
|
|
|
CONFIG_DEFAULTS = { |
|
'output': 'shell', |
|
'playlist': '', |
|
'animation_dir': '.', |
|
'enabled': False, |
|
'shuffle': False, |
|
'autorefresh': False |
|
} |
|
|
|
def find(f, list): |
|
for x in list: |
|
if f(x): |
|
return x |
|
|
|
class AnimationInfo(): |
|
def __init__(self, filename): |
|
self.__filename = filename |
|
self.__tags = None |
|
self.__valid = None |
|
|
|
@property |
|
def filesize(self): |
|
try: |
|
statinfo = os.stat(self.__filename) |
|
return statinfo.st_size |
|
except OSError: |
|
return -1 |
|
|
|
@property |
|
def mtime(self): |
|
try: |
|
statinfo = os.stat(self.__filename) |
|
return statinfo.st_mtime |
|
except OSError: |
|
return -1 |
|
|
|
@property |
|
def tags(self): |
|
return self.__tags |
|
|
|
@property |
|
def valid(self): |
|
if self.__valid == None: |
|
try: |
|
self.check() |
|
except Animation.AnimationFileError: |
|
pass |
|
return self.__valid |
|
|
|
@property |
|
def filename(self): |
|
return self.__filename |
|
|
|
def check(self): |
|
self.__valid = False |
|
self.__tags = None |
|
|
|
anim = Animation.load(self.__filename) |
|
|
|
self.__valid = True |
|
self.__tags = anim.tags |
|
|
|
|
|
class AnimationDatabase(): |
|
def __init__(self, animDir): |
|
self.__animDir = animDir |
|
self.__animInfos = [] |
|
self.__lock = threading.Lock() |
|
|
|
def update(self): |
|
with self.__lock: |
|
oldAnimInfos = self.__animInfos |
|
animInfos = [] |
|
|
|
for (dirpath, dirnames, filenames) in os.walk(self.__animDir): |
|
files = map(lambda f: os.path.join(dirpath, f), filenames) |
|
files = map(lambda f: os.path.relpath(os.path.join(dirpath, f), self.__animDir), filenames) |
|
animInfos.extend(map(lambda f: AnimationInfo(f), files)) |
|
#animInfos = filter(lambda a: a.valid, animInfos) |
|
|
|
newAnimInfos = [] |
|
for animInfo in animInfos: |
|
oldAnimInfo = find(lambda a: a.filename == animInfo.filename, oldAnimInfos) |
|
if oldAnimInfo is not None and oldAnimInfo.mtime == animInfo.mtime: |
|
newAnimInfos.append(oldAnimInfo) |
|
else: |
|
if animInfo.valid: |
|
newAnimInfos.append(animInfo) |
|
|
|
self.__animInfos = newAnimInfos |
|
|
|
def findAnimation(self, filename): |
|
with self.__lock: |
|
animInfo = find(lambda a: a.filename == filename, self.__animInfos) |
|
if animInfo is not None: |
|
return animInfo |
|
else: |
|
raise Exception('animation not found!') |
|
|
|
def containsAnimation(self, filename): |
|
with self.__lock: |
|
animInfo = find(lambda a: a.filename == filename, self.__animInfos) |
|
return (animInfo is not None) |
|
|
|
def getAllFilenames(self): |
|
with self.__lock: |
|
return map(lambda a: a.filename, self.__animInfos) |
|
|
|
|
|
#class Playlist(list): |
|
# def __init__(self, animationDatabase, filename=None): |
|
# self.__animDb = animationDatabase |
|
# self.__filename = filename |
|
# if filename == None: |
|
# self.append(self.__animDb.getAllFilenames()) |
|
# else: |
|
# f = open(filename, 'r') |
|
# lines = f.read().split('\n') |
|
# f.close() |
|
# |
|
# for line in lines: |
|
# if self.__animDb.containsAnimation(line): |
|
# self.append(line) |
|
# |
|
# def refresh(self): |
|
# #f = open(filename, 'r') |
|
# #lines = f.read().split('\n') |
|
# #f.close() |
|
# pass |
|
# |
|
# #for line in lines: |
|
# # if self.__animDb.containsAnimation(line): |
|
# # self.append(line) |
|
|
|
|
|
|
|
class PlayerThread(threading.Thread): |
|
def __init__(self, database, output): |
|
threading.Thread.__init__(self) |
|
self.__playlist = [] |
|
self.database = database |
|
self.__playNext = [] |
|
self.maxPlayNext = 3 |
|
self.__output = output |
|
self.shuffle = False |
|
self.autoRefresh = True |
|
self.loopTime = 10 |
|
self.gap = 800 |
|
self.__running = False |
|
self.__player = Animation.AnimationPlayer() |
|
self.__currentIndex = 0 |
|
self.__currentFile = '' |
|
self.__paused = False |
|
|
|
@property |
|
def currentFile(self): |
|
if not self.__paused: |
|
return self.__currentFile |
|
else: |
|
return "" |
|
|
|
@property |
|
def currentFileInfo(self): |
|
if not self.__paused: |
|
return self.database.findAnimation(self.__currentFile) |
|
else: |
|
# TODO: mhh... |
|
return None |
|
|
|
@property |
|
def paused(self): |
|
return self.__paused |
|
|
|
def playNext(self, next): |
|
if len(self.__playNext) < self.maxPlayNext: |
|
self.__playNext.append(next) |
|
else: |
|
raise Exception |
|
|
|
def next(self): |
|
self.__player.stop() |
|
|
|
def togglePaused(self): |
|
self.__paused = not self.__paused |
|
if self.__paused: |
|
self.__player.stop() |
|
|
|
def refreshPlaylist(self): |
|
self.database.update() |
|
self.__playlist = self.database.getAllFilenames() |
|
|
|
def loadPlaylistFile(self): |
|
raise Exception('not yet implemented :(') |
|
|
|
def terminate(self): |
|
self.__player.stop() |
|
self.__running = False |
|
|
|
def run(self): |
|
self.__running = True |
|
while self.__running: |
|
anim = None |
|
if self.autoRefresh: # and self.__currentIndex == len(self.__playlist) - 1: |
|
self.refreshPlaylist() |
|
|
|
if len(self.__playNext) == 0: |
|
if len(self.__playlist) == 0: |
|
print 'busywait!!' |
|
continue |
|
|
|
if self.shuffle: |
|
self.__currentFile = self.__playlist[random.randint(0, len(self.__playlist) - 1)] |
|
else: |
|
if self.__currentIndex >= len(self.__playlist): |
|
self.__currentIndex = 0 |
|
self.__currentFile = self.__playlist[self.__currentIndex] |
|
self.__currentIndex += 1 |
|
else: |
|
self.__currentFile = self.__playNext.pop(0) |
|
anim = Animation.load(self.__currentFile) |
|
|
|
count = 1 |
|
if anim.tags.has_key('loop') and anim.tags['loop'].lower() in ['yes', 'true']: |
|
if anim.duration < self.loopTime * 1000: |
|
count = (self.loopTime * 1000) / anim.duration |
|
|
|
|
|
print 'playing:', anim |
|
print 'tags:', anim.tags |
|
self.__player.play(anim, self.__output, count=count) |
|
print 'elapsed: ', self.__player.elapsed |
|
|
|
if self.__paused: |
|
# TODO: use correct frame size |
|
self.__output.sendFrame(Frame.Frame(18,8)) |
|
while self.__paused: |
|
time.sleep(1) |
|
print 'busywait!!' |
|
|
|
if self.gap > 0 and self.__running: |
|
# TODO: use correct frame size |
|
self.__output.sendFrame(Frame.Frame(18,8)) |
|
time.sleep(self.gap / 1000.0) |
|
|
|
|
|
class PlayerControlClientHandler(asynchat.async_chat): |
|
def __init__(self, sock, player): |
|
asynchat.async_chat.__init__(self, sock=sock) |
|
self.set_terminator('\n') |
|
self.__buffer = '' |
|
self.__player = player |
|
|
|
def collect_incoming_data(self, data): |
|
self.__buffer += data |
|
|
|
def sendPacket(self, data): |
|
self.send(json.dumps(data) + '\n') |
|
|
|
def found_terminator(self): |
|
try: |
|
data = json.loads(self.__buffer) |
|
except ValueError: |
|
self.sendPacket({'error': 'invalid json'}) |
|
return |
|
finally: |
|
self.__buffer = '' |
|
|
|
try: |
|
cmd = data['cmd'] |
|
except (TypeError, KeyError): |
|
self.sendPacket({'error': 'no command given'}) |
|
return |
|
|
|
if cmd == 'refresh': |
|
self.__player.refreshPlaylist() |
|
self.sendPacket({'refresh': 'ok'}) |
|
elif cmd == 'next': |
|
self.__player.next() |
|
self.sendPacket({'next': 'ok'}) |
|
elif cmd == 'togglepaused': |
|
self.__player.togglePaused() |
|
self.sendPacket({'togglepaused': 'ok', 'paused': self.__player.paused}) |
|
elif cmd == 'getpaused': |
|
self.sendPacket({'paused': self.__player.paused}) |
|
elif cmd == 'playnext': |
|
if data.has_key('filename') and type(data['filename']) in [str, unicode]: |
|
try: |
|
self.__player.playNext(data['filename']) |
|
except Exception: |
|
self.sendPacket({'error':'too many animations queued'}) |
|
else: |
|
self.sendPacket({'playnext':'ok'}) |
|
else: |
|
self.sendPacket({'error': 'no or invalid animation file'}) |
|
elif cmd == 'getallfilenames': |
|
self.sendPacket({'allfilenames': self.__player.database.getAllFilenames()}) |
|
elif cmd == 'getcurrentfileinfo': |
|
if not self.__player.paused: |
|
info = self.__player.currentFileInfo.tags |
|
else: |
|
info = {} |
|
self.sendPacket({'currentfileinfo': info}) |
|
elif cmd == 'getcurrentfilename': |
|
self.sendPacket({'currentfilename': self.__player.currentFile}) |
|
else: |
|
self.sendPacket({'error': 'unknown command'}) |
|
|
|
|
|
class PlayerControlServer(asyncore.dispatcher): |
|
def __init__(self, host, port, player): |
|
asyncore.dispatcher.__init__(self) |
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) |
|
self.set_reuse_addr() |
|
self.bind((host, port)) |
|
self.listen(5) |
|
self.__player = player |
|
|
|
def handle_accept(self): |
|
pair = self.accept() |
|
if pair is not None: |
|
sock, addr = pair |
|
print 'control connetcion from', addr |
|
PlayerControlClientHandler(sock, self.__player) |
|
|
|
|
|
|
|
|
|
|
|
# |
|
# main |
|
# |
|
|
|
cfg = ConfigParser.SafeConfigParser(CONFIG_DEFAULTS) |
|
cfg.read('/var/tmp/blup2/blayer.cfg') |
|
|
|
try: |
|
out = BlupOutput.getOutput(cfg.get('output', 'output')) |
|
except ConfigParser.NoSectionError: |
|
sys.err.write('config error: missing \'output\' section') |
|
except ConfigParser.NoOptionError: |
|
sys.err.write('config error: missing \'output\' option in \'output\' section') |
|
|
|
try: |
|
animation_dir = cfg.get('player', 'animation_dir') |
|
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
animation_dir = '.' |
|
|
|
db = AnimationDatabase(animation_dir) |
|
|
|
print 'updating database...' |
|
db.update() |
|
print 'done with updating database...' |
|
print 'updating database (again)...' |
|
db.update() |
|
print 'done with updating database...' |
|
#print db.getAllFilenames() |
|
|
|
#try: |
|
# playlist = Playlist(db, cfg.get('player', 'playlist')) |
|
# print 'file-playluist:', playlist |
|
#except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
# playlist = Playlist(db) |
|
# print 'db-playluist:', playlist |
|
|
|
blayer = PlayerThread(db, out) |
|
blayer.refreshPlaylist() |
|
#sys.exit(0) |
|
|
|
try: |
|
blayer.loopTime = cfg.getint('player', 'looptime') |
|
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
pass |
|
|
|
try: |
|
blayer.shuffle = cfg.getboolean('player', 'shuffle') |
|
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
pass |
|
|
|
try: |
|
blayer.autoRefresh = cfg.getboolean('player', 'autorefresh') |
|
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
pass |
|
|
|
try: |
|
blayer.maxPlayNext = cfg.getint('player', 'maxplaynext') |
|
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
pass |
|
|
|
blayer.start() |
|
|
|
try: |
|
cser = PlayerControlServer(cfg.get('control', 'bind_host'), cfg.getint('control', 'bind_port'), blayer) |
|
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): |
|
cser = None |
|
|
|
|
|
try: |
|
|
|
if cser is not None: |
|
asyncore.loop() |
|
else: |
|
blayer.join() |
|
except KeyboardInterrupt: |
|
print 'nawk!!' |
|
blayer.terminate() |
|
|
|
|
|
|
|
|