import os import grp import pwd import logging import stat import subprocess import traceback from subprocess import Popen, PIPE from airtimemetadata import AirtimeMetadata from api_clients import api_client import pyinotify class MediaMonitorCommon: timestamp_file = "/var/tmp/airtime/media-monitor/last_index" def __init__(self, airtime_config, wm=None): self.supported_file_formats = ['mp3', 'ogg'] self.logger = logging.getLogger() self.config = airtime_config self.md_manager = AirtimeMetadata() self.wm = wm def is_parent_directory(self, filepath, directory): filepath = os.path.normpath(filepath) directory = os.path.normpath(directory) return (directory == filepath[0:len(directory)]) def is_temp_file(self, filename): info = filename.split(".") # if file doesn't have any extension, info[-2] throws exception # Hence, checking length of info before we do anything if(len(info) >= 2): if(info[-2] in self.supported_file_formats): return True else: return False else: return False def is_audio_file(self, filename): info = filename.split(".") if(info[-1] in self.supported_file_formats): return True else: return False #check if file is readable by "nobody" def has_correct_permissions(self, filepath, euid='nobody', egid='nogroup'): try: uid = pwd.getpwnam(euid)[2] gid = grp.getgrnam(egid)[2] #drop root permissions and become "nobody" os.setegid(gid) os.seteuid(uid) open(filepath) readable = True except IOError: self.logger.warn("File does not have correct permissions: '%s'", filepath) readable = False self.logger.error("traceback: %s", traceback.format_exc()) except Exception, e: self.logger.error("Unexpected exception thrown: %s", e) readable = False self.logger.error("traceback: %s", traceback.format_exc()) finally: #reset effective user to root os.seteuid(0) os.setegid(0) return readable # the function only changes the permission if its not readable by www-data def set_needed_file_permissions(self, item, is_dir): try: omask = os.umask(0) if not self.has_correct_permissions(item, 'www-data', 'www-data') or not self.has_correct_permissions(item, 'pypo', 'pypo'): # stats.st_mode is the original permission # stat.S_IROTH - readable by all permission # stat.S_IXOTH - excutable by all permission # try to set permission if self.is_parent_directory(item, self.config.storage_directory) or self.is_parent_directory(item, self.config.imported_directory) or self.is_parent_directory(item, self.config.organize_directory): if is_dir is True: os.chmod(item, 02777) else: os.chmod(item, 0666) else : # add world readable permission stats = os.stat(item) if is_dir is True: bitor = stats.st_mode | stat.S_IROTH | stat.S_IXOTH else: bitor = stats.st_mode | stat.S_IROTH os.chmod(item, bitor) except Exception, e: self.logger.error("Failed to change file's owner/group/permissions. %s", e) self.logger.error("traceback: %s", traceback.format_exc()) return False finally: os.umask(omask) return True #checks if path is a directory, and if it doesnt exist, then creates it. #Otherwise prints error to log file. def ensure_is_dir(self, directory): try: omask = os.umask(0) if not os.path.exists(directory): os.makedirs(directory, 02777) self.wm.add_watch(directory, pyinotify.ALL_EVENTS, rec=True, auto_add=True) elif not os.path.isdir(directory): #path exists but it is a file not a directory! self.logger.error("path %s exists, but it is not a directory!!!") finally: os.umask(omask) #moves file from source to dest but also recursively removes the #the source file's parent directories if they are now empty. def move_file(self, source, dest): try: omask = os.umask(0) os.rename(source, dest) except Exception, e: self.logger.error("failed to move file. %s", e) self.logger.error("traceback: %s", traceback.format_exc()) finally: os.umask(omask) dir = os.path.dirname(source) self.cleanup_empty_dirs(dir) #keep moving up the file hierarchy and deleting parent #directories until we hit a non-empty directory, or we #hit the organize dir. def cleanup_empty_dirs(self, dir): if os.path.normpath(dir) != self.config.organize_directory: if len(os.listdir(dir)) == 0: try: os.rmdir(dir) except Exception, e: #non-critical exception because we probably tried to delete a non-empty dir. #Don't need to log this, let's just "return" return pdir = os.path.dirname(dir) self.cleanup_empty_dirs(pdir) #checks if path exists already in stor. If the path exists and the md5s are the #same just overwrite. def create_unique_filename(self, filepath, old_filepath): try: if(os.path.exists(filepath)): self.logger.info("Path %s exists", filepath) self.logger.info("Checking if md5s are the same.") md5_fp = self.md_manager.get_md5(filepath) md5_ofp = self.md_manager.get_md5(old_filepath) if(md5_fp == md5_ofp): self.logger.info("Md5s are the same, moving to same filepath.") return filepath self.logger.info("Md5s aren't the same, appending to filepath.") file_dir = os.path.dirname(filepath) filename = os.path.basename(filepath).split(".")[0] #will be in the format .ext file_ext = os.path.splitext(filepath)[1] i = 1; while(True): new_filepath = '%s/%s(%s)%s' % (file_dir, filename, i, file_ext) self.logger.error("Trying %s", new_filepath) if(os.path.exists(new_filepath)): i = i+1; else: filepath = new_filepath break except Exception, e: self.logger.error("Exception %s", e) return filepath #create path in /srv/airtime/stor/imported/[song-metadata] def create_file_path(self, original_path, orig_md): storage_directory = self.config.storage_directory is_recorded_show = False try: #will be in the format .ext file_ext = os.path.splitext(original_path)[1] file_ext = api_client.encode_to(file_ext, 'utf-8') path_md = ['MDATA_KEY_TITLE', 'MDATA_KEY_CREATOR', 'MDATA_KEY_SOURCE', 'MDATA_KEY_TRACKNUMBER', 'MDATA_KEY_BITRATE'] md = {} for m in path_md: if m not in orig_md: md[m] = api_client.encode_to(u'unknown', 'utf-8') else: #get rid of any "/" which will interfere with the filepath. if isinstance(orig_md[m], basestring): md[m] = orig_md[m].replace("/", "-") else: md[m] = orig_md[m] if 'MDATA_KEY_TRACKNUMBER' in orig_md: #make sure all track numbers are at least 2 digits long in the filepath. md['MDATA_KEY_TRACKNUMBER'] = "%02d" % (int(md['MDATA_KEY_TRACKNUMBER'])) #format bitrate as 128kbps md['MDATA_KEY_BITRATE'] = str(md['MDATA_KEY_BITRATE']/1000)+"kbps" filepath = None #file is recorded by Airtime #/srv/airtime/stor/recorded/year/month/year-month-day-time-showname-bitrate.ext if(md['MDATA_KEY_CREATOR'] == api_client.encode_to("Airtime Show Recorder", 'utf-8')): #yyyy-mm-dd-hh-MM-ss y = orig_md['MDATA_KEY_YEAR'].split("-") filepath = '%s/%s/%s/%s/%s-%s-%s%s' % (storage_directory, api_client.encode_to("recorded", 'utf-8'), y[0], y[1], orig_md['MDATA_KEY_YEAR'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) #"Show-Title-2011-03-28-17:15:00" title = md['MDATA_KEY_TITLE'].split("-") show_hour = title[0] show_min = title[1] show_sec = title[2] show_name = '-'.join(title[3:]) new_md = {} new_md["MDATA_KEY_FILEPATH"] = original_path new_md['MDATA_KEY_TITLE'] = '%s-%s-%s:%s:%s' % (show_name, orig_md['MDATA_KEY_YEAR'], show_hour, show_min, show_sec) self.md_manager.save_md_to_file(new_md) elif(md['MDATA_KEY_TRACKNUMBER'] == api_client.encode_to(u'unknown', 'utf-8')): filepath = '%s/%s/%s/%s/%s-%s%s' % (storage_directory, api_client.encode_to("imported", 'utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) else: filepath = '%s/%s/%s/%s/%s-%s-%s%s' % (storage_directory, api_client.encode_to("imported", 'utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TRACKNUMBER'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) filepath = self.create_unique_filename(filepath, original_path) self.logger.info('Unique filepath: %s', filepath) self.ensure_is_dir(os.path.dirname(filepath)) except Exception, e: self.logger.error('Exception: %s', e) return filepath def exec_command(self, command): p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: self.logger.warn("command \n%s\n return with a non-zero return value", command) self.logger.error(stderr) try: """ File name charset encoding is UTF-8. """ stdout = stdout.decode("UTF-8") except Exception, e: self.logger.error("Could not decode %s using UTF-8" % stdout) return stdout def scan_dir_for_new_files(self, dir): command = 'find "%s" -type f -iname "*.ogg" -o -iname "*.mp3" -readable' % dir.replace('"', '\\"') self.logger.debug(command) stdout = self.exec_command(command) return stdout.splitlines() def touch_index_file(self): dirname = os.path.dirname(self.timestamp_file) try: if not os.path.exists(dirname): os.makedirs(dirname) open(self.timestamp_file, "w") except Exception, e: self.logger.error('Exception: %s', e) self.logger.error("traceback: %s", traceback.format_exc()) def organize_new_file(self, pathname): self.logger.info("Organizing new file: %s", pathname) file_md = self.md_manager.get_md_from_file(pathname) if file_md is not None: filepath = self.create_file_path(pathname, file_md) self.logger.debug("Moving from %s to %s", pathname, filepath) self.move_file(pathname, filepath) else: filepath = None self.logger.warn("File %s, has invalid metadata", pathname) return filepath def test_file_playability(pathname): """ Test if the file can be played by Liquidsoap. Return "True" if Liquidsoap can play it, or if Liquidsoap is not found. """ liquidsoap_found = subprocess.call("which liquidsoap", shell=True) if liquidsoap_found == 0: #return_code = subprocess.call("liquidsoap -r \"%s\"" % pathname.replace('"', '\\"'), shell=True) #when there is an single apostrophe inside of a string quoted by apostrophes, we can only escape it by replace that apostrophe #with '\''. This breaks the string into two, and inserts an escaped single quote in between them. command = "liquidsoap -c 'output.dummy(single(\"%s\"))'" % pathname.replace("'", "'\\''") return_code = subprocess.call(command, shell=True) else: return_code = 0 return (return_code == 0)