12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- #
- # Requires:
- # apt install mpd
- # apt install mpc
- # apt install lirc
- # apt install python3-pip
- # apt install python3-rpi.gpio
- # apt install python3-pil
- # apt install python3-numpy
- # apt install python3-lirc
- # apt install fonts-takao
- # apt install exfat-fuse exfat-utils
- # pip3 install pyalsaaudio
- # pip3 install pysmb
- # Python_ST7735-raspimag.tar.gz
- # python-mpd2.tar.gz
- # stdlibs
- import sys
- import os
- import signal
- import threading
- from threading import BoundedSemaphore
- from threading import Semaphore
- import queue
- import platform
- from select import select
- from time import sleep
- from urllib.parse import urlparse
- import subprocess
- import hashlib
- import glob
- # ABCMeta
- from abc import ABCMeta, abstractmethod
- # Raspberry pi
- import RPi.GPIO as GPIO
- # PIL
- from PIL import Image
- from PIL import ImageDraw
- from PIL import ImageFont
- # Adafruit
- import ST7735 as TFT
- # import Adafruit_GPIO as GPIO
- import Adafruit_GPIO.SPI as SPI
- # lirc
- import lirc
- # mpd
- from mpd import MPDClient
- from socket import error as SocketError
- # alsaaudio
- from alsaaudio import Mixer
- # SMB
- from smb.SMBConnection import SMBConnection
- from nmb.NetBIOS import NetBIOS
- # sdlib
- from sdlib import simpleSMB
- from sdlib import castQueue
- from sdlib import SDTimer
- # LCD spec.
- LCD_WIDTH = 128
- LCD_HEIGHT = 160
- SPEED_HZ = 8000000
- # SUPERDAC configration
- DC = 24
- RST = 23
- SPI_PORT = 0
- SPI_DEVICE = 0
- SW1 = 5
- SW2 = 6
- # FONT settings
- DEFAULT_FONT = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
- FONT_SIZE = 12
- # PID File
- PIDFILE='/var/run/superdac.pid'
- LOGFILE='/var/log/superdac.log'
- # MPD Socket
- MPD_HOST='/var/run/mpd/socket'
- MPD_PORT=6600
- # lirc configration files
- LIRCRC = ['/etc/lirc/superdac.lircrc', '/usr/local/etc/superdac.lircrc', './superdac.lircrc' ]
- # Volume file
- VOLUME_FILE='/var/tmp/.superdac.volume'
- # Openning Logo Image
- LOGO_FILE = '/home/mpd/scripts/logo.jpg'
- # Message class
- class Message():
- TERM = 0 # 'term'
- DISPLAY = 1 # 'disp'
- RCONTROL = 2 # 'rcontrol'
- MENUON = 3 # 'menuon'
- MENUOFF = 4 # 'menuoff'
- MPD = 5 # 'mpd'
- MPC = 6 # 'mpc'
- PLAYLIST = 7 # 'playlist'
- INDICATOR = 8 # 'indicate'
- MPC_REPLY = 9 # 'mpc_reply'
- VOLUME = 10 # 'volume'
- SYSMENU = 11 # 'sysmenu'
-
- def __init__(self, sender, message, param):
- self.__sender = sender
- self.__message = message
- self.__param = param
-
- def getSender(self):
- return self.__sender
-
- def getMessage(self):
- return self.__message
-
- def getParam(self):
- return self.__param
- # LCD Display Item
- class LCDItem():
-
- FULL = 0
- TOP = 1
- INDICATE = 2
- BOTTOM = 3
- def __init__(self, image, clear=False, pos=FULL, time=0):
- self.__image = image
- self.__pos = pos
- self.__time = time
- self.__clear = clear
- return
-
- def getClear(self):
- return self.__clear
-
- def getImage(self):
- return self.__image
-
- def getPos(self):
- return self.__pos
-
- def getTime(self):
- return self.__time
-
- def getWidth(self):
- return self.__image.size[0]
-
- def getHeight(self):
- return self.__image.size[1]
- # Worker Base
- class Worker(metaclass=ABCMeta):
- # type: (Object, Master)
- def __init__(self, pm):
- self.playermain = pm
-
- def sendMessage(self, message):
- self.playermain.postMessage(message)
-
- @abstractmethod
- def receiveMessage(self, message):
- pass
- # Master Base
- class Master(metaclass=ABCMeta):
- @abstractmethod
- def postMessage(self, message):
- pass
- # Switch worker
- class Switches(Worker):
- def __init__(self, pm):
- # type: (Object, Master)
- super().__init__(pm)
-
- GPIO.setmode(GPIO.BCM)
- GPIO.setup(SW1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
- GPIO.setup(SW2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-
- GPIO.add_event_detect(SW1, GPIO.FALLING, callback=self.swCallback, bouncetime=20)
- GPIO.add_event_detect(SW2, GPIO.FALLING, callback=self.swCallback, bouncetime=20)
-
- def swCallback(self, ch):
- if ch == SW1:
- os.system('/sbin/reboot')
- elif ch == SW2:
- os.system('/sbin/poweroff')
- return
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.TERM:
- GPIO.remove_event_detect(SW1)
- GPIO.remove_event_detect(SW2)
- GPIO.cleanup(SW1)
- GPIO.cleanup(SW2)
-
- return True
- # Playlist manage worker
- class Playlist(Worker):
-
- active = False
- playermain = None
-
- playlists = []
- artists = []
- albums = []
-
- items = []
- current = []
- type = ''
-
- LINES = 10
- LINE_HEIGHT = 16
-
- def __init__(self, pm):
- # type: (Object, Master)
- super().__init__(pm)
-
- # 初期データ取得
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
-
- def makeMenu(self, draw, font):
- self.current = self.items[0:self.LINES-1]
- del self.items[0:self.LINES-1]
- for i in range(0, len(self.current)):
- draw.text((0, i*self.LINE_HEIGHT), str(i+1)+':'+self.current[i], font=font, fill='#FFFFFF' )
- i = i + 1
- if len(self.items) == 0:
- draw.text((0,i*self.LINE_HEIGHT), '0:中止', font=font, fill='#FFFFFF' )
- else:
- draw.text((0,i*self.LINE_HEIGHT), '0:次へ', font=font, fill='#FFFFFF' )
- return
-
- def receiveMessage(self, message):
- # リスト取得
- if message.getMessage() == Message.MPC_REPLY:
- if message.getParam()['type'] == 'playlists':
- self.playlists = message.getParam()['list']
- elif message.getParam()['type'] == 'artist':
- self.artists = message.getParam()['list']
- elif message.getParam()['type'] == 'album':
- self.albums = message.getParam()['list']
- return True
-
- if message.getMessage() == Message.PLAYLIST:
- if not self.active:
- self.type = message.getParam()
- if self.type == 'playlists':
- self.items = self.playlists
- elif self.type == 'artist':
- self.items = self.artists
- elif self.type == 'album':
- self.items = self.albums
-
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- # プレイリストが空
- if len(self.items) == 0:
- draw.text((0,64), 'プレイリストが', font=font, fill='#FF0000')
- draw.text((0,80), ' ありません', font=font, fill='#FF0000')
- item = LCDItem(image, time=2)
- self.sendMessage(Message(self, Message.DISPLAY, item))
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- return False
- else:
- # 新規メニュー描画
- self.makeMenu(draw, font)
- item = LCDItem(image)
- self.playermain.lcd.displayItem(item, lock=True)
- self.active = True
-
- return False
- return False
- # リモコン
- if message.getMessage() == Message.RCONTROL and self.active:
- # 数字ボタン
- if message.getParam() in '0123456789':
- btn = int(message.getParam()) - 1
- if btn < 0:
- if len(self.items) == 0:
- # 終了
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- self.playermain.lcd.release()
- else:
- # 次ページ
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- self.makeMenu(draw, font)
- item = LCDItem(image)
- self.playermain.lcd.release()
- self.playermain.lcd.displayItem(item, lock=True)
- self.active = True
- return False
- elif btn < len(self.current):
- if self.type == 'playlists':
- self.sendMessage(Message(self, Message.MPC, {'command': 'load', 'arg': self.current[btn]}))
- else:
- param = {'command':'findadd', 'arg': {'type':self.type,'value': self.current[btn]}}
- msg = Message(self, Message.MPC, param)
- self.sendMessage(msg)
- self.playermain.lcd.release()
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- self.active = False
- return True
- return True
- # メインメニューに戻る
- elif message.getParam() == 'menu':
- self.playermain.lcd.release()
- self.active = False
-
- return True
- return True
- # system menu
- class SysMenu(Worker):
- active = False
-
- def __init__(self, pm):
- super().__init__(pm)
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.SYSMENU:
- if self.active == False:
- self.active = True
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- jpfont = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
-
- draw.text((0,0) , '1:再起動' , font=jpfont, fill='#FFFFFF')
- draw.text((0,16) , '2:シャットダウン', font=jpfont, fill='#FFFFFF')
- draw.text((0,32) , '3:キャッシュ消去', font=jpfont, fill='#FFFFFF')
- draw.text((0,144), '0:中止' , font=jpfont, fill='#FFFFFF')
-
- item = LCDItem(image)
- self.playermain.lcd.displayItem(item, lock=True)
- return False
-
- elif message.getMessage() == Message.RCONTROL and self.active:
- c = message.getParam()
- if c in '0123':
- if c == '1': # reboot
- os.system('/sbin/reboot')
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- elif c == '2': # shutdown
- os.system('/sbin/poweroff')
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- elif c == '3': # cache clear
- cache_dir = AlbumArt.TEMP_DIR
- [os.remove(f) for f in glob.glob(cache_dir+'/*' )]
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- elif c == '0': # cacnel
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == 'menu': # メインメニューに戻る
- self.active = False
- self.playermain.lcd.release()
- return True
-
- if self.active == False:
- self.playermain.lcd.release()
- return False
- return True
- return True
- # MainMenu Worker
- class MainMenu(Worker):
-
- active = False
- playermain = None # type: Master
- status = []
- playlists = []
-
- def __init__(self, pm):
- # type: (Object, Master)
- super().__init__(pm)
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
-
- def receiveMessage(self,message):
- # MPD Message
- if message.getMessage() == Message.MPD:
- param = message.getParam()
- self.status = param['status']
- return True
-
- if message.getMessage() == Message.MPC_REPLY:
- if message.getParam()['type'] == 'playlists':
- self.playlists = message.getParam()['list']
- return True
-
- # リモコン以外は無視
- if message.getMessage() != Message.RCONTROL:
- return True
-
- # Remote Control Message
- if message.getParam() == 'menu':
- # おそらくありえない
- if len(self.status) == 0:
- return True
-
- # アクティブなら閉じる
- if self.active:
- self.active = False
- self.playermain.lcd.release()
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- return True
-
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists'} ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
- # メニュー描画
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- if self.status['random'] == '1':
- draw.text((0,0),'1:ノーマル再生', font=font,fill='#FFFFFF')
- else:
- draw.text((0,0),'1:ランダム再生', font=font,fill='#FFFFFF')
-
- if self.status['repeat'] == '1':
- draw.text((0,16),'2:リピートしない', font=font,fill='#FFFFFF')
- else:
- draw.text((0,16),'2:リピート再生', font=font,fill='#FFFFFF')
-
- draw.text((0,32 ), '3:現在の曲を外す', font=font,fill='#FFFFFF')
- draw.text((0,48 ), '4:プレイリスト保存', font=font,fill='#FFFFFF')
- draw.text((0,64 ), '5:全曲再生', font=font,fill='#FFFFFF')
- draw.text((0,80 ), '6:プレイリスト読込', font=font,fill='#FFFFFF')
-
- draw.text((0,96 ), '7:アルバム選択', font=font,fill='#FFFFFF')
- draw.text((0,112), '8:アーティスト選択', font=font,fill='#FFFFFF')
-
- draw.text((0,128), '9:DBアップデート', font=font,fill='#FFFFFF')
- draw.text((0,144), '0:システムメニュー', font=font,fill='#FFFFFF')
-
- item = LCDItem(image, pos=LCDItem.FULL)
- self.playermain.lcd.displayItem(item,lock=True)
- self.active = True
- return True
-
- # アクティブかつリモコンが押された
- if self.active:
- c = message.getParam()
- if c in '0123456789':
- if c == '1': # random
- arg = 'on'
- if self.status['random'] == '1':
- arg = 'off'
- self.sendMessage(Message(self, Message.MPC, {'command':'random', 'arg': arg} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '2': # repeat
- arg = 'on'
- if self.status['repeat'] == '1':
- arg = 'off'
- self.sendMessage(Message(self, Message.MPC, {'command':'repeat', 'arg': arg} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '3': # delete
- self.sendMessage(Message(self, Message.MPC, {'command':'delete', 'arg': None} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '4': # save
- for i in range(0,100):
- if not (('playlist'+str(i)) in self.playlists):
- break
- self.sendMessage(Message(self, Message.MPC, {'command':'save', 'arg': 'playlist'+str(i)}))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '5': # play all
- self.sendMessage(Message(self, Message.MPC, {'command':'playall', 'arg': None} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '6': # playlist
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
- self.sendMessage(Message(self, Message.PLAYLIST, 'playlists' ))
- self.active = False
-
- elif c == '7': # album
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
- self.sendMessage(Message(self, Message.PLAYLIST, 'album' ))
- self.active = False
-
- elif c == '8': # artist
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
- self.sendMessage(Message(self, Message.PLAYLIST, 'artist' ))
- self.active = False
-
- elif c == '9': # db update
- self.sendMessage(Message(self, Message.MPC, {'command':'update', 'arg': None} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '0': # sysmenu
- self.sendMessage(Message(self, Message.SYSMENU, None))
- self.active = False
-
- if self.active == False:
- self.playermain.lcd.release()
- return False
- return True
- return True
- # Volume Control Worker
- class VolumeControl(Worker):
-
- MAX_VALUE = 81 # 81% == 0dB
- enabled = True
- term = False
- value = 100
-
- mute = False
-
- def __init__(self, pm):
- super().__init__(pm)
-
- # read initial value
- if os.path.exists(VOLUME_FILE):
- with open(VOLUME_FILE, "r") as f:
- try:
- self.value = int(f.read())
- except Exception as e:
- pass
-
- try:
- self.mixer = Mixer('Digital')
- except Exception as e:
- print(e, file=self.playermain.out)
- self.enabled = False
-
- if self.enabled:
- self.mixer.setvolume(int((self.MAX_VALUE *self.value)/100))
- self.mixer.setmute(self.mute)
-
- def receiveMessage(self,message):
- if message.getMessage() == Message.RCONTROL:
- # enabled ?
- if not self.enabled:
- return True
- if message.getParam() == 'volup':
- self.value = self.value + 2
- if self.value > 100:
- self.value = 100
- elif message.getParam() == 'voldown':
- self.value = self.value - 2
- if self.value < 0:
- self.value = 0
- elif message.getParam() == 'mute':
- self.mute = not(self.mute)
- self.mixer.setmute(self.mute)
- self.sendMessage(Message(self, Message.VOLUME, {'type':'mute', 'value': self.mute}))
- if message.getParam() == 'volup' or message.getParam() == 'voldown':
- self.mixer.setvolume(int((self.MAX_VALUE*self.value)/100))
- self.sendMessage(Message(self, Message.VOLUME, {'type':'volume', 'value': self.value}))
- # Terminate
- elif message.getMessage() == Message.TERM:
- try:
- with open(VOLUME_FILE, "w") as f:
- f.write(str(self.value))
- except Exception as e:
- pass
-
- return True
- # LCD Worker
- class LCD(Worker, threading.Thread):
- term = False
-
- HEIGHT = LCD_HEIGHT
- WIDTH = LCD_WIDTH
-
- BOTTOM_HEIGHT = 12
- INDICATE_HEIGHT = 10
- TOP_HEIGHT = 138
-
-
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self,pm)
-
- self.spi = SPI.SpiDev(SPI_PORT, SPI_DEVICE,max_speed_hz=SPEED_HZ)
- self.disp = TFT.ST7735(DC, rst=RST, spi=self.spi)
- self.disp.begin()
- self.disp.clear((0,0,0));
- if os.path.exists(LOGO_FILE):
- image = Image.open(LOGO_FILE)
- self.disp.buffer.paste(image)
- self.disp.display()
- self.messageq = castQueue(5)
- self.semaphore = BoundedSemaphore()
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.DISPLAY:
- self.messageq.put(message.getParam())
- return False
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def release(self):
- try:
- self.semaphore.release()
- except Exception as e:
- print(e, file=self.playermain.out)
-
- def displayItem(self, item, lock=False):
- self.semaphore.acquire()
- try:
- # 画面全体に貼り付け
- if item.getPos() == LCDItem.FULL:
- if item.getClear():
- self.disp.clear((0,0,0))
- if item.getImage() is not None:
- self.disp.buffer.paste(item.getImage())
- self.disp.display()
- else:
- left = 0
- upper = 0
- item_width = item.getWidth()
- item_height = item.getHeight()
- image = item.getImage()
- # サイズ制限
- if item.getWidth() != self.WIDTH:
- scale = float(float(self.WIDTH)/float(item.getWidth()))
- image = item.getImage().resize((int(item_width * scale), int(item_height*scale)), Image.BILINEAR)
-
- # Display TOP
- if item.getPos() == LCDItem.TOP:
- topimg = Image.new('RGB', (LCD.WIDTH, LCD.TOP_HEIGHT), 0)
- topimg.paste(image)
- self.disp.display(topimg, x0=left, y0=upper, x1=self.WIDTH-1, y1=topimg.size[1]-1 )
-
- # Display Bottom
- elif item.getPos() == LCDItem.BOTTOM:
- if image.size[1] > self.BOTTOM_HEIGHT:
- image = image.crop( box=(0, 0, self.WIDTH, self.BOTTOM_HEIGHT) )
- self.disp.display(image, x0=left, y0 = self.HEIGHT-self.BOTTOM_HEIGHT-1, x1=self.WIDTH-1, y1=self.HEIGHT-1 )
-
- # Display Indicator
- elif item.getPos() == LCDItem.INDICATE:
- if image.size[1] > self.INDICATE_HEIGHT:
- image = image.crop( box=(0, 0, self.WIDTH, self.INDICATE_HEIGHT) )
- self.disp.display(image, x0=left, y0 = self.HEIGHT-self.BOTTOM_HEIGHT-self.INDICATE_HEIGHT-1, x1=self.WIDTH-1, y1=self.HEIGHT-self.BOTTOM_HEIGHT-1 )
-
- if item.getTime() != 0:
- sleep(item.getTime())
- except Exception as e: # 何かエラーが起きたら無条件で開放
- print(e, file=self.playermain.out)
- self.release()
- return
-
- if lock == False:
- self.release()
- return
-
- def run(self):
- while self.term == False:
- try:
- # LCDItem
- item = self.messageq.get_nowait()
- self.displayItem(item)
- except queue.Empty:
- sleep(0.05)
-
- return True
- # MPD state watch worker
- class MPD(Worker, threading.Thread):
-
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self,pm)
-
- try:
- self.mpc = MPDClient(use_unicode=True)
- self.mpc.connect( MPD_HOST, MPD_PORT );
- except SocketError as e:
- print(e, file=self.playermain.out);
- return
-
- self.config = self.mpc.config()
- current = self.mpc.currentsong()
- status = self.mpc.status()
- self.param = {'changes':None, 'current': current, 'status': status, 'config': self.config }
-
- def receiveMessage(self,message):
- if message.getMessage() == Message.TERM:
- self.term = True
- elif message.getMessage() == Message.MENUOFF:
- self.sendMessage()
-
- return True
-
- def sendMessage(self):
- super().sendMessage(Message(self, Message.MPD, self.param))
- return
-
- def run(self):
- # 現状レポート
- current = self.mpc.currentsong()
- status = self.mpc.status()
- self.param = {'changes':None, 'current': current, 'status': status, 'config': self.config }
- self.sendMessage()
-
- self.mpc.send_idle()
- while self.term == False:
- canRead = select([self.mpc], [], [], 0)[0]
- if canRead:
- changes = self.mpc.fetch_idle()
- current = self.mpc.currentsong()
- status = self.mpc.status()
- self.param = {'changes': changes, 'current': current, 'status': status, 'config': self.config }
- self.sendMessage()
- self.mpc.send_idle()
- sleep(0.1)
- # idle中にCloseすると怒られる
- # self.mpc.close()
- return
- # Album art worker
- class AlbumArt(Worker, threading.Thread):
- TEMP_DIR = '/var/tmp/.superdac.aa'
- EXTRACTCA = '/usr/local/bin/extractCoverArt'
-
- term = False
- refresh = False
- prev_path = 'foobar'
- term = False
- use_folderjpg = True
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self,pm)
- self.messageq = queue.Queue()
-
- if not os.path.exists(self.TEMP_DIR):
- os.mkdir(self.TEMP_DIR)
-
- if os.path.exists(self.EXTRACTCA):
- self.use_folderjpg = False
-
- def extractCoverArt(self, config, current):
- if 'file' in current:
- song_file = config + '/' + current['file']
- cache_file = self.TEMP_DIR + '/' + hashlib.sha256(song_file.encode()).hexdigest() + '.jpg'
- if os.path.exists(cache_file):
- return cache_file
-
- if urlparse(song_file).scheme == 'smb':
- song_path = config + '/' + os.path.dirname(current['file'])
- song_ext = os.path.splitext(current['file'])[1]
- remote_song = os.path.basename(current['file'])
- smb = simpleSMB(song_path)
- if smb.isEnable():
- if smb.exists(remote_song):
- if smb.copyTo(remote_song, self.TEMP_DIR+'/t'+ song_ext) == False:
- return None
- song_file = self.TEMP_DIR + '/t'+ song_ext
- else:
- del smb
- return None
- else:
- del smb
- return None
- del smb
-
- if os.path.exists(song_file):
- r = subprocess.run([self.EXTRACTCA, song_file, cache_file])
- if r.returncode == 0:
- return cache_file
- else:
- return None
- else:
- return None
- return None
-
- def getFolderjpg(self, config, current):
- if 'file' in current:
- song_path = config + '/' + os.path.dirname(current['file'])
- if urlparse(song_path).scheme == 'smb':
- smb = simpleSMB(song_path)
- if smb.isEnable():
- aafile = None
- if smb.exists('Folder.jpg'):
- aafile = 'Folder.jpg'
- elif smb.exists('folder.jpg'):
- aafile = 'folder.jpg'
-
- elif smb.exists('AlbumArt.jpg'):
- aafile = 'AlbumArt.jpg'
- elif smb.exists('AlbumArtSmall.jpg'):
- aafile = 'AlbumArtSmall.jpg'
-
- if aafile is None:
- return None
-
- if smb.copyTo(aafile, self.TEMP_DIR+'/aa.jpg') == False:
- del smb
- return None
- else:
- del smb
- return (self.TEMP_DIR+'/aa.jpg')
- else:
- return None
- else:
- imgfile = None
- if os.path.exists( song_path+'/Folder.jpg'):
- imgfile = song_path + '/Folder.jpg'
- elif os.path.exists(song_path + '/folder.jpg'):
- imgfile = song_path + '/folder.jpg'
- elif os.path.exists(song_path + '/AlbumArt.jpg'):
- imgfile = song_path + '/AlbumArt.jpg'
- elif os.path.exists(song_path + '/AlbumArtSmall.jpg'):
- imgfile = song_path + '/AlbumArtSmall.jpg'
- return imgfile
- return None
- return None
-
- def draw(self, param):
- status = param['status']
- current = param['current']
- config = param['config']
-
- imgfile = None
- if self.use_folderjpg:
- if 'file' in current:
- path = config + '/' + os.path.dirname(current['file'])
- # Album change
- if path != self.prev_path:
- self.prev_path = path
- imgfile = self.getFolderjpg(config, current)
- else:
- imgfile = self.extractCoverArt(config, current)
-
- if imgfile is None:
- if os.path.exists( LOGO_FILE ):
- imgfile = LOGO_FILE
-
- # Display AlbumArt
- if imgfile is not None:
- image = Image.open(imgfile)
- param = LCDItem(image, pos=LCDItem.TOP)
- msg = Message(self, Message.DISPLAY, param)
- self.sendMessage(msg)
-
- return
-
- def receiveMessage(self,message):
- if message.getMessage() == Message.MENUOFF:
- self.prev_path = '' # メニューが閉じたらアルバムアートをクリア
- elif message.getMessage() == Message.MPD:
- self.messageq.put(message.getParam())
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def run(self):
- while self.term == False:
- try:
- param = self.messageq.get_nowait()
- self.draw(param)
- except queue.Empty:
- sleep(0.05)
- return
- # SongTitle
- class SongTitle(Worker, threading.Thread):
-
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
- self.messageq = queue.Queue()
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.MPD:
- self.messageq.put(message.getParam())
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def draw(self, title):
- jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.BOTTOM_HEIGHT, encoding="unic");
- image = Image.new('RGB', (LCD.WIDTH, LCD.BOTTOM_HEIGHT), 0)
- draw = ImageDraw.Draw(image)
- draw.text((0,0) , title, font=jpfont, fill='#FFFFFF')
- param = LCDItem(image, pos=LCDItem.BOTTOM)
- msg = Message(self, Message.DISPLAY, param)
- self.sendMessage(msg)
-
- def run(self):
- while self.term == False:
- try:
- param = self.messageq.get_nowait()
- if 'title' in param['current']:
- self.draw(param['current']['title'])
- except queue.Empty:
- sleep(0.05)
- return
-
- # Indicator worker
- class Indicator(Worker, threading.Thread):
-
- status = {'state': 'stop'}
- term = False
- mute = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
-
- self.messageq = queue.Queue()
- self.timer = SDTimer(5, self.refresh)
-
- def refresh(self):
- jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.INDICATE_HEIGHT, encoding="unic");
- image = Image.new('RGB', (LCD.WIDTH, LCD.INDICATE_HEIGHT), 0)
- draw = ImageDraw.Draw(image)
- str = ''
- if self.mute:
- str = 'Mute....'
- else:
- if self.status['state'] == 'stop':
- str = 'Stop'
- elif self.status['state'] == 'pause':
- str = 'Pause'
- else:
- if self.status['random'] == '1':
- str = str + 'Random'
- else:
- str = str + 'Normal'
- if self.status['repeat'] == '1':
- str = str + '/Repeat'
- str = str + '/'+ self.status['bitrate'] + 'kbps'
-
- draw.text((0,0) , str, font=jpfont, fill='#00BFFF')
- param = LCDItem(image, pos=LCDItem.INDICATE)
- msg = Message(self, Message.DISPLAY, param)
- self.sendMessage(msg)
- return
-
- def volumeBar(self, value):
- jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.INDICATE_HEIGHT, encoding="unic");
- image = Image.new('RGB', (LCD.WIDTH, LCD.INDICATE_HEIGHT), 0)
- draw = ImageDraw.Draw(image)
- draw.text((0,0) ,'Vol:', font=jpfont, fill='#7CFC00')
- bar_length = int(((LCD.WIDTH - 30) * value) / 100)
- draw.rectangle((30, 0, bar_length+30, LCD.INDICATE_HEIGHT), fill=(124, 252, 0))
- msgparam = LCDItem(image, pos=LCDItem.INDICATE)
- self.sendMessage(Message(self, Message.DISPLAY, msgparam))
- return
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.MENUOFF:
- pass
- # self.messageq.put(Message(None, Message.INDICATOR, None))
- elif message.getMessage() == Message.INDICATOR:
- self.messageq.put(message)
- elif message.getMessage() == Message.MPD:
- self.status = message.getParam()['status']
- self.messageq.put(Message(None, Message.INDICATOR, None))
- elif message.getMessage() == Message.VOLUME:
- self.messageq.put(message)
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def run(self):
- while self.term == False:
- try:
- message = self.messageq.get_nowait()
- if message.getMessage() == Message.VOLUME:
- param = message.getParam()
- if param['type'] == 'volume':
- self.volumeBar(int(param['value']))
- if self.timer.getActive():
- self.timer.setRemaining(5)
- else:
- self.timer = SDTimer(5, self.refresh)
- self.timer.start()
- elif param['type'] == 'mute':
- self.mute = param['value']
- self.refresh()
- elif message.getMessage() == Message.INDICATOR:
- self.refresh()
- except queue.Empty:
- sleep(0.05)
- if self.timer.getActive():
- self.timer.cancel()
- return
- # MPD Client worker
- class MPC(Worker, threading.Thread):
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
-
- self.messageq = queue.Queue()
- self.semaphore = BoundedSemaphore()
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.RCONTROL:
- param = {'command': message.getParam(), 'arg': None }
- self.messageq.put(param)
- elif message.getMessage() == Message.MPC:
- self.messageq.put(message.getParam())
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def execute(self, param):
- self.semaphore.acquire()
- try:
- mpc = MPDClient(use_unicode=True)
- mpc.connect(MPD_HOST, MPD_PORT)
- except Exception as e:
- print(e, file=self.playermain.out)
- self.semaphore.release()
- return
-
- try:
- if param['command'] == 'playall':
- mpc.stop()
- mpc.clear()
- songs = mpc.list('file')
- for song in songs:
- mpc.add(song)
- mpc.play()
-
- elif param['command'] == 'random':
- if param['arg'] == 'on':
- mpc.random(1)
- else:
- mpc.random(0)
-
- elif param['command'] == 'repeat':
- if param['arg'] == 'on':
- mpc.repeat(1)
- else:
- mpc.repeat(0)
-
- elif param['command'] == 'update':
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- draw.text((0,64), 'DB更新中....', font=font, fill='#FF0000')
- draw.text((0,80), 'お待ち下さい', font=font, fill='#FF0000')
- item = LCDItem(image)
- self.playermain.lcd.displayItem(item, lock=True)
- jobno = mpc.update()
- while 'updating_db' in mpc.status():
- if mpc.status()['updating_db'] != jobno:
- break
- sleep(0.1)
- self.playermain.lcd.release()
-
- elif param['command'] == 'load':
- mpc.stop()
- mpc.clear()
- mpc.load(param['arg'])
- mpc.play()
-
- elif param['command'] == 'list':
- list = []
- if param['arg'] == 'playlists':
- playlists = mpc.listplaylists()
- for p in playlists:
- list.append(p['playlist'])
- elif param['arg'] == 'artist':
- list = mpc.list('artist')
- elif param['arg'] == 'album':
- list = mpc.list('album')
- self.sendMessage(Message(self, Message.MPC_REPLY, { 'type': param['arg'], 'list': list }))
-
- elif param['command'] == 'findadd':
- mpc.stop()
- mpc.clear()
- mpc.findadd(param['arg']['type'], param['arg']['value'])
- mpc.play()
-
- elif param['command'] == 'delete':
- status = mpc.status()
- if 'songid' in status:
- mpc.deleteid(status['songid'])
-
- elif param['command'] == 'save':
- mpc.save(param['arg'])
-
- elif param['command'] == 'play':
- mpc.play()
-
- elif param['command'] == 'stop':
- mpc.stop()
-
- elif param['command'] == 'pause':
- mpc.pause()
-
- elif param['command'] == 'clear':
- mpc.clear()
-
- elif param['command'] == 'next':
- mpc.next()
-
- elif param['command'] == 'prev':
- mpc.previous()
- #
- except Exception as e:
- print(e, file=self.playermain.out)
-
- mpc.close()
- self.semaphore.release()
- return
-
- def run(self):
- while self.term == False:
- try:
- param = self.messageq.get_nowait()
- self.execute(param)
- except queue.Empty:
- sleep(0.05)
-
- return
-
- # lirc remote control worker
- class Remote(Worker, threading.Thread):
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
-
- lircrc = None
- for f in LIRCRC:
- if os.path.exists(f):
- lircrc = f
- break
- if lircrc is None:
- raise Exception('lircrc not found')
-
- self.sockid = lirc.init("superdac", lircrc, blocking=False)
- self.term = False
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def run(self):
- while self.term == False:
- code = lirc.nextcode()
- if len(code) == 0:
- sleep(0.1)
- continue
- message = Message(self, Message.RCONTROL, code[0])
- self.sendMessage(message)
-
- lirc.deinit()
- return
- # デーモンの終了フラグ
- Terminate = False
- noDaemon = False
- class PlayerMain(Master, threading.Thread):
- global Terminate
- global noDaemon
-
- workers = [] # type: Worker[]
- out = None
-
- def __init__(self, err):
- super().__init__()
- self.out = err
- self.messageq = queue.Queue()
-
- # Message Queuing
- def postMessage(self, message):
- if noDaemon:
- print(message.getMessage() , file=self.out)
- print(message.getParam() , file=self.out)
- self.messageq.put(message)
- return
-
- def run(self):
- #
- # MPD status watcher
- mi = None
- try:
- mi = MPD(self)
- except Exception as e:
- # MPDが動いていない
- print(e, file=self.out)
- return
- mi.setDaemon(True)
- mi.start()
- self.workers.append(mi)
-
- # LCD
- self.lcd = LCD(self)
- self.lcd.setDaemon(True)
- self.lcd.start()
- self.workers.append(self.lcd)
-
- # AlbumArt
- aa = AlbumArt(self)
- aa.setDaemon(True)
- aa.start()
- self.workers.append(aa)
-
- # リモコン
- try:
- remocon = Remote(self)
- remocon.setDaemon(True)
- remocon.start()
- self.workers.append(remocon)
- except Exception as e:
- print(e, file=self.out)
-
- # VolumeControl
- vo = VolumeControl(self)
- self.workers.append(vo)
-
- # MPC
- self.mpc = MPC(self)
- self.mpc.setDaemon(True)
- self.mpc.start()
- self.workers.append(self.mpc)
-
- # SongTitle
- st = SongTitle(self)
- st.setDaemon(True)
- st.start()
- self.workers.append(st)
-
- # Indicator
- id = Indicator(self)
- id.setDaemon(True)
- id.start()
- self.workers.append(id)
-
- # Switches
- sw = Switches(self)
- self.workers.append(sw)
-
- # SystemMenu
- sm = SysMenu(self)
- self.workers.append(sm)
-
- # Playlist
- ps = Playlist(self)
- self.workers.append(ps)
-
- # MainMenu
- mm = MainMenu(self)
- self.workers.append(mm)
-
-
- #
- # イベントループ
- #
- while Terminate == False:
- try:
- msg = self.messageq.get_nowait()
- # イベント送出
- for w in self.workers:
- if type(msg.getSender()) != type(w):
- if w.receiveMessage(msg) == False:
- break
-
- except queue.Empty:
- sleep(0.05)
-
- # 終了
- for w in self.workers:
- w.receiveMessage(Message(self, Message.TERM, None))
-
- self.out.close()
- return
-
- def main_loop():
- global noDaemon
-
- logfile = sys.stdout
- if not noDaemon:
- try:
- logfile = open(LOGFILE, 'w')
- except Exception as e:
- logfile = sys.stdout
-
- p = PlayerMain(logfile)
- p.setDaemon(False)
- p.start()
- p.join(None)
-
- def signal_handler(signal, handler):
- global Terminate
- global noDaemon
-
- Terminate = True
- if not noDaemon:
- os.remove(PIDFILE)
- def daemonize():
- pid = os.fork()
- if pid > 0:
- pidf = open(PIDFILE, 'w')
- pidf.write(str(pid)+"\n")
- pidf.close()
- sys.exit()
-
- if pid == 0:
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- main_loop()
-
- # Script starts here
- if __name__ == "__main__":
- argv = sys.argv
- if len(argv) > 1:
- if argv[1] == '--nodaemon':
- noDaemon = True
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- main_loop()
- else:
- daemonize()
|