Got rid of all the stuff related to GUNID hex-to-int conversion. Commented out lots of functions that are either not in use or will no longer work. Pypo: made things more generic and pluggable, added documentation. Added the PHP scripts to serve the right info back to pypo.
1026 lines
40 KiB
Python
Executable File
1026 lines
40 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# author Jonas Ohrstrom <jonas@digris.ch>
|
|
|
|
"""
|
|
Python part of radio playout (pypo)
|
|
|
|
The main functions are "fetch" (./pypo_cli.py -f) and "push" (./pypo_cli.py -p)
|
|
|
|
There are two layers: scheduler & daypart (fallback)
|
|
|
|
The daypart is a fallback-layer generated by the playlists daypart-settings
|
|
(eg a playlist creator can say that the list is good for Monday and Tues,
|
|
between 14:00 and 16:00). So if there is nothing in the schedule, pypo will
|
|
still play something (instead of silence..) This layer is optional.
|
|
It is there so that you dont have a fallback player which plays the same 100
|
|
tracks over and over again.
|
|
|
|
Attention & ToDos
|
|
- liquidsoap does not like mono files! So we have to make sure that only files with
|
|
2 channels are fed to LS
|
|
(solved: current = audio_to_stereo(current) - maybe not with ultimate performance)
|
|
|
|
|
|
made for python version 2.5!!
|
|
should work with 2.6 as well with a bit of adaption. for
|
|
sure the json parsing has to be changed
|
|
(2.6 has an parser, pypo brings it's own -> util/json.py)
|
|
"""
|
|
|
|
# python defaults (debian default)
|
|
import time
|
|
import os
|
|
import traceback
|
|
from optparse import *
|
|
import sys
|
|
import time
|
|
import datetime
|
|
import logging
|
|
import logging.config
|
|
import shutil
|
|
import urllib
|
|
import urllib2
|
|
import pickle
|
|
import telnetlib
|
|
import random
|
|
import string
|
|
import operator
|
|
import inspect
|
|
|
|
# additional modules (should be checked)
|
|
from configobj import ConfigObj
|
|
|
|
# custom imports
|
|
from util import *
|
|
from api_clients import *
|
|
|
|
PYPO_VERSION = '0.2'
|
|
|
|
# Set up command-line options
|
|
parser = OptionParser()
|
|
|
|
# help screeen / info
|
|
usage = "%prog [options]" + " - python playout system"
|
|
parser = OptionParser(usage=usage)
|
|
|
|
# Options
|
|
parser.add_option("-v", "--compat", help="Check compatibility with server API version", default=False, action="store_true", dest="check_compat")
|
|
|
|
parser.add_option("-f", "--fetch-scheduler", help="Fetch from scheduler - scheduler (loop, interval in config file)", default=False, action="store_true", dest="fetch_scheduler")
|
|
parser.add_option("-p", "--push-scheduler", help="Push scheduler to Liquidsoap (loop, interval in config file)", default=False, action="store_true", dest="push_scheduler")
|
|
|
|
parser.add_option("-F", "--fetch-daypart", help="Fetch from daypart - scheduler (loop, interval in config file)", default=False, action="store_true", dest="fetch_daypart")
|
|
parser.add_option("-P", "--push-daypart", help="Push daypart to Liquidsoap (loop, interval in config file)", default=False, action="store_true", dest="push_daypart")
|
|
|
|
parser.add_option("-b", "--cleanup", help="Cleanup", default=False, action="store_true", dest="cleanup")
|
|
parser.add_option("-j", "--jingles", help="Get new jingles from obp, comma separated list if jingle-id's as argument", metavar="LIST")
|
|
parser.add_option("-c", "--check", help="Check the cached schedule and exit", default=False, action="store_true", dest="check")
|
|
|
|
# parse options
|
|
(options, args) = parser.parse_args()
|
|
|
|
# configure logging
|
|
logging.config.fileConfig("logging.cfg")
|
|
|
|
# loading config file
|
|
try:
|
|
config = ConfigObj('config.cfg')
|
|
CACHE_DIR = config['cache_dir']
|
|
FILE_DIR = config['file_dir']
|
|
TMP_DIR = config['tmp_dir']
|
|
API_BASE = config['api_base']
|
|
API_KEY = config['api_key']
|
|
POLL_INTERVAL = float(config['poll_interval'])
|
|
PUSH_INTERVAL = float(config['push_interval'])
|
|
LS_HOST = config['ls_host']
|
|
LS_PORT = config['ls_port']
|
|
#PREPARE_AHEAD = config['prepare_ahead']
|
|
CACHE_FOR = config['cache_for']
|
|
CUE_STYLE = config['cue_style']
|
|
#print config
|
|
except Exception, e:
|
|
print 'Error loading config file: ', e
|
|
sys.exit()
|
|
|
|
#TIME = time.localtime(time.time())
|
|
TIME = (2010, 6, 26, 15, 33, 23, 2, 322, 0)
|
|
|
|
# to help with debugging - get the current line number
|
|
def lineno():
|
|
"""Returns the current function name and line number in our program."""
|
|
return "File " +inspect.currentframe().f_code.co_filename + " / Line " + str(inspect.currentframe().f_back.f_lineno) + ": "
|
|
|
|
class Global:
|
|
def __init__(self):
|
|
#print '# global initialisation'
|
|
print
|
|
|
|
def selfcheck(self):
|
|
self.api_auth = urllib.urlencode({'api_key': API_KEY})
|
|
self.api_client = api_client.api_client_factory(config)
|
|
self.api_client.check_version()
|
|
|
|
"""
|
|
Uncomment the following lines to let pypo check if
|
|
liquidsoap is running. (checks for a telnet server)
|
|
"""
|
|
# while self.status.check_ls(LS_HOST, LS_PORT) == 0:
|
|
# print 'Unable to connect to liquidsoap. Is it up and running?'
|
|
# time.sleep(2)
|
|
|
|
|
|
"""
|
|
|
|
"""
|
|
class Playout:
|
|
def __init__(self):
|
|
#print '# init fallback'
|
|
|
|
self.file_dir = FILE_DIR
|
|
self.tmp_dir = TMP_DIR
|
|
|
|
self.api_auth = urllib.urlencode({'api_key': API_KEY})
|
|
self.api_client = api_client.api_client_factory(config)
|
|
self.cue_file = CueFile()
|
|
|
|
# set initial state
|
|
self.range_updated = False
|
|
|
|
"""
|
|
Fetching part of pypo
|
|
- Reads the scheduled entries of a given range (actual time +/- "prepare_ahead" / "cache_for")
|
|
- Saves a serialized file of the schedule
|
|
- playlists are prepared. (brought to liquidsoap format) and, if not mounted via nsf, files are copied
|
|
to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss)
|
|
- runs the cleanup routine, to get rid of unused cashed files
|
|
"""
|
|
def fetch(self, export_source):
|
|
"""
|
|
wrapper script for fetching the whole schedule (in json)
|
|
"""
|
|
logger = logging.getLogger("fetch")
|
|
|
|
self.export_source = export_source
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
try: os.mkdir(self.cache_dir)
|
|
except Exception, e: pass
|
|
|
|
|
|
"""
|
|
Trigger daypart range-generation. (Only if daypart-instance)
|
|
"""
|
|
if self.export_source == 'daypart':
|
|
|
|
print '******************************'
|
|
print '*** TRIGGER DAYPART UPDATE ***'
|
|
print '******************************'
|
|
|
|
try:
|
|
self.generate_range_dp()
|
|
except Exception, e:
|
|
logger.error(lineno() + "%s", e)
|
|
|
|
# get schedule
|
|
try:
|
|
while self.get_schedule() != 1:
|
|
logger.warning("failed to read from export url")
|
|
time.sleep(1)
|
|
|
|
except Exception, e: logger.error(lineno() +"%s", e)
|
|
|
|
# prepare the playlists
|
|
if CUE_STYLE == 'pre':
|
|
try: self.prepare_playlists_cue(self.export_source)
|
|
except Exception, e: logger.error(lineno() + "%s", e)
|
|
elif CUE_STYLE == 'otf':
|
|
try: self.prepare_playlists(self.export_source)
|
|
except Exception, e: logger.error(lineno() + "%s", e)
|
|
|
|
# cleanup
|
|
try: self.cleanup(self.export_source)
|
|
except Exception, e: logger.error(lineno() + "%s", e)
|
|
|
|
logger.info("fetch loop completed")
|
|
|
|
|
|
"""
|
|
This is actually a bit ugly (again feel free to improve!!)
|
|
The generate_range_dp function should be called once a day,
|
|
we do this at 18h. The hour before the state is set back to 'False'
|
|
"""
|
|
def generate_range_dp(self):
|
|
logger = logging.getLogger("generate_range_dp")
|
|
logger.debug("trying to trigger daypart update")
|
|
|
|
tnow = time.localtime(time.time())
|
|
|
|
if(tnow[3] == 16):
|
|
self.range_updated = False
|
|
|
|
if(tnow[3] == 17 and self.range_updated == False):
|
|
try:
|
|
print self.api_client.generate_range_dp()
|
|
logger.info("daypart updated")
|
|
self.range_updated = True
|
|
|
|
except Exception, e:
|
|
print e
|
|
|
|
|
|
def get_schedule(self):
|
|
logger = logging.getLogger("Playout.get_schedule")
|
|
status, response = self.api_client.get_schedule();
|
|
|
|
if status == 1:
|
|
logger.info("dump serialized schedule to %s", self.schedule_file)
|
|
schedule = response['playlists']
|
|
try:
|
|
schedule_file = open(self.schedule_file, "w")
|
|
pickle.dump(schedule, schedule_file)
|
|
schedule_file.close()
|
|
|
|
except Exception, e:
|
|
print lineno() + e
|
|
status = 0
|
|
|
|
return status
|
|
|
|
|
|
"""
|
|
Alternative version of playout preparation. Every playlist entry is
|
|
pre-cued if neccessary (cue_in/cue_out != 0) and stored in the
|
|
playlist folder.
|
|
file is eg 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3
|
|
"""
|
|
def prepare_playlists_cue(self, export_source):
|
|
logger = logging.getLogger("fetch.prepare_playlists")
|
|
|
|
self.export_source = export_source
|
|
|
|
try:
|
|
schedule_file = open(self.schedule_file, "r")
|
|
schedule = pickle.load(schedule_file)
|
|
schedule_file.close()
|
|
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
schedule = None
|
|
|
|
#for pkey in schedule:
|
|
try:
|
|
for pkey in sorted(schedule.iterkeys()):
|
|
logger.info("found playlist at %s", pkey)
|
|
#print pkey
|
|
playlist = schedule[pkey]
|
|
|
|
# create playlist directory
|
|
try: os.mkdir(self.cache_dir + str(pkey))
|
|
except Exception, e: pass
|
|
|
|
ls_playlist = '';
|
|
|
|
print '*****************************************'
|
|
print 'pkey: ' + str(pkey)
|
|
print 'cached at : ' + self.cache_dir + str(pkey)
|
|
print 'subtype: ' + str(playlist['subtype'])
|
|
print 'played: ' + str(playlist['played'])
|
|
print 'schedule id: ' + str(playlist['schedule_id'])
|
|
print 'duration: ' + str(playlist['duration'])
|
|
print 'source id: ' + str(playlist['x_ident'])
|
|
print '*****************************************'
|
|
|
|
# TODO: maybe a bit more modular..
|
|
silence_file = self.file_dir + 'basic/silence.mp3'
|
|
|
|
if int(playlist['played']) == 1:
|
|
logger.info("playlist %s already played / sent to liquidsoap, so will ignore it", pkey)
|
|
|
|
elif int(playlist['subtype']) == 5:
|
|
"""
|
|
This is a live session, so silence is scheduled
|
|
Maybe not the most elegant solution :)
|
|
It adds 20 time 30min silence to the playlist
|
|
Silence file has to be in <file_dir>/basic/silence.mp3
|
|
"""
|
|
logger.debug("found %s seconds of live/studio session at %s", pkey, playlist['duration'])
|
|
|
|
if os.path.isfile(silence_file):
|
|
logger.debug('file stored at: %s' + silence_file)
|
|
|
|
for i in range (0, 19):
|
|
ls_playlist += silence_file + "\n"
|
|
|
|
else:
|
|
print 'Could not find silence file!'
|
|
print 'File is expected to be at: ' + silence_file
|
|
logger.critical('File is expected to be at: %s', silence_file)
|
|
sys.exit()
|
|
|
|
elif int(playlist['subtype']) == 6:
|
|
"""
|
|
This is a live-cast session
|
|
Create a silence list. (could eg also be a fallback list..)
|
|
"""
|
|
logger.debug("found %s seconds of live-cast session at %s", pkey, playlist['duration'])
|
|
|
|
if os.path.isfile(silence_file):
|
|
logger.debug('file stored at: %s' + silence_file)
|
|
|
|
for i in range (0, 19):
|
|
ls_playlist += silence_file + "\n"
|
|
|
|
else:
|
|
print 'Could not find silence file!'
|
|
print 'File is expected to be at: ' + silence_file
|
|
logger.critical('File is expected to be at: %s', silence_file)
|
|
sys.exit()
|
|
|
|
|
|
elif int(playlist['subtype']) > 0 and int(playlist['subtype']) < 5:
|
|
|
|
for media in playlist['medias']:
|
|
logger.debug("found track at %s", media['uri'])
|
|
|
|
try:
|
|
src = media['uri']
|
|
|
|
if str(media['cue_in']) == '0' and str(media['cue_out']) == '0':
|
|
dst = "%s%s/%s.mp3" % (self.cache_dir, str(pkey), str(media['id']))
|
|
do_cue = False
|
|
else:
|
|
dst = "%s%s/%s_cue_%s-%s.mp3" % \
|
|
(self.cache_dir, str(pkey), str(media['id']), str(float(media['cue_in']) / 1000), str(float(media['cue_out']) / 1000))
|
|
do_cue = True
|
|
|
|
#print "dst_cue: " + dst
|
|
|
|
# check if it is a remote file, if yes download
|
|
if src[0:4] == 'http' and do_cue == False:
|
|
|
|
if os.path.isfile(dst):
|
|
logger.debug("file already in cache: %s", dst)
|
|
|
|
else:
|
|
logger.debug("try to download %s", src)
|
|
api_client.get_media(src, dst)
|
|
#try:
|
|
# print '** urllib auth with: ',
|
|
# print self.api_auth
|
|
# urllib.urlretrieve (src, dst, False, self.api_auth)
|
|
# logger.info("downloaded %s to %s", src, dst)
|
|
#except Exception, e:
|
|
# logger.error("%s", e)
|
|
|
|
elif src[0:4] == 'http' and do_cue == True:
|
|
if os.path.isfile(dst):
|
|
logger.debug("file already in cache: %s", dst)
|
|
print 'cached'
|
|
|
|
else:
|
|
logger.debug("try to download and cue %s", src)
|
|
|
|
print '***'
|
|
dst_tmp = self.tmp_dir + "".join([random.choice(string.letters) for i in xrange(10)]) + '.mp3'
|
|
print dst_tmp
|
|
print '***'
|
|
api_client.get_media(src, dst_tmp)
|
|
#try:
|
|
# print '** urllib auth with: ',
|
|
# print self.api_auth
|
|
# urllib.urlretrieve (src, dst_tmp, False, self.api_auth)
|
|
# logger.info("downloaded %s to %s", src, dst_tmp)
|
|
#except Exception, e:
|
|
# logger.error("%s", e)
|
|
#
|
|
|
|
# cue
|
|
print "STARTIONG CUE"
|
|
print self.cue_file.cue(dst_tmp, dst, float(media['cue_in']) / 1000, float(media['cue_out']) / 1000)
|
|
print "END CUE"
|
|
|
|
if True == os.access(dst, os.R_OK):
|
|
|
|
try: fsize = os.path.getsize(dst)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
fsize = 0
|
|
|
|
if fsize > 0:
|
|
logger.debug('try to remove temporary file: %s' + dst_tmp)
|
|
try: os.remove(dst_tmp)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
|
|
else:
|
|
logger.warning('something went wrong cueing: %s - using uncued file' + dst)
|
|
try: os.rename(dst_tmp, dst)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
|
|
|
|
else:
|
|
"""
|
|
Handle files on nas. Pre-cueing not implemented at the moment.
|
|
(not needed by openbroadcast, feel free to add this)
|
|
"""
|
|
# assume the file is local
|
|
# logger.info("local file assumed for : %s", src)
|
|
# dst = src
|
|
|
|
|
|
"""
|
|
Here an implementation for localy stored files.
|
|
Works the same as with remote files, just replaced API-download with
|
|
file copy.
|
|
"""
|
|
if do_cue == False:
|
|
|
|
if os.path.isfile(dst):
|
|
logger.debug("file already in cache: %s", dst)
|
|
|
|
else:
|
|
logger.debug("try to copy file to cache %s", src)
|
|
try:
|
|
shutil.copy(src, dst)
|
|
logger.info("copied %s to %s", src, dst)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
|
|
if do_cue == True:
|
|
|
|
if os.path.isfile(dst):
|
|
logger.debug("file already in cache: %s", dst)
|
|
|
|
else:
|
|
logger.debug("try to copy and cue %s", src)
|
|
|
|
print '***'
|
|
dst_tmp = self.tmp_dir + "".join([random.choice(string.letters) for i in xrange(10)])
|
|
print dst_tmp
|
|
print '***'
|
|
|
|
try:
|
|
shutil.copy(src, dst_tmp)
|
|
logger.info("copied %s to %s", src, dst_tmp)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
|
|
|
|
# cue
|
|
print "STARTIONG CUE"
|
|
print self.cue_file.cue(dst_tmp, dst, float(media['cue_in']) / 1000, float(media['cue_out']) / 1000)
|
|
print "END CUE"
|
|
|
|
if True == os.access(dst, os.R_OK):
|
|
|
|
try: fsize = os.path.getsize(dst)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
fsize = 0
|
|
|
|
if fsize > 0:
|
|
logger.debug('try to remove temporary file: %s' + dst_tmp)
|
|
try: os.remove(dst_tmp)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
|
|
else:
|
|
logger.warning('something went wrong cueing: %s - using uncued file' + dst)
|
|
try: os.rename(dst_tmp, dst)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
|
|
if True == os.access(dst, os.R_OK):
|
|
# check filesize (avoid zero-byte files)
|
|
#print 'waiting: ' + dst
|
|
|
|
try: fsize = os.path.getsize(dst)
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
fsize = 0
|
|
|
|
if fsize > 0:
|
|
|
|
pl_entry = 'annotate:export_source="%s",media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s":%s' % \
|
|
(str(media['export_source']), media['id'], 0, str(float(media['fade_in']) / 1000), str(float(media['fade_out']) / 1000), dst)
|
|
|
|
print pl_entry
|
|
|
|
"""
|
|
Tracks are only added to the playlist if they are accessible
|
|
on the file system and larger than 0 bytes.
|
|
So this can lead to playlists shorter than expectet.
|
|
(there is a hardware silence detector for this cases...)
|
|
"""
|
|
ls_playlist += pl_entry + "\n"
|
|
|
|
logger.debug("everything ok, adding %s to playlist", pl_entry)
|
|
else:
|
|
print 'zero-file: ' + dst + ' from ' + src
|
|
logger.warning("zero-size file - skiping %s. will not add it to playlist", dst)
|
|
|
|
else:
|
|
logger.warning("something went wrong. file %s not available. will not add it to playlist", dst)
|
|
|
|
except Exception, e: logger.info("%s", e)
|
|
|
|
|
|
"""
|
|
This is kind of hackish. We add a bunch of "silence" tracks to the end of each playlist.
|
|
So we can make sure the list does not get repeated just before a new one is called.
|
|
(or in case nothing is in the scheduler afterwards)
|
|
20 x silence = 10 hours
|
|
"""
|
|
for i in range (0, 1):
|
|
ls_playlist += silence_file + "\n"
|
|
print '',
|
|
|
|
# write playlist file
|
|
plfile = open(self.cache_dir + str(pkey) + '/list.lsp', "w")
|
|
plfile.write(ls_playlist)
|
|
plfile.close()
|
|
logger.info('ls playlist file written to %s', self.cache_dir + str(pkey) + '/list.lsp')
|
|
|
|
except Exception, e:
|
|
logger.info("%s", e)
|
|
|
|
|
|
def cleanup(self, export_source):
|
|
logger = logging.getLogger("cleanup")
|
|
|
|
self.export_source = export_source
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
"""
|
|
Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR"
|
|
and deletes them.
|
|
"""
|
|
|
|
offset = 3600 * int(CACHE_FOR)
|
|
now = time.time()
|
|
|
|
for r, d, f in os.walk(CACHE_DIR):
|
|
for dir in d:
|
|
timestamp = os.path.getmtime(os.path.join(r, dir))
|
|
|
|
logger.debug('Folder "Age": %s - %s', round((((now - offset) - timestamp) / 60), 2), os.path.join(r, dir))
|
|
|
|
if now - offset > timestamp:
|
|
try:
|
|
logger.debug('trying to remove %s - timestamp: %s', os.path.join(r, dir), timestamp)
|
|
shutil.rmtree(os.path.join(r, dir))
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
pass
|
|
else:
|
|
logger.info('sucessfully removed %s', os.path.join(r, dir))
|
|
|
|
|
|
|
|
|
|
"""
|
|
The counterpart - the push loop periodically (minimal 1/2 of the playlist-grid)
|
|
checks if there is a playlist that should be scheduled at the current time.
|
|
If yes, the temporary liquidsoap playlist gets replaced with the corresponding one,
|
|
then liquidsoap is asked (via telnet) to reload and immediately play it.
|
|
"""
|
|
def push(self, export_source):
|
|
logger = logging.getLogger("push")
|
|
|
|
self.export_source = export_source
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
self.push_ahead = 15
|
|
|
|
try:
|
|
dummy = self.schedule
|
|
logger.debug('schedule already loaded')
|
|
except Exception, e:
|
|
self.schedule = self.push_init(self.export_source)
|
|
|
|
self.schedule = self.push_init(self.export_source)
|
|
|
|
|
|
"""
|
|
I'm quite sure that this could be achieved in a much more elegant way in python...
|
|
"""
|
|
|
|
tcomming = time.localtime(time.time() + self.push_ahead)
|
|
tnow = time.localtime(time.time())
|
|
|
|
str_tnow = "%04d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4])
|
|
str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5])
|
|
|
|
str_tcomming = "%04d-%02d-%02d-%02d-%02d" % (tcomming[0], tcomming[1], tcomming[2], tcomming[3], tcomming[4])
|
|
str_tcomming_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcomming[0], tcomming[1], tcomming[2], tcomming[3], tcomming[4], tcomming[5])
|
|
|
|
print '--'
|
|
print str_tnow_s + ' now'
|
|
print str_tcomming_s + ' comming'
|
|
|
|
playnow = None
|
|
|
|
if self.schedule == None:
|
|
print 'unable to loop schedule - maybe write in progress'
|
|
print 'will try in next loop'
|
|
|
|
else:
|
|
for pkey in self.schedule:
|
|
logger.debug('found playlist schedulet at: %s', pkey)
|
|
|
|
#if pkey[0:16] == str_tnow:
|
|
if pkey[0:16] == str_tcomming:
|
|
playlist = self.schedule[pkey]
|
|
|
|
if int(playlist['played']) != 1:
|
|
print '!!!!!!!!!!!!!!!!!!!'
|
|
print 'MATCH'
|
|
|
|
"""
|
|
ok we have a match, replace the current playlist and
|
|
force liquidsoap to refresh
|
|
Add a 'played' state to the list in schedule, so it is not called again
|
|
in the next push loop
|
|
"""
|
|
ptype = playlist['subtype']
|
|
|
|
try:
|
|
user_id = playlist['user_id']
|
|
playlist_id = playlist['id']
|
|
transmission_id = playlist['schedule_id']
|
|
|
|
except Exception, e:
|
|
playlist_id = 0
|
|
user_id = 0
|
|
transmission_id = 0
|
|
print e
|
|
|
|
print 'Playlist id:',
|
|
|
|
if(self.push_liquidsoap(pkey, ptype, user_id, playlist_id, transmission_id, self.push_ahead) == 1):
|
|
self.schedule[pkey]['played'] = 1
|
|
"""
|
|
Call api to update schedule states and
|
|
write changes back to cache file
|
|
"""
|
|
self.api_client.update_scheduled_item(int(playlist['schedule_id']), 1)
|
|
schedule_file = open(self.schedule_file, "w")
|
|
pickle.dump(self.schedule, schedule_file)
|
|
schedule_file.close()
|
|
|
|
#else:
|
|
# print 'Nothing to do...'
|
|
|
|
|
|
|
|
|
|
def push_init(self, export_source):
|
|
logger = logging.getLogger("push_init")
|
|
|
|
self.export_source = export_source
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
# load the shedule from cache
|
|
logger.debug('load shedule from cache')
|
|
try:
|
|
schedule_file = open(self.schedule_file, "r")
|
|
schedule = pickle.load(schedule_file)
|
|
schedule_file.close()
|
|
|
|
except Exception, e:
|
|
logger.error('%s', e)
|
|
schedule = None
|
|
|
|
return schedule
|
|
|
|
|
|
def push_liquidsoap(self, pkey, ptype, user_id, playlist_id, transmission_id, push_ahead):
|
|
logger = logging.getLogger("push_liquidsoap")
|
|
|
|
#self.export_source = export_source
|
|
|
|
self.push_ahead = push_ahead
|
|
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
src = self.cache_dir + str(pkey) + '/list.lsp'
|
|
|
|
print src
|
|
|
|
try:
|
|
if True == os.access(src, os.R_OK):
|
|
print 'OK - Can read'
|
|
|
|
pl_file = open(src, "r")
|
|
|
|
"""
|
|
i know this could be wrapped, maybe later..
|
|
"""
|
|
tn = telnetlib.Telnet(LS_HOST, 1234)
|
|
|
|
|
|
if(int(ptype) == 6):
|
|
tn.write("live_in.start")
|
|
tn.write("\n")
|
|
|
|
|
|
if(int(ptype) < 5):
|
|
for line in pl_file.readlines():
|
|
print line.strip()
|
|
tn.write(self.export_source + '.push %s' % (line.strip()))
|
|
tn.write("\n")
|
|
#time.sleep(0.1)
|
|
|
|
tn.write("exit\n")
|
|
print tn.read_all()
|
|
print 'sleeping for %s s' % (self.push_ahead)
|
|
time.sleep(self.push_ahead)
|
|
|
|
print 'sending "flip"'
|
|
tn = telnetlib.Telnet(LS_HOST, 1234)
|
|
|
|
"""
|
|
Pass some extra information to liquidsoap
|
|
"""
|
|
print 'user_id: %s' % user_id
|
|
print 'playlist_id: %s' % playlist_id
|
|
print 'transmission_id: %s' % transmission_id
|
|
print 'ptype: %s' % ptype
|
|
|
|
tn.write("vars.user_id %s\n" % user_id)
|
|
tn.write("vars.playlist_id %s\n" % playlist_id)
|
|
tn.write("vars.transmission_id %s\n" % transmission_id)
|
|
tn.write("vars.playlist_type %s\n" % ptype)
|
|
|
|
# if(int(ptype) < 5):
|
|
# tn.write(self.export_source + '.flip')
|
|
# tn.write("\n")
|
|
|
|
tn.write(self.export_source + '.flip')
|
|
tn.write("\n")
|
|
|
|
if(int(ptype) == 6):
|
|
tn.write("live.active 1")
|
|
tn.write("\n")
|
|
else:
|
|
tn.write("live.active 0")
|
|
tn.write("\n")
|
|
tn.write("live_in.stop")
|
|
tn.write("\n")
|
|
|
|
tn.write("exit\n")
|
|
|
|
print tn.read_all()
|
|
status = 1
|
|
except Exception, e:
|
|
logger.error('%s', e)
|
|
status = 0
|
|
|
|
return status
|
|
|
|
|
|
def push_liquidsoap_legacy(self, pkey, ptype, p_id, user_id):
|
|
logger = logging.getLogger("push_liquidsoap")
|
|
logger.debug('trying to push %s to liquidsoap', pkey)
|
|
|
|
self.export_source = export_source
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
src = self.cache_dir + str(pkey) + '/list.lsp'
|
|
dst = self.cache_dir + 'current.lsp'
|
|
|
|
print src
|
|
print dst
|
|
|
|
print '*************'
|
|
print ptype
|
|
print '*************'
|
|
|
|
if True == os.access(src, os.R_OK):
|
|
try:
|
|
shutil.copy2(src, dst)
|
|
logger.debug('copy %s to %s', src, dst)
|
|
"""
|
|
i know this could be wrapped, maybe later..
|
|
"""
|
|
tn = telnetlib.Telnet(LS_HOST, 1234)
|
|
tn.write("\n")
|
|
tn.write("live_in.stop\n")
|
|
tn.write("stream_disable\n")
|
|
time.sleep(0.2)
|
|
tn.write("\n")
|
|
#tn.write("reload_current\n")
|
|
tn.write("current.reload\n")
|
|
time.sleep(0.2)
|
|
tn.write("skip_current\n")
|
|
|
|
if(int(ptype) == 6):
|
|
"""
|
|
Couchcaster comming. Stop/Start live input to have ls re-read it's playlist
|
|
"""
|
|
print 'Couchcaster - switching to stream'
|
|
tn.write("live_in.start\n")
|
|
time.sleep(0.2)
|
|
tn.write("stream_enable\n")
|
|
|
|
if(int(ptype) == 7):
|
|
"""
|
|
Recast comming. Start the live input
|
|
"""
|
|
print 'Recast - switching to stream'
|
|
tn.write("live_in.start\n")
|
|
time.sleep(0.2)
|
|
tn.write("stream_enable\n")
|
|
|
|
"""
|
|
Pass some extra information to liquidsoap
|
|
"""
|
|
tn.write("pl.pl_id '%s'\n" % p_id)
|
|
tn.write("pl.user_id '%s'\n" % user_id)
|
|
tn.write("exit\n")
|
|
|
|
print tn.read_all()
|
|
|
|
status = 1
|
|
|
|
except Exception, e:
|
|
logger.error('%s', e)
|
|
status = 0
|
|
else:
|
|
status = 0
|
|
|
|
return status
|
|
|
|
|
|
"""
|
|
Updates the jingles. Give comma separated list of jingle tracks.
|
|
"""
|
|
def update_jingles(self, options):
|
|
print 'jingles'
|
|
|
|
jingle_list = string.split(options, ',')
|
|
print jingle_list
|
|
for media_id in jingle_list:
|
|
# api path maybe should not be hard-coded
|
|
src = API_BASE + 'api/pypo/get_media/' + str(media_id)
|
|
print src
|
|
# include the hourly jungles for the moment
|
|
dst = "%s%s/%s.mp3" % (self.file_dir, 'jingles/hourly', str(media_id))
|
|
print dst
|
|
|
|
try:
|
|
print '** urllib auth with: ',
|
|
print self.api_auth
|
|
opener = urllib.URLopener()
|
|
opener.retrieve (src, dst, False, self.api_auth)
|
|
logger.info("downloaded %s to %s", src, dst)
|
|
except Exception, e:
|
|
print e
|
|
logger.error("%s", e)
|
|
|
|
|
|
def check_schedule(self, export_source):
|
|
logger = logging.getLogger("check_schedule")
|
|
|
|
self.export_source = export_source
|
|
self.cache_dir = CACHE_DIR + self.export_source + '/'
|
|
self.schedule_file = self.cache_dir + 'schedule'
|
|
|
|
try:
|
|
schedule_file = open(self.schedule_file, "r")
|
|
schedule = pickle.load(schedule_file)
|
|
schedule_file.close()
|
|
|
|
except Exception, e:
|
|
logger.error("%s", e)
|
|
schedule = None
|
|
|
|
#for pkey in schedule:
|
|
for pkey in sorted(schedule.iterkeys()):
|
|
|
|
playlist = schedule[pkey]
|
|
|
|
print '*****************************************'
|
|
print '\033[0;32m%s %s\033[m' % ('scheduled at:', str(pkey))
|
|
print 'cached at : ' + self.cache_dir + str(pkey)
|
|
print 'subtype: ' + str(playlist['subtype'])
|
|
print 'played: ' + str(playlist['played'])
|
|
print 'schedule id: ' + str(playlist['schedule_id'])
|
|
print 'duration: ' + str(playlist['duration'])
|
|
print 'source id: ' + str(playlist['x_ident'])
|
|
|
|
print '-----------------------------------------'
|
|
|
|
for media in playlist['medias']:
|
|
print media
|
|
|
|
print
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
print
|
|
print '###########################################'
|
|
print '# *** pypo *** #'
|
|
print '# Liquidsoap + External Scheduler #'
|
|
print '# Playout System #'
|
|
print '###########################################'
|
|
print
|
|
|
|
# initialize
|
|
g = Global()
|
|
g.selfcheck()
|
|
po = Playout()
|
|
|
|
|
|
run = True
|
|
while run == True:
|
|
|
|
logger = logging.getLogger("pypo")
|
|
|
|
loops = 0
|
|
|
|
while options.fetch_scheduler:
|
|
try: po.fetch('scheduler')
|
|
except Exception, e:
|
|
print e
|
|
sys.exit()
|
|
|
|
print 'ZZzZzZzzzzZZZz.... sleeping for ' + str(POLL_INTERVAL) + ' seconds'
|
|
logger.info('fetch loop %s - ZZzZzZzzzzZZZz.... sleeping for %s seconds', loops, POLL_INTERVAL)
|
|
loops += 1
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
while options.fetch_daypart:
|
|
try: po.fetch('daypart')
|
|
except Exception, e:
|
|
print e
|
|
sys.exit()
|
|
|
|
print 'ZZzZzZzzzzZZZz.... sleeping for ' + str(POLL_INTERVAL) + ' seconds'
|
|
logger.info('fetch loop %s - ZZzZzZzzzzZZZz.... sleeping for %s seconds', loops, POLL_INTERVAL)
|
|
loops += 1
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
|
|
while options.push_scheduler:
|
|
|
|
po.push('scheduler')
|
|
|
|
try: po.push('scheduler')
|
|
except Exception, e:
|
|
print 'PUSH ERROR!! WILL EXIT NOW:('
|
|
print e
|
|
sys.exit()
|
|
|
|
logger.info('push loop %s - ZZzZzZzzzzZZZz.... sleeping for %s seconds', loops, PUSH_INTERVAL)
|
|
loops += 1
|
|
time.sleep(PUSH_INTERVAL)
|
|
|
|
|
|
while options.push_daypart:
|
|
|
|
po.push('daypart')
|
|
|
|
try: po.push('daypart')
|
|
except Exception, e:
|
|
print 'PUSH ERROR!! WILL EXIT NOW:('
|
|
print e
|
|
sys.exit()
|
|
|
|
logger.info('push loop %s - ZZzZzZzzzzZZZz.... sleeping for %s seconds', loops, PUSH_INTERVAL)
|
|
loops += 1
|
|
time.sleep(PUSH_INTERVAL)
|
|
|
|
|
|
while options.jingles:
|
|
try: po.update_jingles(options.jingles)
|
|
except Exception, e:
|
|
print e
|
|
sys.exit()
|
|
|
|
|
|
while options.check:
|
|
try: po.check_schedule()
|
|
except Exception, e:
|
|
print e
|
|
sys.exit()
|
|
|
|
while options.cleanup:
|
|
try: po.cleanup()
|
|
except Exception, e:
|
|
print e
|
|
sys.exit()
|
|
|
|
|
|
sys.exit()
|