#!/usr/bin/python3 import magic,os,pickle,re,subprocess # Which content folder does this kiosk read from folder = 'Test3' basedir = '/home/pi/kiosk' remotebase = 'Kiosks:Kiosks' remote = os.path.join(remotebase, folder) contentbase = os.path.join(basedir, 'content') slidebase = os.path.join(basedir, 'slides') tmpdir = os.path.join(basedir, 'tmp') statusfile = os.path.join(basedir, 'var', 'statusfile') loopfile = os.path.join(basedir, 'var', 'loop') waitfile = os.path.join(basedir, 'var', 'wait') confcsv = os.path.join(basedir, 'var', 'conf.csv') rclonebase = ['/usr/bin/rclone', 'sync'] synccommand = rclonebase + [remote, contentbase] # Default parameter values; each value is given as a triple: # [default, minimum, maximum] defaults = { 'slide_rate' : [ 5, 2, 60], 'check_time' : [ 90, 1, 3600], } # File formats that "feh" can display directly with no processing directformats = [ 'JPEG image', 'PNG image', 'GIF image', 'Netpbm image', ] # Video file formats that "omxplayer" can display videoformats = [ 'ISO Media', 'AVI', 'DivX', ] # Template for loop script looptemplate = '''#!/bin/bash feh --cycle-once -F -Z -D %d ''' # Template for wait script waittemplate = '''#!/bin/bash sleep %d ''' # Keep track of history to avoid repeating work filestatus = {} # List of videos to play will get built here videolist = [] # Actual parameter values will get filled in here parameters = {} # Set a parameter from the config file and the defaults def setparam(parname): # Set up parameters v = -1 try: v = int(parameters[parname]) except: pass if (v < defaults[parname][1]) or (v > defaults[parname][2]): v = defaults[parname][0] return(v) # Empty the temporary working directory def cleantmp(): try: tmpfiles = os.listdir(tmpdir) for f in tmpfiles: fpath = os.path.join(tmpdir, f) os.unlink(fpath) except: pass # Copy a file into the temporary directory def tmpcopy(filename): srcpath = os.path.join(contentbase, filename) dstpath = os.path.join(tmpdir, filename) subprocess.call(['/bin/cp', '-p', '-f', srcpath, dstpath]) # Move PNG files from temporary to slide directory # Returns a list of moved files, and cleans the temporary directory def tmpmove(moveall): tmpfiles = os.listdir(tmpdir) tmpfiles.sort() filelist = [] for f in tmpfiles: srcpath = os.path.join(tmpdir, f) if moveall or (f[-4:] == '.png'): dstpath = os.path.join(slidebase, f) subprocess.call(['/bin/mv', '-f', srcpath, dstpath]) filelist += [f] else: os.unlink(srcpath) return(filelist) # Write out a new version of an executable script file def writescript(scriptpath, content): tmppath = scriptpath + '.new' fdout = open(tmppath, 'w') fdout.write(content) fdout.close() subprocess.call(['/bin/chmod', 'u+x', tmppath]) subprocess.call(['/bin/mv', '-f', tmppath, scriptpath]) def processfile(f): global videolist print('processfile start', f) filepath = os.path.join(contentbase, f) sb = os.stat(filepath) filetype = ms.file(filepath) # Active file, save status record keepcontent[f] = True if f in filestatus: # Status record exists # Check size and modification time thisstatus = filestatus[f] if (thisstatus['s'] == sb.st_size) and (thisstatus['m'] == sb.st_mtime) and (thisstatus['t'] == filetype): # exact match, do not need to process this file print('exact', f) # See if it is video or slides if thisstatus['v']: # Mark this video to get played videolist += [f] else: # mark resulting slide files to save them for i in thisstatus['f']: keepslides[i] = True return else: # something has changed print('changed', f) filestatus[f]['s'] = sb.st_size filestatus[f]['m'] = sb.st_mtime filestatus[f]['t'] = filetype filestatus[f]['v'] = False else: # new file, no status record yet, so make one print('new', f) filestatus[f] = {} filestatus[f]['s'] = sb.st_size filestatus[f]['m'] = sb.st_mtime filestatus[f]['t'] = filetype filestatus[f]['v'] = False # Figure out some useful paths destpath = os.path.join(slidebase, f) # Check for images that can be displayed directly as slides direct = False for ff in directformats: if ff in filetype: direct = True # Check for files that can be played as video clips video = False for ff in videoformats: if ff in filetype: video = True if direct: # files that can be displayed directly just need to be copied into place print('direct', filepath) tmpcopy(f) subprocess.call(['/bin/cp', '-p', '-f', filepath, destpath]) keepslides[f] = True elif 'PDF document' in filetype: tmpcopy(f) subprocess.call(['/home/pi/kiosk/bin/file-pdf']) elif 'PostScript document' in filetype: tmpcopy(f) subprocess.call(['/home/pi/kiosk/bin/file-ps']) elif 'PowerPoint' in filetype or 'Microsoft Word' in filetype or 'Zip archive data' in filetype: tmpcopy(f) subprocess.call(['/home/pi/kiosk/bin/file-office']) elif 'ASCII text' in filetype: tmpcopy(f) subprocess.call(['/home/pi/kiosk/bin/file-text']) elif video: videolist += [f] else: print('unknown', filepath) print(filetype) print(f, filetype, direct, video) filelist = tmpmove(direct) filestatus[f]['f'] = filelist for i in filelist: keepslides[i] = True filestatus[f]['v'] = video print('processfile done', f) # end of functions, main program starts here # Start by synchronizing with cloud storage to get content subprocess.call(synccommand) # Set up to read magic numbers and guess file types ms = magic.open(magic.NONE) ms.load() # Empty temporary directory cleantmp() # Read previous file status, if it exists try: stf = open(statusfile, 'rb') filestatus = pickle.load(stf) stf.close() except: pass # Slide files to preserve keepcontent = {} keepslides = {} # Turn the configuration file from a Google Sheet to a CSV file subprocess.call(['/home/pi/kiosk/bin/process-conf']) # Run through the configuration file and record parameters confdata = open(confcsv).readlines() for confline in confdata[1:]: fields = confline.split(',') if len(fields) > 1: parameter = fields[0].strip() value = fields[1].strip() parameters[parameter] = value slide_rate = setparam('slide_rate') check_time = setparam('check_time') print('slide_rate', slide_rate) print('check_time', check_time) # Examine and process all content files contentfiles = os.listdir(contentbase) contentfiles.sort() for f in contentfiles: # Do not process config file as content if f != 'conf.xlsx': processfile(f) # Prepare the looping script loopscript = looptemplate % slide_rate # Videos get run one at a time for video in videolist: loopscript += "omxplayer -b '../content/%s'\n" % video # Write out the completed looping script writescript(loopfile, loopscript) # Prepare and write out the waiting script waitscript = waittemplate % check_time writescript(waitfile, waitscript) print('keepslides', keepslides) # final cleanup, delete obsolete slides # Get list of slide files files = os.listdir(slidebase) files.sort() for f in files: if f not in keepslides: try: os.unlink(os.path.join(slidebase, f)) print('unlink slide', f) except: pass # Delete status records for obsolete content deletekeys = [] for s in filestatus.keys(): if s not in keepcontent: deletekeys += [s] for s in deletekeys: del filestatus[s] print('delete status', s) # Write new status file stf = open(statusfile, 'wb') pickle.dump(filestatus, stf) stf.close() # If requested, upload rendered slides for verification try: updst = remote + parmeters['verify_folder'] doupload = rclonebase + [slidebase, updst] subprocess.call(doupload) except: pass