CC-430: Audio normalization (Replaygain Support)

-Added support on media-monitor's side
This commit is contained in:
Martin Konecny 2012-07-05 21:54:44 -04:00
parent 1cbb0345b3
commit a687e48d80
7 changed files with 195 additions and 116 deletions

View File

@ -5,11 +5,8 @@ import time
import pyinotify import pyinotify
import shutil import shutil
from subprocess import Popen, PIPE
from api_clients import api_client
class AirtimeMediaMonitorBootstrap(): class AirtimeMediaMonitorBootstrap():
"""AirtimeMediaMonitorBootstrap constructor """AirtimeMediaMonitorBootstrap constructor
Keyword Arguments: Keyword Arguments:
@ -29,11 +26,11 @@ class AirtimeMediaMonitorBootstrap():
self.curr_mtab_file = "/var/tmp/airtime/media-monitor/currMtab" self.curr_mtab_file = "/var/tmp/airtime/media-monitor/currMtab"
self.logger.info("Adding %s on watch list...", self.mount_file) self.logger.info("Adding %s on watch list...", self.mount_file)
self.wm.add_watch(self.mount_file, pyinotify.ALL_EVENTS, rec=False, auto_add=False) self.wm.add_watch(self.mount_file, pyinotify.ALL_EVENTS, rec=False, auto_add=False)
tmp_dir = os.path.dirname(self.curr_mtab_file) tmp_dir = os.path.dirname(self.curr_mtab_file)
if not os.path.exists(tmp_dir): if not os.path.exists(tmp_dir):
os.makedirs(tmp_dir) os.makedirs(tmp_dir)
# create currMtab file if it's the first time # create currMtab file if it's the first time
if not os.path.exists(self.curr_mtab_file): if not os.path.exists(self.curr_mtab_file):
shutil.copy('/etc/mtab', self.curr_mtab_file) shutil.copy('/etc/mtab', self.curr_mtab_file)
@ -77,10 +74,10 @@ class AirtimeMediaMonitorBootstrap():
dir -- pathname of the directory dir -- pathname of the directory
""" """
def sync_database_to_filesystem(self, dir_id, dir): def sync_database_to_filesystem(self, dir_id, dir):
dir = os.path.normpath(dir)+"/" dir = os.path.normpath(dir) + "/"
""" """
set to hold new and/or modified files. We use a set to make it ok if files are added set to hold new and/or modified files. We use a set to make it ok if files are added
twice. This is because some of the tests for new files return result sets that are not twice. This is because some of the tests for new files return result sets that are not
@ -109,13 +106,13 @@ class AirtimeMediaMonitorBootstrap():
if os.path.exists(self.mmc.timestamp_file): if os.path.exists(self.mmc.timestamp_file):
"""find files that have been modified since the last time media-monitor process started.""" """find files that have been modified since the last time media-monitor process started."""
time_diff_sec = time.time() - os.path.getmtime(self.mmc.timestamp_file) time_diff_sec = time.time() - os.path.getmtime(self.mmc.timestamp_file)
command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable -mmin -%d" % (dir, time_diff_sec/60+1) command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable -mmin -%d" % (dir, time_diff_sec / 60 + 1)
else: else:
command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable" % dir command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable" % dir
self.logger.debug(command) self.logger.debug(command)
stdout = self.mmc.exec_command(command) stdout = self.mmc.exec_command(command)
if stdout is None: if stdout is None:
self.logger.error("Unrecoverable error when syncing db to filesystem.") self.logger.error("Unrecoverable error when syncing db to filesystem.")
return return

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import replay_gain
import os import os
import hashlib import hashlib
import mutagen import mutagen
import logging import logging
import math import math
import re
import traceback import traceback
from api_clients import api_client
""" """
list of supported easy tags in mutagen version 1.20 list of supported easy tags in mutagen version 1.20
@ -18,39 +19,39 @@ class AirtimeMetadata:
def __init__(self): def __init__(self):
self.airtime2mutagen = {\ self.airtime2mutagen = {\
"MDATA_KEY_TITLE": "title",\ "MDATA_KEY_TITLE": "title", \
"MDATA_KEY_CREATOR": "artist",\ "MDATA_KEY_CREATOR": "artist", \
"MDATA_KEY_SOURCE": "album",\ "MDATA_KEY_SOURCE": "album", \
"MDATA_KEY_GENRE": "genre",\ "MDATA_KEY_GENRE": "genre", \
"MDATA_KEY_MOOD": "mood",\ "MDATA_KEY_MOOD": "mood", \
"MDATA_KEY_TRACKNUMBER": "tracknumber",\ "MDATA_KEY_TRACKNUMBER": "tracknumber", \
"MDATA_KEY_BPM": "bpm",\ "MDATA_KEY_BPM": "bpm", \
"MDATA_KEY_LABEL": "organization",\ "MDATA_KEY_LABEL": "organization", \
"MDATA_KEY_COMPOSER": "composer",\ "MDATA_KEY_COMPOSER": "composer", \
"MDATA_KEY_ENCODER": "encodedby",\ "MDATA_KEY_ENCODER": "encodedby", \
"MDATA_KEY_CONDUCTOR": "conductor",\ "MDATA_KEY_CONDUCTOR": "conductor", \
"MDATA_KEY_YEAR": "date",\ "MDATA_KEY_YEAR": "date", \
"MDATA_KEY_URL": "website",\ "MDATA_KEY_URL": "website", \
"MDATA_KEY_ISRC": "isrc",\ "MDATA_KEY_ISRC": "isrc", \
"MDATA_KEY_COPYRIGHT": "copyright",\ "MDATA_KEY_COPYRIGHT": "copyright", \
} }
self.mutagen2airtime = {\ self.mutagen2airtime = {\
"title": "MDATA_KEY_TITLE",\ "title": "MDATA_KEY_TITLE", \
"artist": "MDATA_KEY_CREATOR",\ "artist": "MDATA_KEY_CREATOR", \
"album": "MDATA_KEY_SOURCE",\ "album": "MDATA_KEY_SOURCE", \
"genre": "MDATA_KEY_GENRE",\ "genre": "MDATA_KEY_GENRE", \
"mood": "MDATA_KEY_MOOD",\ "mood": "MDATA_KEY_MOOD", \
"tracknumber": "MDATA_KEY_TRACKNUMBER",\ "tracknumber": "MDATA_KEY_TRACKNUMBER", \
"bpm": "MDATA_KEY_BPM",\ "bpm": "MDATA_KEY_BPM", \
"organization": "MDATA_KEY_LABEL",\ "organization": "MDATA_KEY_LABEL", \
"composer": "MDATA_KEY_COMPOSER",\ "composer": "MDATA_KEY_COMPOSER", \
"encodedby": "MDATA_KEY_ENCODER",\ "encodedby": "MDATA_KEY_ENCODER", \
"conductor": "MDATA_KEY_CONDUCTOR",\ "conductor": "MDATA_KEY_CONDUCTOR", \
"date": "MDATA_KEY_YEAR",\ "date": "MDATA_KEY_YEAR", \
"website": "MDATA_KEY_URL",\ "website": "MDATA_KEY_URL", \
"isrc": "MDATA_KEY_ISRC",\ "isrc": "MDATA_KEY_ISRC", \
"copyright": "MDATA_KEY_COPYRIGHT",\ "copyright": "MDATA_KEY_COPYRIGHT", \
} }
self.logger = logging.getLogger() self.logger = logging.getLogger()
@ -67,9 +68,9 @@ class AirtimeMetadata:
## return format hh:mm:ss.uuu ## return format hh:mm:ss.uuu
def format_length(self, mutagen_length): def format_length(self, mutagen_length):
t = float(mutagen_length) t = float(mutagen_length)
h = int(math.floor(t/3600)) h = int(math.floor(t / 3600))
t = t % 3600 t = t % 3600
m = int(math.floor(t/60)) m = int(math.floor(t / 60))
s = t % 60 s = t % 60
# will be ss.uuu # will be ss.uuu
@ -94,12 +95,12 @@ class AirtimeMetadata:
for key in m: for key in m:
if key in self.airtime2mutagen: if key in self.airtime2mutagen:
value = m[key] value = m[key]
if value is not None: if value is not None:
value = unicode(value) value = unicode(value)
else: else:
value = unicode(''); value = unicode('');
#if len(value) > 0: #if len(value) > 0:
self.logger.debug("Saving key '%s' with value '%s' to file", key, value) self.logger.debug("Saving key '%s' with value '%s' to file", key, value)
airtime_file[self.airtime2mutagen[key]] = value airtime_file[self.airtime2mutagen[key]] = value
@ -123,8 +124,14 @@ class AirtimeMetadata:
self.logger.info("getting info from filepath %s", filepath) self.logger.info("getting info from filepath %s", filepath)
md = {}
replay_gain_val = replay_gain.calculate_replay_gain(filepath)
self.logger.info('ReplayGain calculated as %s for %s' % (replay_gain_val, filepath))
md['MDATA_KEY_REPLAYGAIN'] = replay_gain_val
try: try:
md = {}
md5 = self.get_md5(filepath) md5 = self.get_md5(filepath)
md['MDATA_KEY_MD5'] = md5 md['MDATA_KEY_MD5'] = md5
@ -135,6 +142,7 @@ class AirtimeMetadata:
self.logger.error("Exception %s", e) self.logger.error("Exception %s", e)
return None return None
#check if file has any metadata #check if file has any metadata
if file_info is None: if file_info is None:
return None return None
@ -149,6 +157,7 @@ class AirtimeMetadata:
except Exception, e: except Exception, e:
self.logger.error('Exception: %s', e) self.logger.error('Exception: %s', e)
self.logger.error("traceback: %s", traceback.format_exc()) self.logger.error("traceback: %s", traceback.format_exc())
if 'MDATA_KEY_TITLE' not in md: if 'MDATA_KEY_TITLE' not in md:
#get rid of file extension from original name, name might have more than 1 '.' in it. #get rid of file extension from original name, name might have more than 1 '.' in it.
original_name = os.path.basename(filepath) original_name = os.path.basename(filepath)
@ -165,8 +174,6 @@ class AirtimeMetadata:
pass pass
if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring): if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring):
match = re.search('^(\d*/\d*)?', md['MDATA_KEY_TRACKNUMBER'])
try: try:
md['MDATA_KEY_TRACKNUMBER'] = int(md['MDATA_KEY_TRACKNUMBER'].split("/")[0], 10) md['MDATA_KEY_TRACKNUMBER'] = int(md['MDATA_KEY_TRACKNUMBER'].split("/")[0], 10)
except Exception, e: except Exception, e:
@ -223,7 +230,7 @@ class AirtimeMetadata:
md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", None) md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", None)
md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", None) md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", None)
self.logger.info( "Bitrate: %s , Samplerate: %s", md['MDATA_KEY_BITRATE'], md['MDATA_KEY_SAMPLERATE'] ) self.logger.info("Bitrate: %s , Samplerate: %s", md['MDATA_KEY_BITRATE'], md['MDATA_KEY_SAMPLERATE'])
try: md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length) try: md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length)
except Exception as e: self.logger.warn("File: '%s' raises: %s", filepath, str(e)) except Exception as e: self.logger.warn("File: '%s' raises: %s", filepath, str(e))

View File

@ -8,13 +8,11 @@ import traceback
# For RabbitMQ # For RabbitMQ
from kombu.connection import BrokerConnection from kombu.connection import BrokerConnection
from kombu.messaging import Exchange, Queue, Consumer, Producer from kombu.messaging import Exchange, Queue, Consumer
import pyinotify import pyinotify
from pyinotify import Notifier from pyinotify import Notifier
#from api_clients import api_client
from api_clients import api_client
from airtimemetadata import AirtimeMetadata from airtimemetadata import AirtimeMetadata
class AirtimeNotifier(Notifier): class AirtimeNotifier(Notifier):
@ -153,7 +151,6 @@ class AirtimeNotifier(Notifier):
md.update(file_md) md.update(file_md)
else: else:
file_md = None file_md = None
data = None
if (os.path.exists(filepath) and (mode == self.config.MODE_CREATE)): if (os.path.exists(filepath) and (mode == self.config.MODE_CREATE)):
if file_md is None: if file_md is None:
@ -184,7 +181,7 @@ class AirtimeNotifier(Notifier):
self.api_client.update_media_metadata(md, mode) self.api_client.update_media_metadata(md, mode)
elif (mode == self.config.MODE_DELETE): elif (mode == self.config.MODE_DELETE):
self.api_client.update_media_metadata(md, mode) self.api_client.update_media_metadata(md, mode)
elif (mode == self.config.MODE_DELETE_DIR): elif (mode == self.config.MODE_DELETE_DIR):
self.api_client.update_media_metadata(md, mode) self.api_client.update_media_metadata(md, mode)

View File

@ -9,7 +9,6 @@ import difflib
import traceback import traceback
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import pyinotify
from pyinotify import ProcessEvent from pyinotify import ProcessEvent
from airtimemetadata import AirtimeMetadata from airtimemetadata import AirtimeMetadata
@ -59,8 +58,8 @@ class AirtimeProcessEvent(ProcessEvent):
if "-unknown-path" in path: if "-unknown-path" in path:
unknown_path = path unknown_path = path
pos = path.find("-unknown-path") pos = path.find("-unknown-path")
path = path[0:pos]+"/" path = path[0:pos] + "/"
list = self.api_client.list_all_watched_dirs() list = self.api_client.list_all_watched_dirs()
# case where the dir that is being watched is moved to somewhere # case where the dir that is being watched is moved to somewhere
if path in list[u'dirs'].values(): if path in list[u'dirs'].values():
@ -81,14 +80,14 @@ class AirtimeProcessEvent(ProcessEvent):
self.logger.info("Removing watch on: %s wd %s", unknown_path, wd) self.logger.info("Removing watch on: %s wd %s", unknown_path, wd)
self.wm.rm_watch(wd, rec=True) self.wm.rm_watch(wd, rec=True)
self.file_events.append({'mode': self.config.MODE_DELETE_DIR, 'filepath': path}) self.file_events.append({'mode': self.config.MODE_DELETE_DIR, 'filepath': path})
def process_IN_DELETE_SELF(self, event): def process_IN_DELETE_SELF(self, event):
#we only care about files that have been moved away from imported/ or organize/ dir #we only care about files that have been moved away from imported/ or organize/ dir
if event.path in self.config.problem_directory or event.path in self.config.organize_directory: if event.path in self.config.problem_directory or event.path in self.config.organize_directory:
return return
self.logger.info("event: %s", event) self.logger.info("event: %s", event)
path = event.path + '/' path = event.path + '/'
if event.dir: if event.dir:
@ -103,7 +102,7 @@ class AirtimeProcessEvent(ProcessEvent):
self.logger.info("%s removed from watch folder list successfully.", path) self.logger.info("%s removed from watch folder list successfully.", path)
else: else:
self.logger.info("Removing the watch folder failed: %s", res['msg']['error']) self.logger.info("Removing the watch folder failed: %s", res['msg']['error'])
def process_IN_CREATE(self, event): def process_IN_CREATE(self, event):
if event.path in self.mount_file_dir: if event.path in self.mount_file_dir:
return return
@ -111,7 +110,7 @@ class AirtimeProcessEvent(ProcessEvent):
if not event.dir: if not event.dir:
# record the timestamp of the time on IN_CREATE event # record the timestamp of the time on IN_CREATE event
self.create_dict[event.pathname] = time.time() self.create_dict[event.pathname] = time.time()
#event.dir: True if the event was raised against a directory. #event.dir: True if the event was raised against a directory.
#event.name: filename #event.name: filename
#event.pathname: pathname (str): Concatenation of 'path' and 'name'. #event.pathname: pathname (str): Concatenation of 'path' and 'name'.
@ -122,7 +121,7 @@ class AirtimeProcessEvent(ProcessEvent):
return return
self.logger.info("event: %s", event) self.logger.info("event: %s", event)
self.logger.info("create_dict: %s", self.create_dict) self.logger.info("create_dict: %s", self.create_dict)
try: try:
del self.create_dict[event.pathname] del self.create_dict[event.pathname]
self.handle_created_file(event.dir, event.pathname, event.name) self.handle_created_file(event.dir, event.pathname, event.name)
@ -130,8 +129,8 @@ class AirtimeProcessEvent(ProcessEvent):
pass pass
#self.logger.warn("%s does not exist in create_dict", event.pathname) #self.logger.warn("%s does not exist in create_dict", event.pathname)
#Uncomment the above warning when we fix CC-3830 for 2.1.1 #Uncomment the above warning when we fix CC-3830 for 2.1.1
def handle_created_file(self, dir, pathname, name): def handle_created_file(self, dir, pathname, name):
if not dir: if not dir:
self.logger.debug("PROCESS_IN_CLOSE_WRITE: %s, name: %s, pathname: %s ", dir, name, pathname) self.logger.debug("PROCESS_IN_CLOSE_WRITE: %s, name: %s, pathname: %s ", dir, name, pathname)
@ -145,12 +144,12 @@ class AirtimeProcessEvent(ProcessEvent):
self.temp_files[pathname] = None self.temp_files[pathname] = None
elif self.mmc.is_audio_file(name): elif self.mmc.is_audio_file(name):
if self.mmc.is_parent_directory(pathname, self.config.organize_directory): if self.mmc.is_parent_directory(pathname, self.config.organize_directory):
#file was created in /srv/airtime/stor/organize. Need to process and move #file was created in /srv/airtime/stor/organize. Need to process and move
#to /srv/airtime/stor/imported #to /srv/airtime/stor/imported
file_md = self.md_manager.get_md_from_file(pathname) file_md = self.md_manager.get_md_from_file(pathname)
playable = self.mmc.test_file_playability(pathname) playable = self.mmc.test_file_playability(pathname)
if file_md and playable: if file_md and playable:
self.mmc.organize_new_file(pathname, file_md) self.mmc.organize_new_file(pathname, file_md)
else: else:
@ -182,7 +181,7 @@ class AirtimeProcessEvent(ProcessEvent):
if self.mmc.is_audio_file(name): if self.mmc.is_audio_file(name):
is_recorded = self.mmc.is_parent_directory(pathname, self.config.recorded_directory) is_recorded = self.mmc.is_parent_directory(pathname, self.config.recorded_directory)
self.file_events.append({'filepath': pathname, 'mode': self.config.MODE_MODIFY, 'is_recorded_show': is_recorded}) self.file_events.append({'filepath': pathname, 'mode': self.config.MODE_MODIFY, 'is_recorded_show': is_recorded})
# if change is detected on /etc/mtab, we check what mount(file system) was added/removed # if change is detected on /etc/mtab, we check what mount(file system) was added/removed
# and act accordingly # and act accordingly
def handle_mount_change(self): def handle_mount_change(self):
@ -192,41 +191,41 @@ class AirtimeProcessEvent(ProcessEvent):
shutil.move(self.curr_mtab_file, self.prev_mtab_file) shutil.move(self.curr_mtab_file, self.prev_mtab_file)
# create the file # create the file
shutil.copy(self.mount_file, self.curr_mtab_file) shutil.copy(self.mount_file, self.curr_mtab_file)
d = difflib.Differ() d = difflib.Differ()
curr_fh = open(self.curr_mtab_file, 'r') curr_fh = open(self.curr_mtab_file, 'r')
prev_fh = open(self.prev_mtab_file, 'r') prev_fh = open(self.prev_mtab_file, 'r')
diff = list(d.compare(prev_fh.readlines(), curr_fh.readlines())) diff = list(d.compare(prev_fh.readlines(), curr_fh.readlines()))
added_mount_points = [] added_mount_points = []
removed_mount_points = [] removed_mount_points = []
for dir in diff: for dir in diff:
info = dir.split(' ') info = dir.split(' ')
if info[0] == '+': if info[0] == '+':
added_mount_points.append(info[2]) added_mount_points.append(info[2])
elif info[0] == '-': elif info[0] == '-':
removed_mount_points.append(info[2]) removed_mount_points.append(info[2])
self.logger.info("added: %s", added_mount_points) self.logger.info("added: %s", added_mount_points)
self.logger.info("removed: %s", removed_mount_points) self.logger.info("removed: %s", removed_mount_points)
# send current mount information to Airtime # send current mount information to Airtime
self.api_client.update_file_system_mount(added_mount_points, removed_mount_points); self.api_client.update_file_system_mount(added_mount_points, removed_mount_points);
def handle_watched_dir_missing(self, dir): def handle_watched_dir_missing(self, dir):
self.api_client.handle_watched_dir_missing(dir); self.api_client.handle_watched_dir_missing(dir);
#if a file is moved somewhere, this callback is run. With details about #if a file is moved somewhere, this callback is run. With details about
#where the file is being moved from. The corresponding process_IN_MOVED_TO #where the file is being moved from. The corresponding process_IN_MOVED_TO
#callback is only called if the destination of the file is also in a watched #callback is only called if the destination of the file is also in a watched
#directory. #directory.
def process_IN_MOVED_FROM(self, event): def process_IN_MOVED_FROM(self, event):
#we don't care about files that have been moved from problem_directory #we don't care about files that have been moved from problem_directory
if event.path in self.config.problem_directory: if event.path in self.config.problem_directory:
return return
self.logger.info("process_IN_MOVED_FROM: %s", event) self.logger.info("process_IN_MOVED_FROM: %s", event)
if not event.dir: if not event.dir:
if event.pathname in self.temp_files: if event.pathname in self.temp_files:
@ -241,10 +240,10 @@ class AirtimeProcessEvent(ProcessEvent):
def process_IN_MOVED_TO(self, event): def process_IN_MOVED_TO(self, event):
self.logger.info("process_IN_MOVED_TO: %s", event) self.logger.info("process_IN_MOVED_TO: %s", event)
# if /etc/mtab is modified # if /etc/mtab is modified
filename = self.mount_file_dir +"/mtab" filename = self.mount_file_dir + "/mtab"
if event.pathname in filename: if event.pathname in filename:
self.handle_mount_change() self.handle_mount_change()
if event.path in self.config.problem_directory: if event.path in self.config.problem_directory:
return return
@ -265,15 +264,15 @@ class AirtimeProcessEvent(ProcessEvent):
#to /srv/airtime/stor/imported #to /srv/airtime/stor/imported
file_md = self.md_manager.get_md_from_file(pathname) file_md = self.md_manager.get_md_from_file(pathname)
playable = self.mmc.test_file_playability(pathname) playable = self.mmc.test_file_playability(pathname)
if file_md and playable: if file_md and playable:
filepath = self.mmc.organize_new_file(pathname, file_md) filepath = self.mmc.organize_new_file(pathname, file_md)
else: else:
#move to problem_files #move to problem_files
self.mmc.move_to_problem_dir(pathname) self.mmc.move_to_problem_dir(pathname)
else: else:
filepath = event.pathname filepath = event.pathname
@ -283,23 +282,23 @@ class AirtimeProcessEvent(ProcessEvent):
#file's original location is from outside an inotify watched dir. #file's original location is from outside an inotify watched dir.
pathname = event.pathname pathname = event.pathname
if self.mmc.is_parent_directory(pathname, self.config.organize_directory): if self.mmc.is_parent_directory(pathname, self.config.organize_directory):
#file was created in /srv/airtime/stor/organize. Need to process and move #file was created in /srv/airtime/stor/organize. Need to process and move
#to /srv/airtime/stor/imported #to /srv/airtime/stor/imported
file_md = self.md_manager.get_md_from_file(pathname) file_md = self.md_manager.get_md_from_file(pathname)
playable = self.mmc.test_file_playability(pathname) playable = self.mmc.test_file_playability(pathname)
if file_md and playable: if file_md and playable:
self.mmc.organize_new_file(pathname, file_md) self.mmc.organize_new_file(pathname, file_md)
else: else:
#move to problem_files #move to problem_files
self.mmc.move_to_problem_dir(pathname) self.mmc.move_to_problem_dir(pathname)
else: else:
#show moved from unwatched folder into a watched folder. Do not "organize". #show moved from unwatched folder into a watched folder. Do not "organize".
@ -309,33 +308,33 @@ class AirtimeProcessEvent(ProcessEvent):
#When we move a directory into a watched_dir, we only get a notification that the dir was created, #When we move a directory into a watched_dir, we only get a notification that the dir was created,
#and no additional information about files that came along with that directory. #and no additional information about files that came along with that directory.
#need to scan the entire directory for files. #need to scan the entire directory for files.
if event.cookie in self.cookies_IN_MOVED_FROM: if event.cookie in self.cookies_IN_MOVED_FROM:
del self.cookies_IN_MOVED_FROM[event.cookie] del self.cookies_IN_MOVED_FROM[event.cookie]
mode = self.config.MODE_MOVED mode = self.config.MODE_MOVED
else: else:
mode = self.config.MODE_CREATE mode = self.config.MODE_CREATE
files = self.mmc.scan_dir_for_new_files(event.pathname) files = self.mmc.scan_dir_for_new_files(event.pathname)
if self.mmc.is_parent_directory(event.pathname, self.config.organize_directory): if self.mmc.is_parent_directory(event.pathname, self.config.organize_directory):
for pathname in files: for pathname in files:
#file was created in /srv/airtime/stor/organize. Need to process and move #file was created in /srv/airtime/stor/organize. Need to process and move
#to /srv/airtime/stor/imported #to /srv/airtime/stor/imported
file_md = self.md_manager.get_md_from_file(pathname) file_md = self.md_manager.get_md_from_file(pathname)
playable = self.mmc.test_file_playability(pathname) playable = self.mmc.test_file_playability(pathname)
if file_md and playable: if file_md and playable:
self.mmc.organize_new_file(pathname, file_md) self.mmc.organize_new_file(pathname, file_md)
#self.file_events.append({'mode': mode, 'filepath': filepath, 'is_recorded_show': False}) #self.file_events.append({'mode': mode, 'filepath': filepath, 'is_recorded_show': False})
else: else:
#move to problem_files #move to problem_files
self.mmc.move_to_problem_dir(pathname) self.mmc.move_to_problem_dir(pathname)
else: else:
for file in files: for file in files:
self.file_events.append({'mode': mode, 'filepath': file, 'is_recorded_show': False}) self.file_events.append({'mode': mode, 'filepath': file, 'is_recorded_show': False})
@ -368,12 +367,12 @@ class AirtimeProcessEvent(ProcessEvent):
for event in self.file_events: for event in self.file_events:
self.multi_queue.put(event) self.multi_queue.put(event)
self.mmc.touch_index_file() self.mmc.touch_index_file()
self.file_events = [] self.file_events = []
#yield to worker thread #yield to worker thread
time.sleep(0) time.sleep(0)
#use items() because we are going to be modifying this #use items() because we are going to be modifying this
#dictionary while iterating over it. #dictionary while iterating over it.
for k, pair in self.cookies_IN_MOVED_FROM.items(): for k, pair in self.cookies_IN_MOVED_FROM.items():
@ -390,7 +389,7 @@ class AirtimeProcessEvent(ProcessEvent):
#it from the Airtime directory. #it from the Airtime directory.
del self.cookies_IN_MOVED_FROM[k] del self.cookies_IN_MOVED_FROM[k]
self.handle_removed_file(False, event.pathname) self.handle_removed_file(False, event.pathname)
# we don't want create_dict grow infinitely # we don't want create_dict grow infinitely
# this part is like a garbage collector # this part is like a garbage collector
for k, t in self.create_dict.items(): for k, t in self.create_dict.items():
@ -404,14 +403,14 @@ class AirtimeProcessEvent(ProcessEvent):
if os.path.exists(k): if os.path.exists(k):
# check if file is open # check if file is open
try: try:
command = "lsof "+k command = "lsof " + k
#f = os.popen(command) #f = os.popen(command)
f = Popen(command, shell=True, stdout=PIPE).stdout f = Popen(command, shell=True, stdout=PIPE).stdout
except Exception, e: except Exception, e:
self.logger.error('Exception: %s', e) self.logger.error('Exception: %s', e)
self.logger.error("traceback: %s", traceback.format_exc()) self.logger.error("traceback: %s", traceback.format_exc())
continue continue
if not f.readlines(): if not f.readlines():
self.logger.info("Handling file: %s", k) self.logger.info("Handling file: %s", k)
self.handle_created_file(False, k, os.path.basename(k)) self.handle_created_file(False, k, os.path.basename(k))

View File

@ -10,7 +10,6 @@ import traceback
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from airtimemetadata import AirtimeMetadata from airtimemetadata import AirtimeMetadata
from api_clients import api_client
import pyinotify import pyinotify
class MediaMonitorCommon: class MediaMonitorCommon:
@ -75,7 +74,7 @@ class MediaMonitorCommon:
try: try:
return self.is_user_readable(item, 'www-data', 'www-data') \ return self.is_user_readable(item, 'www-data', 'www-data') \
and self.is_user_readable(item, 'pypo', 'pypo') and self.is_user_readable(item, 'pypo', 'pypo')
except Exception, e: except Exception:
self.logger.warn(u"Failed to check owner/group/permissions for %s", item) self.logger.warn(u"Failed to check owner/group/permissions for %s", item)
return False return False
@ -110,7 +109,7 @@ class MediaMonitorCommon:
else: else:
pathname = dirname pathname = dirname
is_dir = True is_dir = True
except Exception, e: except Exception:
#something went wrong while we were trying to make world readable. #something went wrong while we were trying to make world readable.
return False return False
@ -194,7 +193,7 @@ class MediaMonitorCommon:
break break
except Exception, e: except Exception, e:
self.logger.error("Exception %s", e) self.logger.error("Exception %s", e)
return filepath return filepath
@ -273,7 +272,7 @@ class MediaMonitorCommon:
File name charset encoding is UTF-8. File name charset encoding is UTF-8.
""" """
stdout = stdout.decode("UTF-8") stdout = stdout.decode("UTF-8")
except Exception, e: except Exception:
stdout = None stdout = None
self.logger.error("Could not decode %s using UTF-8" % stdout) self.logger.error("Could not decode %s using UTF-8" % stdout)

View File

@ -0,0 +1,81 @@
from subprocess import Popen, PIPE
import re
import os
import sys
def get_process_output(command):
"""
Run subprocess and return stdout
"""
p = Popen(command, shell=True, stdout=PIPE)
return p.communicate()[0].strip()
def run_process(command):
"""
Run subprocess and return "return code"
"""
p = Popen(command, shell=True)
return os.waitpid(p.pid, 0)[1]
def get_mime_type(file_path):
"""
Attempts to get the mime type but will return prematurely if the process
takes longer than 5 seconds. Note that this function should only be called
for files which do not have a mp3/ogg/flac extension.
"""
return get_process_output("timeout 5 file -b --mime-type %s" % file_path)
def calculate_replay_gain(file_path):
"""
This function accepts files of type mp3/ogg/flac and returns a calculated ReplayGain value.
If the value cannot be calculated for some reason, then we default to 1.
TODO:
Currently some of the subprocesses called will actually insert metadata into the file itself,
which we do *not* want as this changes the file's hash. Need to make a copy of the file before
we run this function.
http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification
"""
search = None
if re.search(r'mp3$', file_path, re.IGNORECASE) or get_mime_type(file_path) == "audio/mpeg":
if run_process("which mp3gain > /dev/null") == 0:
out = get_process_output('mp3gain -q "%s" 2> /dev/null' % file_path)
search = re.search(r'Recommended "Track" dB change: (.*)', out)
else:
print "mp3gain not found"
#Log warning
elif re.search(r'ogg$', file_path, re.IGNORECASE) or get_mime_type(file_path) == "application/ogg":
if run_process("which vorbisgain > /dev/null && which ogginfo > dev/null") == 0:
run_process('vorbisgain -q -f "%s" 2>/dev/null >/dev/null' % file_path)
out = get_process_output('ogginfo "%s"' % file_path)
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else:
print "vorbisgain/ogginfo not found"
#Log warning
elif re.search(r'flac$', file_path, re.IGNORECASE) or get_mime_type(file_path) == "audio/x-flac":
if run_process("which metaflac > /dev/null") == 0:
out = get_process_output('metaflac --show-tag=REPLAYGAIN_TRACK_GAIN "%s"' % file_path)
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else:
print "metaflac not found"
#Log warning
else:
pass
#Log unknown file type.
replay_gain = 1
if search:
matches = search.groups()
if len(matches) == 1:
replay_gain = matches[0]
return replay_gain
# Example of running from command line:
# python replay_gain.py /path/to/filename.mp3
if __name__ == "__main__":
print calculate_replay_gain(sys.argv[1])

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from mediaconfig import AirtimeMediaConfig
import traceback import traceback
import os import os
class MediaMonitorWorkerProcess: class MediaMonitorWorkerProcess:
def __init__(self, config, mmc): def __init__(self, config, mmc):
self.config = config self.config = config
self.mmc = mmc self.mmc = mmc