1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312 |
- #!/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
- # 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'
-
- 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
- # 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')
-
- # self.sendMessage(Message(self, Message.MENUON, None ))
- 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 ))
- 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 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 not os.path.exists(self.TEMP_DIR):
- os.mkdir(self.TEMP_DIR)
- 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 not os.path.exists(self.TEMP_DIR):
- os.mkdir(self.TEMP_DIR)
- 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)
-
- # 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()
|