remove media-monitor
airtime-media-monitor conflicts with airtime-analyzer, which is what is now used to monitor the LibreTime application https://github.com/LibreTime/libretime-debian-packaging/issues/2#issuecomment-359987457
This commit is contained in:
parent
4508d0d8c0
commit
c16d7c2a1d
|
@ -54,7 +54,7 @@ class Application_Model_RabbitMq
|
||||||
{
|
{
|
||||||
$md["event_type"] = $event_type;
|
$md["event_type"] = $event_type;
|
||||||
|
|
||||||
$exchange = 'airtime-media-monitor';
|
$exchange = 'airtime-analyzer';
|
||||||
$data = json_encode($md);
|
$data = json_encode($md);
|
||||||
self::sendMessage($exchange, 'direct', true, $data);
|
self::sendMessage($exchange, 'direct', true, $data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,7 +184,7 @@ class Application_Model_Systemstatus
|
||||||
$ip = $component->getDbIp();
|
$ip = $component->getDbIp();
|
||||||
|
|
||||||
$docRoot = self::GetMonitStatus($ip);
|
$docRoot = self::GetMonitStatus($ip);
|
||||||
$data = self::ExtractServiceInformation($docRoot, "airtime-media-monitor");
|
$data = self::ExtractServiceInformation($docRoot, "airtime-analyzer");
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
Make sure RabbitMQ is installed correctly, and that your settings in /etc/airtime/airtime.conf
|
Make sure RabbitMQ is installed correctly, and that your settings in /etc/airtime/airtime.conf
|
||||||
are correct. Try using <code>sudo rabbitmqctl list_users</code> and <code>sudo rabbitmqctl list_vhosts</code>
|
are correct. Try using <code>sudo rabbitmqctl list_users</code> and <code>sudo rabbitmqctl list_vhosts</code>
|
||||||
to see if the airtime user (or your custom RabbitMQ user) exists, then checking that
|
to see if the airtime user (or your custom RabbitMQ user) exists, then checking that
|
||||||
<code>sudo rabbitmqctl list_exchanges</code> contains entries for airtime-media-monitor, airtime-pypo,
|
<code>sudo rabbitmqctl list_exchanges</code> contains entries for airtime-analyzer, airtime-pypo,
|
||||||
and airtime-uploads.
|
and airtime-uploads.
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,9 +107,9 @@ function checkRMQConnection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if airtime-media-monitor is currently running
|
* Check if airtime-analyzer is currently running
|
||||||
*
|
*
|
||||||
* @return boolean true if airtime-media-monitor is running
|
* @return boolean true if airtime-analyzer is running
|
||||||
*/
|
*/
|
||||||
function checkAnalyzerService() {
|
function checkAnalyzerService() {
|
||||||
exec("pgrep -f -u www-data airtime_analyzer", $out, $status);
|
exec("pgrep -f -u www-data airtime_analyzer", $out, $status);
|
||||||
|
|
|
@ -8,7 +8,7 @@ rabbitmqctl start_app
|
||||||
RABBITMQ_VHOST="/airtime_tests"
|
RABBITMQ_VHOST="/airtime_tests"
|
||||||
RABBITMQ_USER="airtime_tests"
|
RABBITMQ_USER="airtime_tests"
|
||||||
RABBITMQ_PASSWORD="airtime_tests"
|
RABBITMQ_PASSWORD="airtime_tests"
|
||||||
EXCHANGES="airtime-pypo|pypo-fetch|airtime-media-monitor|media-monitor"
|
EXCHANGES="airtime-pypo|pypo-fetch|airtime-analyzer|media-monitor"
|
||||||
|
|
||||||
rabbitmqctl list_vhosts | grep $RABBITMQ_VHOST
|
rabbitmqctl list_vhosts | grep $RABBITMQ_VHOST
|
||||||
RESULT="$?"
|
RESULT="$?"
|
||||||
|
|
|
@ -2,7 +2,7 @@ If your Airtime server is not working as expected, individual components of the
|
||||||
|
|
||||||
sudo invoke-rc.d airtime-liquidsoap start|stop|restart|status
|
sudo invoke-rc.d airtime-liquidsoap start|stop|restart|status
|
||||||
sudo invoke-rc.d airtime-playout start|stop|restart|status
|
sudo invoke-rc.d airtime-playout start|stop|restart|status
|
||||||
sudo invoke-rc.d airtime-media-monitor start|stop|restart|status
|
sudo invoke-rc.d airtime-analyzer start|stop|restart|status
|
||||||
sudo invoke-rc.d apache2 start|stop|restart|status
|
sudo invoke-rc.d apache2 start|stop|restart|status
|
||||||
sudo invoke-rc.d rabbitmq-server start|stop|restart|status
|
sudo invoke-rc.d rabbitmq-server start|stop|restart|status
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ The server should respond:
|
||||||
|
|
||||||
Restarting Airtime Playout: Done.
|
Restarting Airtime Playout: Done.
|
||||||
|
|
||||||
The **status** option for **airtime-playout** and **airtime-media-monitor** runs the **airtime-check-system** script to confirm that all of Airtime's dependencies are installed and running correctly.
|
The **status** option for **airtime-playout** and **airtime-analyzer** runs the **airtime-check-system** script to confirm that all of Airtime's dependencies are installed and running correctly.
|
||||||
|
|
||||||
Log files
|
Log files
|
||||||
---------
|
---------
|
||||||
|
@ -79,6 +79,6 @@ where the hostname is *airtime.example.com*. If the hostname has changed, it may
|
||||||
rabbitmqctl add_vhost /airtime
|
rabbitmqctl add_vhost /airtime
|
||||||
rabbitmqctl add_user airtime XXXXXXXXXXXXXXXXXXXX
|
rabbitmqctl add_user airtime XXXXXXXXXXXXXXXXXXXX
|
||||||
rabbitmqctl set_permissions -p /airtime airtime
|
rabbitmqctl set_permissions -p /airtime airtime
|
||||||
"airtime-pypo|pypo-fetch|airtime-media-monitor|media-monitor"
|
"airtime-pypo|pypo-fetch|airtime-analyzer|media-monitor"
|
||||||
"airtime-pypo|pypo-fetch|airtime-media-monitor|media-monitor"
|
"airtime-pypo|pypo-fetch|airtime-analyzer|media-monitor"
|
||||||
"airtime-pypo|pypo-fetch|airtime-media-monitor|media-monitor"
|
"airtime-pypo|pypo-fetch|airtime-analyzer|media-monitor"
|
||||||
|
|
2
install
2
install
|
@ -1017,7 +1017,7 @@ loud "-----------------------------------------------------"
|
||||||
RABBITMQ_VHOST=/airtime
|
RABBITMQ_VHOST=/airtime
|
||||||
RABBITMQ_USER=airtime
|
RABBITMQ_USER=airtime
|
||||||
RABBITMQ_PASSWORD=airtime
|
RABBITMQ_PASSWORD=airtime
|
||||||
EXCHANGES="airtime-pypo|pypo-fetch|airtime-media-monitor|media-monitor"
|
EXCHANGES="airtime-pypo|pypo-fetch|airtime-analyzer|media-monitor"
|
||||||
|
|
||||||
# Ignore errors in this check to avoid dying when vhost isn't found
|
# Ignore errors in this check to avoid dying when vhost isn't found
|
||||||
set +e
|
set +e
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
import logging
|
|
||||||
import locale
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import mm2.mm2 as mm2
|
|
||||||
from std_err_override import LogWriter
|
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
|
||||||
|
|
||||||
def run():
|
|
||||||
global_cfg = '/etc/airtime/airtime.conf'
|
|
||||||
logging_cfg = '/etc/airtime/media_monitor_logging.cfg'
|
|
||||||
|
|
||||||
mm2.main( global_cfg, logging_cfg )
|
|
||||||
|
|
||||||
run()
|
|
|
@ -1,32 +0,0 @@
|
||||||
[loggers]
|
|
||||||
keys= root,notifier,metadata
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys=fileOutHandler
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys=simpleFormatter
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level=DEBUG
|
|
||||||
handlers=fileOutHandler
|
|
||||||
|
|
||||||
[logger_notifier]
|
|
||||||
level=DEBUG
|
|
||||||
handlers=fileOutHandler
|
|
||||||
qualname=notifier
|
|
||||||
|
|
||||||
[logger_metadata]
|
|
||||||
level=DEBUG
|
|
||||||
handlers=fileOutHandler
|
|
||||||
qualname=metadata
|
|
||||||
|
|
||||||
[handler_fileOutHandler]
|
|
||||||
class=logging.handlers.RotatingFileHandler
|
|
||||||
level=DEBUG
|
|
||||||
formatter=simpleFormatter
|
|
||||||
args=("/var/log/airtime/media-monitor/media-monitor.log", 'a', 10000000, 5,)
|
|
||||||
|
|
||||||
[formatter_simpleFormatter]
|
|
||||||
format=%(asctime)s %(levelname)s - [%(threadName)s] [%(filename)s : %(funcName)s()] : LINE %(lineno)d - %(message)s
|
|
||||||
datefmt=
|
|
|
@ -1,78 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
### BEGIN INIT INFO
|
|
||||||
# Provides: airtime-media-monitor
|
|
||||||
# Required-Start: $local_fs $remote_fs $network $syslog $all
|
|
||||||
# Required-Stop: $local_fs $remote_fs $network $syslog
|
|
||||||
# Default-Start: 2 3 4 5
|
|
||||||
# Default-Stop: 0 1 6
|
|
||||||
# Short-Description: Manage airtime-media-monitor daemon
|
|
||||||
### END INIT INFO
|
|
||||||
|
|
||||||
USERID=www-data
|
|
||||||
GROUPID=www-data
|
|
||||||
NAME=airtime-media-monitor
|
|
||||||
|
|
||||||
DAEMON=/usr/bin/$NAME
|
|
||||||
PIDFILE=/var/run/$NAME.pid
|
|
||||||
|
|
||||||
# Exit if the package is not installed
|
|
||||||
[ -x "$DAEMON" ] || exit 0
|
|
||||||
|
|
||||||
# Read configuration variable file if it is present
|
|
||||||
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
|
|
||||||
|
|
||||||
# Load the VERBOSE setting and other rcS variables
|
|
||||||
. /lib/init/vars.sh
|
|
||||||
|
|
||||||
# Define LSB log_* functions.
|
|
||||||
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
|
|
||||||
# and status_of_proc is working.
|
|
||||||
. /lib/lsb/init-functions
|
|
||||||
|
|
||||||
start () {
|
|
||||||
start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID \
|
|
||||||
--make-pidfile --pidfile $PIDFILE --startas $DAEMON
|
|
||||||
}
|
|
||||||
|
|
||||||
stop () {
|
|
||||||
# Send TERM after 5 seconds, wait at most 30 seconds.
|
|
||||||
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE
|
|
||||||
rm -f $PIDFILE
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-''}" in
|
|
||||||
'start')
|
|
||||||
# start commands here
|
|
||||||
echo -n "Starting $NAME: "
|
|
||||||
start
|
|
||||||
echo "Done."
|
|
||||||
;;
|
|
||||||
'stop')
|
|
||||||
# stop commands here
|
|
||||||
echo -n "Stopping $NAME: "
|
|
||||||
stop
|
|
||||||
echo "Done."
|
|
||||||
;;
|
|
||||||
'restart')
|
|
||||||
# restart commands here
|
|
||||||
echo -n "Restarting $NAME: "
|
|
||||||
stop
|
|
||||||
start
|
|
||||||
echo "Done."
|
|
||||||
;;
|
|
||||||
'force-reload')
|
|
||||||
# reload commands here
|
|
||||||
echo -n "Reloading $NAME: "
|
|
||||||
stop
|
|
||||||
start
|
|
||||||
echo "Done."
|
|
||||||
;;
|
|
||||||
'status')
|
|
||||||
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
|
|
||||||
;;
|
|
||||||
*) # no parameter specified
|
|
||||||
echo "Usage: $SELF start|stop|restart|status"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
|
@ -1,15 +0,0 @@
|
||||||
description "Airtime Media Monitor"
|
|
||||||
author "help@sourcefabric.org"
|
|
||||||
|
|
||||||
start on runlevel [2345]
|
|
||||||
stop on runlevel [!2345]
|
|
||||||
|
|
||||||
respawn
|
|
||||||
|
|
||||||
setuid WEB_USER
|
|
||||||
setgid WEB_USER
|
|
||||||
|
|
||||||
env LANG='en_US.UTF-8'
|
|
||||||
env LC_ALL='en_US.UTF-8'
|
|
||||||
|
|
||||||
exec airtime-media-monitor
|
|
|
@ -1,16 +0,0 @@
|
||||||
import logging
|
|
||||||
import locale
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import mm2.mm2 as mm2
|
|
||||||
from std_err_override import LogWriter
|
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
|
||||||
|
|
||||||
def run():
|
|
||||||
global_cfg = '/etc/airtime/airtime.conf'
|
|
||||||
logging_cfg = os.path.join(os.path.dirname(__file__), 'logging.cfg')
|
|
||||||
|
|
||||||
mm2.main( global_cfg, logging_cfg )
|
|
||||||
|
|
||||||
run()
|
|
|
@ -1,168 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import pyinotify
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
class AirtimeMediaMonitorBootstrap():
|
|
||||||
|
|
||||||
"""AirtimeMediaMonitorBootstrap constructor
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
logger -- reference to the media-monitor logging facility
|
|
||||||
pe -- reference to an instance of ProcessEvent
|
|
||||||
api_clients -- reference of api_clients to communicate with airtime-server
|
|
||||||
"""
|
|
||||||
def __init__(self, logger, pe, api_client, mmc, wm, config):
|
|
||||||
self.logger = logger
|
|
||||||
self.pe = pe
|
|
||||||
self.api_client = api_client
|
|
||||||
self.mmc = mmc
|
|
||||||
self.wm = wm
|
|
||||||
self.config = config
|
|
||||||
# add /etc on watch list so we can detect mount
|
|
||||||
self.mount_file = "/etc"
|
|
||||||
self.curr_mtab_file = "/var/tmp/airtime/media-monitor/currMtab"
|
|
||||||
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)
|
|
||||||
|
|
||||||
tmp_dir = os.path.dirname(self.curr_mtab_file)
|
|
||||||
if not os.path.exists(tmp_dir):
|
|
||||||
os.makedirs(tmp_dir)
|
|
||||||
|
|
||||||
# create currMtab file if it's the first time
|
|
||||||
if not os.path.exists(self.curr_mtab_file):
|
|
||||||
shutil.copy('/etc/mtab', self.curr_mtab_file)
|
|
||||||
|
|
||||||
"""On bootup we want to scan all directories and look for files that
|
|
||||||
weren't there or files that changed before media-monitor process
|
|
||||||
went offline.
|
|
||||||
"""
|
|
||||||
def scan(self):
|
|
||||||
directories = self.get_list_of_watched_dirs()
|
|
||||||
self.logger.info("watched directories found: %s", directories)
|
|
||||||
|
|
||||||
for id, dir in directories.iteritems():
|
|
||||||
self.logger.debug("%s, %s", id, dir)
|
|
||||||
self.sync_database_to_filesystem(id, dir)
|
|
||||||
|
|
||||||
"""Gets a list of files that the Airtime database knows for a specific directory.
|
|
||||||
You need to provide the directory's row ID, which is obtained when calling
|
|
||||||
get_list_of_watched_dirs function.
|
|
||||||
dir_id -- row id of the directory in the cc_watched_dirs database table
|
|
||||||
"""
|
|
||||||
def list_db_files(self, dir_id):
|
|
||||||
return self.api_client.list_all_db_files(dir_id)
|
|
||||||
|
|
||||||
"""
|
|
||||||
returns the path and its corresponding database row idfor all watched directories. Also
|
|
||||||
returns the Stor directory, which can be identified by its row id (always has value of "1")
|
|
||||||
|
|
||||||
Return type is a dictionary similar to:
|
|
||||||
{"1":"/srv/airtime/stor/"}
|
|
||||||
"""
|
|
||||||
def get_list_of_watched_dirs(self):
|
|
||||||
json = self.api_client.list_all_watched_dirs()
|
|
||||||
|
|
||||||
try:
|
|
||||||
return json["dirs"]
|
|
||||||
except KeyError as e:
|
|
||||||
self.logger.error("Could not find index 'dirs' in dictionary: %s", str(json))
|
|
||||||
self.logger.error(str(e))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
"""
|
|
||||||
This function takes in a path name provided by the database (and its corresponding row id)
|
|
||||||
and reads the list of files in the local file system. Its purpose is to discover which files
|
|
||||||
exist on the file system but not in the database and vice versa, as well as which files have
|
|
||||||
been modified since the database was last updated. In each case, this method will call an
|
|
||||||
appropiate method to ensure that the database actually represents the filesystem.
|
|
||||||
dir_id -- row id of the directory in the cc_watched_dirs database table
|
|
||||||
dir -- pathname of the directory
|
|
||||||
"""
|
|
||||||
def sync_database_to_filesystem(self, dir_id, dir):
|
|
||||||
# TODO: is this line even necessary?
|
|
||||||
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
|
|
||||||
twice. This is because some of the tests for new files return result sets that are not
|
|
||||||
mutually exclusive from each other.
|
|
||||||
"""
|
|
||||||
removed_files = set()
|
|
||||||
|
|
||||||
|
|
||||||
db_known_files_set = set()
|
|
||||||
files = self.list_db_files(dir_id)
|
|
||||||
|
|
||||||
for f in files:
|
|
||||||
db_known_files_set.add(f)
|
|
||||||
|
|
||||||
all_files = self.mmc.clean_dirty_file_paths( self.mmc.scan_dir_for_new_files(dir) )
|
|
||||||
|
|
||||||
all_files_set = set()
|
|
||||||
for file_path in all_files:
|
|
||||||
if self.config.problem_directory not in file_path:
|
|
||||||
all_files_set.add(file_path[len(dir):])
|
|
||||||
|
|
||||||
# if dir doesn't exists, update db
|
|
||||||
if not os.path.exists(dir):
|
|
||||||
self.pe.handle_stdout_files(dir)
|
|
||||||
|
|
||||||
if os.path.exists(self.mmc.timestamp_file):
|
|
||||||
"""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)
|
|
||||||
command = self.mmc.find_command(directory=dir, extra_arguments=("-type f -readable -mmin -%d" % (time_diff_sec/60+1)))
|
|
||||||
else:
|
|
||||||
command = self.mmc.find_command(directory=dir, extra_arguments="-type f -readable")
|
|
||||||
|
|
||||||
self.logger.debug(command)
|
|
||||||
stdout = self.mmc.exec_command(command)
|
|
||||||
|
|
||||||
if stdout is None:
|
|
||||||
new_files = []
|
|
||||||
else:
|
|
||||||
new_files = stdout.splitlines()
|
|
||||||
|
|
||||||
new_and_modified_files = set()
|
|
||||||
for file_path in new_files:
|
|
||||||
if self.config.problem_directory not in file_path:
|
|
||||||
new_and_modified_files.add(file_path[len(dir):])
|
|
||||||
|
|
||||||
"""
|
|
||||||
new_and_modified_files gives us a set of files that were either copied or modified
|
|
||||||
since the last time media-monitor was running. These files were collected based on
|
|
||||||
their modified timestamp. But this is not all that has changed in the directory. Files
|
|
||||||
could have been removed, or files could have been moved into this directory (moving does
|
|
||||||
not affect last modified timestamp). Lets get a list of files that are on the file-system
|
|
||||||
that the db has no record of, and vice-versa.
|
|
||||||
"""
|
|
||||||
deleted_files_set = db_known_files_set - all_files_set
|
|
||||||
new_files_set = all_files_set - db_known_files_set
|
|
||||||
modified_files_set = new_and_modified_files - new_files_set
|
|
||||||
|
|
||||||
self.logger.info(u"Deleted files: \n%s\n\n", deleted_files_set)
|
|
||||||
self.logger.info(u"New files: \n%s\n\n", new_files_set)
|
|
||||||
self.logger.info(u"Modified files: \n%s\n\n", modified_files_set)
|
|
||||||
|
|
||||||
#"touch" file timestamp
|
|
||||||
try:
|
|
||||||
self.mmc.touch_index_file()
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.warn(e)
|
|
||||||
|
|
||||||
for file_path in deleted_files_set:
|
|
||||||
self.logger.debug("deleted file")
|
|
||||||
full_file_path = os.path.join(dir, file_path)
|
|
||||||
self.logger.debug(full_file_path)
|
|
||||||
self.pe.handle_removed_file(False, full_file_path)
|
|
||||||
|
|
||||||
|
|
||||||
for file_set, debug_message, handle_attribute in [(new_files_set, "new file", "handle_created_file"),
|
|
||||||
(modified_files_set, "modified file", "handle_modified_file")]:
|
|
||||||
for file_path in file_set:
|
|
||||||
self.logger.debug(debug_message)
|
|
||||||
full_file_path = os.path.join(dir, file_path)
|
|
||||||
self.logger.debug(full_file_path)
|
|
||||||
if os.path.exists(full_file_path):
|
|
||||||
getattr(self.pe,handle_attribute)(False,full_file_path, os.path.basename(full_file_path))
|
|
|
@ -1,268 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import replaygain
|
|
||||||
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import mutagen
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
list of supported easy tags in mutagen version 1.20
|
|
||||||
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry',
|
|
||||||
'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby',
|
|
||||||
'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid',
|
|
||||||
'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort',
|
|
||||||
'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor',
|
|
||||||
'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid',
|
|
||||||
'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid',
|
|
||||||
'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid',
|
|
||||||
'arranger', 'albumsort', 'replaygain_*_peak', 'organization']
|
|
||||||
"""
|
|
||||||
|
|
||||||
class AirtimeMetadata:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
self.airtime2mutagen = {\
|
|
||||||
"MDATA_KEY_TITLE": "title", \
|
|
||||||
"MDATA_KEY_CREATOR": "artist", \
|
|
||||||
"MDATA_KEY_SOURCE": "album", \
|
|
||||||
"MDATA_KEY_GENRE": "genre", \
|
|
||||||
"MDATA_KEY_MOOD": "mood", \
|
|
||||||
"MDATA_KEY_TRACKNUMBER": "tracknumber", \
|
|
||||||
"MDATA_KEY_BPM": "bpm", \
|
|
||||||
"MDATA_KEY_LABEL": "organization", \
|
|
||||||
"MDATA_KEY_COMPOSER": "composer", \
|
|
||||||
"MDATA_KEY_ENCODER": "encodedby", \
|
|
||||||
"MDATA_KEY_CONDUCTOR": "conductor", \
|
|
||||||
"MDATA_KEY_YEAR": "date", \
|
|
||||||
"MDATA_KEY_URL": "website", \
|
|
||||||
"MDATA_KEY_ISRC": "isrc", \
|
|
||||||
"MDATA_KEY_COPYRIGHT": "copyright", \
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mutagen2airtime = {\
|
|
||||||
"title": "MDATA_KEY_TITLE", \
|
|
||||||
"artist": "MDATA_KEY_CREATOR", \
|
|
||||||
"album": "MDATA_KEY_SOURCE", \
|
|
||||||
"genre": "MDATA_KEY_GENRE", \
|
|
||||||
"mood": "MDATA_KEY_MOOD", \
|
|
||||||
"tracknumber": "MDATA_KEY_TRACKNUMBER", \
|
|
||||||
"bpm": "MDATA_KEY_BPM", \
|
|
||||||
"organization": "MDATA_KEY_LABEL", \
|
|
||||||
"composer": "MDATA_KEY_COMPOSER", \
|
|
||||||
"encodedby": "MDATA_KEY_ENCODER", \
|
|
||||||
"conductor": "MDATA_KEY_CONDUCTOR", \
|
|
||||||
"date": "MDATA_KEY_YEAR", \
|
|
||||||
"website": "MDATA_KEY_URL", \
|
|
||||||
"isrc": "MDATA_KEY_ISRC", \
|
|
||||||
"copyright": "MDATA_KEY_COPYRIGHT", \
|
|
||||||
}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger()
|
|
||||||
|
|
||||||
def get_md5(self, filepath):
|
|
||||||
"""
|
|
||||||
Returns an md5 of the file located at filepath. Returns an empty string
|
|
||||||
if there was an error reading the file.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
f = open(filepath, 'rb')
|
|
||||||
m = hashlib.md5()
|
|
||||||
m.update(f.read())
|
|
||||||
md5 = m.hexdigest()
|
|
||||||
except Exception, e:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
return md5
|
|
||||||
|
|
||||||
## mutagen_length is in seconds with the format (d+).dd
|
|
||||||
## return format hh:mm:ss.uuu
|
|
||||||
def format_length(self, mutagen_length):
|
|
||||||
t = float(mutagen_length)
|
|
||||||
h = int(math.floor(t / 3600))
|
|
||||||
t = t % 3600
|
|
||||||
m = int(math.floor(t / 60))
|
|
||||||
|
|
||||||
s = t % 60
|
|
||||||
# will be ss.uuu
|
|
||||||
s = str(s)
|
|
||||||
seconds = s.split(".")
|
|
||||||
s = seconds[0]
|
|
||||||
|
|
||||||
# have a maximum of 6 subseconds.
|
|
||||||
if len(seconds[1]) >= 6:
|
|
||||||
ss = seconds[1][0:6]
|
|
||||||
else:
|
|
||||||
ss = seconds[1][0:]
|
|
||||||
|
|
||||||
length = "%s:%s:%s.%s" % (h, m, s, ss)
|
|
||||||
|
|
||||||
return length
|
|
||||||
|
|
||||||
def save_md_to_file(self, m):
|
|
||||||
try:
|
|
||||||
airtime_file = mutagen.File(m['MDATA_KEY_FILEPATH'], easy=True)
|
|
||||||
|
|
||||||
for key in m:
|
|
||||||
if key in self.airtime2mutagen:
|
|
||||||
value = m[key]
|
|
||||||
|
|
||||||
if value is not None:
|
|
||||||
value = unicode(value)
|
|
||||||
else:
|
|
||||||
value = unicode('');
|
|
||||||
|
|
||||||
#if len(value) > 0:
|
|
||||||
self.logger.debug("Saving key '%s' with value '%s' to file", key, value)
|
|
||||||
airtime_file[self.airtime2mutagen[key]] = value
|
|
||||||
|
|
||||||
airtime_file.save()
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error('Trying to save md')
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
self.logger.error('Filepath %s', m['MDATA_KEY_FILEPATH'])
|
|
||||||
|
|
||||||
def truncate_to_length(self, item, length):
|
|
||||||
if isinstance(item, int):
|
|
||||||
item = str(item)
|
|
||||||
if isinstance(item, basestring):
|
|
||||||
if len(item) > length:
|
|
||||||
return item[0:length]
|
|
||||||
else:
|
|
||||||
return item
|
|
||||||
|
|
||||||
def get_md_from_file(self, filepath):
|
|
||||||
"""
|
|
||||||
Returns None if error retrieving metadata. Otherwise returns a dictionary
|
|
||||||
representing the file's metadata
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.logger.info("getting info from filepath %s", filepath)
|
|
||||||
|
|
||||||
md = {}
|
|
||||||
|
|
||||||
replay_gain_val = replaygain.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:
|
|
||||||
|
|
||||||
md5 = self.get_md5(filepath)
|
|
||||||
md['MDATA_KEY_MD5'] = md5
|
|
||||||
|
|
||||||
file_info = mutagen.File(filepath, easy=True)
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error("failed getting metadata from %s", filepath)
|
|
||||||
self.logger.error("Exception %s", e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
#check if file has any metadata
|
|
||||||
if file_info is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for key in file_info.keys() :
|
|
||||||
if key in self.mutagen2airtime:
|
|
||||||
val = file_info[key]
|
|
||||||
try:
|
|
||||||
if val is not None and len(val) > 0 and val[0] is not None and len(val[0]) > 0:
|
|
||||||
md[self.mutagen2airtime[key]] = val[0]
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
self.logger.error("traceback: %s", traceback.format_exc())
|
|
||||||
if 'MDATA_KEY_TITLE' not in md:
|
|
||||||
#get rid of file extension from original name, name might have more than 1 '.' in it.
|
|
||||||
original_name = os.path.basename(filepath)
|
|
||||||
original_name = original_name.split(".")[0:-1]
|
|
||||||
original_name = ''.join(original_name)
|
|
||||||
md['MDATA_KEY_TITLE'] = original_name
|
|
||||||
|
|
||||||
#incase track number is in format u'4/11'
|
|
||||||
#need to also check that the tracknumber is even a tracknumber (cc-2582)
|
|
||||||
if 'MDATA_KEY_TRACKNUMBER' in md:
|
|
||||||
try:
|
|
||||||
md['MDATA_KEY_TRACKNUMBER'] = int(md['MDATA_KEY_TRACKNUMBER'])
|
|
||||||
except Exception, e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring):
|
|
||||||
try:
|
|
||||||
md['MDATA_KEY_TRACKNUMBER'] = int(md['MDATA_KEY_TRACKNUMBER'].split("/")[0], 10)
|
|
||||||
except Exception, e:
|
|
||||||
del md['MDATA_KEY_TRACKNUMBER']
|
|
||||||
|
|
||||||
#make sure bpm is valid, need to check more types of formats for this tag to assure correct parsing.
|
|
||||||
if 'MDATA_KEY_BPM' in md:
|
|
||||||
if isinstance(md['MDATA_KEY_BPM'], basestring):
|
|
||||||
try:
|
|
||||||
md['MDATA_KEY_BPM'] = int(md['MDATA_KEY_BPM'])
|
|
||||||
except Exception, e:
|
|
||||||
del md['MDATA_KEY_BPM']
|
|
||||||
|
|
||||||
#following metadata is truncated if needed to fit db requirements.
|
|
||||||
if 'MDATA_KEY_GENRE' in md:
|
|
||||||
md['MDATA_KEY_GENRE'] = self.truncate_to_length(md['MDATA_KEY_GENRE'], 64)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_TITLE' in md:
|
|
||||||
md['MDATA_KEY_TITLE'] = self.truncate_to_length(md['MDATA_KEY_TITLE'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_CREATOR' in md:
|
|
||||||
md['MDATA_KEY_CREATOR'] = self.truncate_to_length(md['MDATA_KEY_CREATOR'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_SOURCE' in md:
|
|
||||||
md['MDATA_KEY_SOURCE'] = self.truncate_to_length(md['MDATA_KEY_SOURCE'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_MOOD' in md:
|
|
||||||
md['MDATA_KEY_MOOD'] = self.truncate_to_length(md['MDATA_KEY_MOOD'], 64)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_LABEL' in md:
|
|
||||||
md['MDATA_KEY_LABEL'] = self.truncate_to_length(md['MDATA_KEY_LABEL'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_COMPOSER' in md:
|
|
||||||
md['MDATA_KEY_COMPOSER'] = self.truncate_to_length(md['MDATA_KEY_COMPOSER'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_ENCODER' in md:
|
|
||||||
md['MDATA_KEY_ENCODER'] = self.truncate_to_length(md['MDATA_KEY_ENCODER'], 255)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_CONDUCTOR' in md:
|
|
||||||
md['MDATA_KEY_CONDUCTOR'] = self.truncate_to_length(md['MDATA_KEY_CONDUCTOR'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_YEAR' in md:
|
|
||||||
md['MDATA_KEY_YEAR'] = self.truncate_to_length(md['MDATA_KEY_YEAR'], 16)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_URL' in md:
|
|
||||||
md['MDATA_KEY_URL'] = self.truncate_to_length(md['MDATA_KEY_URL'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_ISRC' in md:
|
|
||||||
md['MDATA_KEY_ISRC'] = self.truncate_to_length(md['MDATA_KEY_ISRC'], 512)
|
|
||||||
|
|
||||||
if 'MDATA_KEY_COPYRIGHT' in md:
|
|
||||||
md['MDATA_KEY_COPYRIGHT'] = self.truncate_to_length(md['MDATA_KEY_COPYRIGHT'], 512)
|
|
||||||
#end of db truncation checks.
|
|
||||||
|
|
||||||
try:
|
|
||||||
md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", 0)
|
|
||||||
md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", 0)
|
|
||||||
|
|
||||||
md['MDATA_KEY_DURATION'] = self.format_length(getattr(file_info.info, "length", 0.0))
|
|
||||||
|
|
||||||
md['MDATA_KEY_MIME'] = ""
|
|
||||||
if len(file_info.mime) > 0:
|
|
||||||
md['MDATA_KEY_MIME'] = file_info.mime[0]
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.warn(e)
|
|
||||||
|
|
||||||
if "mp3" in md['MDATA_KEY_MIME']:
|
|
||||||
md['MDATA_KEY_FTYPE'] = "audioclip"
|
|
||||||
elif "vorbis" in md['MDATA_KEY_MIME']:
|
|
||||||
md['MDATA_KEY_FTYPE'] = "audioclip"
|
|
||||||
else:
|
|
||||||
self.logger.error("File %s of mime type %s does not appear to be a valid vorbis or mp3 file." % (filepath, md['MDATA_KEY_MIME']))
|
|
||||||
return None
|
|
||||||
|
|
||||||
return md
|
|
|
@ -1,213 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
# For RabbitMQ
|
|
||||||
from kombu.connection import BrokerConnection
|
|
||||||
from kombu.messaging import Exchange, Queue, Consumer
|
|
||||||
|
|
||||||
import pyinotify
|
|
||||||
from pyinotify import Notifier
|
|
||||||
|
|
||||||
from airtimemetadata import AirtimeMetadata
|
|
||||||
|
|
||||||
class AirtimeNotifier(Notifier):
|
|
||||||
|
|
||||||
def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=0, timeout=None, airtime_config=None, api_client=None, bootstrap=None, mmc=None):
|
|
||||||
Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout)
|
|
||||||
|
|
||||||
self.logger = logging.getLogger()
|
|
||||||
self.config = airtime_config
|
|
||||||
self.api_client = api_client
|
|
||||||
self.bootstrap = bootstrap
|
|
||||||
self.md_manager = AirtimeMetadata()
|
|
||||||
self.import_processes = {}
|
|
||||||
self.watched_folders = []
|
|
||||||
self.mmc = mmc
|
|
||||||
self.wm = watch_manager
|
|
||||||
self.mask = pyinotify.ALL_EVENTS
|
|
||||||
|
|
||||||
while not self.init_rabbit_mq():
|
|
||||||
self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
def init_rabbit_mq(self):
|
|
||||||
"""
|
|
||||||
This function will attempt to connect to RabbitMQ Server and if successful
|
|
||||||
return 'True'. Returns 'False' otherwise.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.logger.info("Initializing RabbitMQ stuff")
|
|
||||||
try:
|
|
||||||
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
|
|
||||||
schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem")
|
|
||||||
self.connection = BrokerConnection(self.config.cfg["rabbitmq"]["rabbitmq_host"], self.config.cfg["rabbitmq"]["rabbitmq_user"], self.config.cfg["rabbitmq"]["rabbitmq_password"], self.config.cfg["rabbitmq"]["rabbitmq_vhost"])
|
|
||||||
channel = self.connection.channel()
|
|
||||||
consumer = Consumer(channel, schedule_queue)
|
|
||||||
consumer.register_callback(self.handle_message)
|
|
||||||
consumer.consume()
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def handle_message(self, body, message):
|
|
||||||
"""
|
|
||||||
Messages received from RabbitMQ are handled here. These messages
|
|
||||||
instruct media-monitor of events such as a new directory being watched,
|
|
||||||
file metadata has been changed, or any other changes to the config of
|
|
||||||
media-monitor via the web UI.
|
|
||||||
"""
|
|
||||||
# ACK the message to take it off the queue
|
|
||||||
message.ack()
|
|
||||||
|
|
||||||
self.logger.info("Received md from RabbitMQ: " + body)
|
|
||||||
m = json.loads(message.body)
|
|
||||||
|
|
||||||
if m['event_type'] == "md_update":
|
|
||||||
self.logger.info("AIRTIME NOTIFIER md update event")
|
|
||||||
self.md_manager.save_md_to_file(m)
|
|
||||||
|
|
||||||
elif m['event_type'] == "new_watch":
|
|
||||||
self.logger.info("AIRTIME NOTIFIER add watched folder event " + m['directory'])
|
|
||||||
self.walk_newly_watched_directory(m['directory'])
|
|
||||||
|
|
||||||
self.watch_directory(m['directory'])
|
|
||||||
|
|
||||||
elif m['event_type'] == "remove_watch":
|
|
||||||
watched_directory = m['directory']
|
|
||||||
|
|
||||||
mm = self.proc_fun()
|
|
||||||
wd = mm.wm.get_wd(watched_directory)
|
|
||||||
self.logger.info("Removing watch on: %s wd %s", watched_directory, wd)
|
|
||||||
mm.wm.rm_watch(wd, rec=True)
|
|
||||||
|
|
||||||
elif m['event_type'] == "rescan_watch":
|
|
||||||
self.bootstrap.sync_database_to_filesystem(str(m['id']), m['directory'])
|
|
||||||
|
|
||||||
elif m['event_type'] == "change_stor":
|
|
||||||
storage_directory = self.config.storage_directory
|
|
||||||
new_storage_directory = m['directory']
|
|
||||||
new_storage_directory_id = str(m['dir_id'])
|
|
||||||
|
|
||||||
mm = self.proc_fun()
|
|
||||||
|
|
||||||
wd = mm.wm.get_wd(storage_directory)
|
|
||||||
self.logger.info("Removing watch on: %s wd %s", storage_directory, wd)
|
|
||||||
mm.wm.rm_watch(wd, rec=True)
|
|
||||||
|
|
||||||
self.bootstrap.sync_database_to_filesystem(new_storage_directory_id, new_storage_directory)
|
|
||||||
|
|
||||||
self.config.storage_directory = os.path.normpath(new_storage_directory)
|
|
||||||
self.config.imported_directory = os.path.normpath(os.path.join(new_storage_directory, '/imported'))
|
|
||||||
self.config.organize_directory = os.path.normpath(os.path.join(new_storage_directory, '/organize'))
|
|
||||||
|
|
||||||
for directory in [self.config.storage_directory, self.config.imported_directory, self.config.organize_directory]:
|
|
||||||
self.mmc.ensure_is_dir(directory)
|
|
||||||
self.mmc.is_readable(directory, True)
|
|
||||||
|
|
||||||
self.watch_directory(new_storage_directory)
|
|
||||||
elif m['event_type'] == "file_delete":
|
|
||||||
filepath = m['filepath']
|
|
||||||
|
|
||||||
mm = self.proc_fun()
|
|
||||||
self.logger.info("Adding file to ignore: %s ", filepath)
|
|
||||||
mm.add_filepath_to_ignore(filepath)
|
|
||||||
|
|
||||||
if m['delete']:
|
|
||||||
self.logger.info("Deleting file: %s ", filepath)
|
|
||||||
try:
|
|
||||||
os.unlink(filepath)
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
self.logger.error("traceback: %s", traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
def update_airtime(self, event):
|
|
||||||
"""
|
|
||||||
Update airtime with information about files discovered in our
|
|
||||||
watched directories.
|
|
||||||
event: a dict() object with the following attributes:
|
|
||||||
-filepath
|
|
||||||
-mode
|
|
||||||
-data
|
|
||||||
-is_recorded_show
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.logger.info("updating filepath: %s ", event['filepath'])
|
|
||||||
filepath = event['filepath']
|
|
||||||
mode = event['mode']
|
|
||||||
|
|
||||||
md = {}
|
|
||||||
md['MDATA_KEY_FILEPATH'] = os.path.normpath(filepath)
|
|
||||||
|
|
||||||
if 'data' in event:
|
|
||||||
file_md = event['data']
|
|
||||||
md.update(file_md)
|
|
||||||
else:
|
|
||||||
file_md = None
|
|
||||||
|
|
||||||
if (os.path.exists(filepath) and (mode == self.config.MODE_CREATE)):
|
|
||||||
if file_md is None:
|
|
||||||
mutagen = self.md_manager.get_md_from_file(filepath)
|
|
||||||
if mutagen is None:
|
|
||||||
return
|
|
||||||
md.update(mutagen)
|
|
||||||
|
|
||||||
if 'is_recorded_show' in event and event['is_recorded_show']:
|
|
||||||
self.api_client.update_media_metadata(md, mode, True)
|
|
||||||
else:
|
|
||||||
self.api_client.update_media_metadata(md, mode)
|
|
||||||
|
|
||||||
elif (os.path.exists(filepath) and (mode == self.config.MODE_MODIFY)):
|
|
||||||
mutagen = self.md_manager.get_md_from_file(filepath)
|
|
||||||
if mutagen is None:
|
|
||||||
return
|
|
||||||
md.update(mutagen)
|
|
||||||
if 'is_recorded_show' in event and event['is_recorded_show']:
|
|
||||||
self.api_client.update_media_metadata(md, mode, True)
|
|
||||||
else:
|
|
||||||
self.api_client.update_media_metadata(md, mode)
|
|
||||||
elif (mode == self.config.MODE_MOVED):
|
|
||||||
md['MDATA_KEY_MD5'] = self.md_manager.get_md5(filepath)
|
|
||||||
if 'is_recorded_show' in event and event['is_recorded_show']:
|
|
||||||
self.api_client.update_media_metadata(md, mode, True)
|
|
||||||
else:
|
|
||||||
self.api_client.update_media_metadata(md, mode)
|
|
||||||
elif (mode == self.config.MODE_DELETE):
|
|
||||||
self.api_client.update_media_metadata(md, mode)
|
|
||||||
|
|
||||||
elif (mode == self.config.MODE_DELETE_DIR):
|
|
||||||
self.api_client.update_media_metadata(md, mode)
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error("failed updating filepath: %s ", event['filepath'])
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
self.logger.error('Traceback: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
#define which directories the pyinotify WatchManager should watch.
|
|
||||||
def watch_directory(self, directory):
|
|
||||||
return self.wm.add_watch(directory, self.mask, rec=True, auto_add=True)
|
|
||||||
|
|
||||||
def walk_newly_watched_directory(self, directory):
|
|
||||||
|
|
||||||
mm = self.proc_fun()
|
|
||||||
|
|
||||||
self.mmc.is_readable(directory, True)
|
|
||||||
for (path, dirs, files) in os.walk(directory):
|
|
||||||
for filename in files:
|
|
||||||
full_filepath = os.path.join(path, filename)
|
|
||||||
|
|
||||||
if self.mmc.is_audio_file(full_filepath):
|
|
||||||
if self.mmc.is_readable(full_filepath, False):
|
|
||||||
self.logger.info("importing %s", full_filepath)
|
|
||||||
event = {'filepath': full_filepath, 'mode': self.config.MODE_CREATE, 'is_recorded_show': False}
|
|
||||||
mm.multi_queue.put(event)
|
|
||||||
else:
|
|
||||||
self.logger.warn("file '%s' has does not have sufficient read permissions. Ignoring.", full_filepath)
|
|
||||||
|
|
|
@ -1,431 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import socket
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import difflib
|
|
||||||
import traceback
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
|
|
||||||
from pyinotify import ProcessEvent
|
|
||||||
|
|
||||||
from airtimemetadata import AirtimeMetadata
|
|
||||||
from airtimefilemonitor.mediaconfig import AirtimeMediaConfig
|
|
||||||
|
|
||||||
from api_clients import api_client
|
|
||||||
|
|
||||||
class AirtimeProcessEvent(ProcessEvent):
|
|
||||||
|
|
||||||
#TODO
|
|
||||||
def my_init(self, queue, airtime_config=None, wm=None, mmc=None, api_client=api_client):
|
|
||||||
"""
|
|
||||||
Method automatically called from ProcessEvent.__init__(). Additional
|
|
||||||
keyworded arguments passed to ProcessEvent.__init__() are then
|
|
||||||
delegated to my_init().
|
|
||||||
"""
|
|
||||||
self.logger = logging.getLogger()
|
|
||||||
self.config = airtime_config
|
|
||||||
|
|
||||||
#put the file path into this dict if we want to ignore certain
|
|
||||||
#events. For example, when deleting a file from the web ui, we
|
|
||||||
#are going to delete it from the db on the server side, so media-monitor
|
|
||||||
#doesn't need to contact the server and tell it to delete again.
|
|
||||||
self.ignore_event = set()
|
|
||||||
|
|
||||||
self.temp_files = {}
|
|
||||||
self.cookies_IN_MOVED_FROM = {}
|
|
||||||
self.file_events = []
|
|
||||||
self.multi_queue = queue
|
|
||||||
self.wm = wm
|
|
||||||
self.md_manager = AirtimeMetadata()
|
|
||||||
self.mmc = mmc
|
|
||||||
self.api_client = api_client
|
|
||||||
self.create_dict = {}
|
|
||||||
self.mount_file_dir = "/etc";
|
|
||||||
self.mount_file = "/etc/mtab";
|
|
||||||
self.curr_mtab_file = "/var/tmp/airtime/media-monitor/currMtab"
|
|
||||||
self.prev_mtab_file = "/var/tmp/airtime/media-monitor/prevMtab"
|
|
||||||
|
|
||||||
def add_filepath_to_ignore(self, filepath):
|
|
||||||
self.ignore_event.add(filepath)
|
|
||||||
|
|
||||||
def process_IN_MOVE_SELF(self, event):
|
|
||||||
self.logger.info("event: %s", event)
|
|
||||||
path = event.path
|
|
||||||
if event.dir:
|
|
||||||
if "-unknown-path" in path:
|
|
||||||
unknown_path = path
|
|
||||||
pos = path.find("-unknown-path")
|
|
||||||
path = path[0:pos] + "/"
|
|
||||||
|
|
||||||
list = self.api_client.list_all_watched_dirs()
|
|
||||||
# case where the dir that is being watched is moved to somewhere
|
|
||||||
if path in list[u'dirs'].values():
|
|
||||||
self.logger.info("Requesting the airtime server to remove '%s'", path)
|
|
||||||
res = self.api_client.remove_watched_dir(path)
|
|
||||||
if(res is None):
|
|
||||||
self.logger.info("Unable to connect to the Airtime server.")
|
|
||||||
# sucess
|
|
||||||
if(res['msg']['code'] == 0):
|
|
||||||
self.logger.info("%s removed from watch folder list successfully.", path)
|
|
||||||
else:
|
|
||||||
self.logger.info("Removing the watch folder failed: %s", res['msg']['error'])
|
|
||||||
else:
|
|
||||||
# subdir being moved
|
|
||||||
# in this case, it has to remove watch manualy and also have to manually delete all records
|
|
||||||
# on cc_files table
|
|
||||||
wd = self.wm.get_wd(unknown_path)
|
|
||||||
self.logger.info("Removing watch on: %s wd %s", unknown_path, wd)
|
|
||||||
self.wm.rm_watch(wd, rec=True)
|
|
||||||
self.file_events.append({'mode': self.config.MODE_DELETE_DIR, 'filepath': path})
|
|
||||||
|
|
||||||
|
|
||||||
def process_IN_DELETE_SELF(self, event):
|
|
||||||
|
|
||||||
#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:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.info("event: %s", event)
|
|
||||||
path = event.path + '/'
|
|
||||||
if event.dir:
|
|
||||||
list = self.api_client.list_all_watched_dirs()
|
|
||||||
if path in list[u'dirs'].values():
|
|
||||||
self.logger.info("Requesting the airtime server to remove '%s'", path)
|
|
||||||
res = self.api_client.remove_watched_dir(path)
|
|
||||||
if(res is None):
|
|
||||||
self.logger.info("Unable to connect to the Airtime server.")
|
|
||||||
# sucess
|
|
||||||
if(res['msg']['code'] == 0):
|
|
||||||
self.logger.info("%s removed from watch folder list successfully.", path)
|
|
||||||
else:
|
|
||||||
self.logger.info("Removing the watch folder failed: %s", res['msg']['error'])
|
|
||||||
|
|
||||||
def process_IN_CREATE(self, event):
|
|
||||||
if event.path in self.mount_file_dir:
|
|
||||||
return
|
|
||||||
self.logger.info("event: %s", event)
|
|
||||||
if not event.dir:
|
|
||||||
# record the timestamp of the time on IN_CREATE event
|
|
||||||
self.create_dict[event.pathname] = time.time()
|
|
||||||
|
|
||||||
#event.dir: True if the event was raised against a directory.
|
|
||||||
#event.name: filename
|
|
||||||
#event.pathname: pathname (str): Concatenation of 'path' and 'name'.
|
|
||||||
# we used to use IN_CREATE event, but the IN_CREATE event gets fired before the
|
|
||||||
# copy was done. Hence, IN_CLOSE_WRITE is the correct one to handle.
|
|
||||||
def process_IN_CLOSE_WRITE(self, event):
|
|
||||||
if event.path in self.mount_file_dir:
|
|
||||||
return
|
|
||||||
self.logger.info("event: %s", event)
|
|
||||||
self.logger.info("create_dict: %s", self.create_dict)
|
|
||||||
|
|
||||||
try:
|
|
||||||
del self.create_dict[event.pathname]
|
|
||||||
self.handle_created_file(event.dir, event.pathname, event.name)
|
|
||||||
except KeyError, e:
|
|
||||||
pass
|
|
||||||
#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
|
|
||||||
|
|
||||||
|
|
||||||
def handle_created_file(self, dir, pathname, name):
|
|
||||||
if not dir:
|
|
||||||
self.logger.debug("PROCESS_IN_CLOSE_WRITE: %s, name: %s, pathname: %s ", dir, name, pathname)
|
|
||||||
|
|
||||||
if self.mmc.is_temp_file(name) :
|
|
||||||
#file created is a tmp file which will be modified and then moved back to the original filename.
|
|
||||||
#Easy Tag creates this when changing metadata of ogg files.
|
|
||||||
self.temp_files[pathname] = None
|
|
||||||
#file is being overwritten/replaced in GUI.
|
|
||||||
elif "goutputstream" in pathname:
|
|
||||||
self.temp_files[pathname] = None
|
|
||||||
elif self.mmc.is_audio_file(name):
|
|
||||||
if self.mmc.is_parent_directory(pathname, self.config.organize_directory):
|
|
||||||
|
|
||||||
#file was created in /srv/airtime/stor/organize. Need to process and move
|
|
||||||
#to /srv/airtime/stor/imported
|
|
||||||
file_md = self.md_manager.get_md_from_file(pathname)
|
|
||||||
playable = self.mmc.test_file_playability(pathname)
|
|
||||||
|
|
||||||
if file_md and playable:
|
|
||||||
self.mmc.organize_new_file(pathname, file_md)
|
|
||||||
else:
|
|
||||||
#move to problem_files
|
|
||||||
self.mmc.move_to_problem_dir(pathname)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# only append to self.file_events if the file isn't going to be altered by organize_new_file(). If file is going
|
|
||||||
# to be altered by organize_new_file(), then process_IN_MOVED_TO event will handle appending it to self.file_events
|
|
||||||
is_recorded = self.mmc.is_parent_directory(pathname, self.config.recorded_directory)
|
|
||||||
self.file_events.append({'mode': self.config.MODE_CREATE, 'filepath': pathname, 'is_recorded_show': is_recorded})
|
|
||||||
|
|
||||||
|
|
||||||
def process_IN_MODIFY(self, event):
|
|
||||||
# if IN_MODIFY is followed by IN_CREATE, it's not true modify event
|
|
||||||
if not event.pathname in self.create_dict:
|
|
||||||
self.logger.info("process_IN_MODIFY: %s", event)
|
|
||||||
self.handle_modified_file(event.dir, event.pathname, event.name)
|
|
||||||
|
|
||||||
def handle_modified_file(self, dir, pathname, name):
|
|
||||||
# if /etc/mtab is modified
|
|
||||||
if pathname in self.mount_file:
|
|
||||||
self.handle_mount_change()
|
|
||||||
# update timestamp on create_dict for the entry with pathname as the key
|
|
||||||
if pathname in self.create_dict:
|
|
||||||
self.create_dict[pathname] = time.time()
|
|
||||||
if not dir and not self.mmc.is_parent_directory(pathname, self.config.organize_directory):
|
|
||||||
self.logger.info("Modified: %s", pathname)
|
|
||||||
if self.mmc.is_audio_file(name):
|
|
||||||
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})
|
|
||||||
|
|
||||||
# if change is detected on /etc/mtab, we check what mount(file system) was added/removed
|
|
||||||
# and act accordingly
|
|
||||||
def handle_mount_change(self):
|
|
||||||
self.logger.info("Mount change detected, handling changes...");
|
|
||||||
# take snapshot of mtab file and update currMtab and prevMtab
|
|
||||||
# move currMtab to prevMtab and create new currMtab
|
|
||||||
shutil.move(self.curr_mtab_file, self.prev_mtab_file)
|
|
||||||
# create the file
|
|
||||||
shutil.copy(self.mount_file, self.curr_mtab_file)
|
|
||||||
|
|
||||||
d = difflib.Differ()
|
|
||||||
curr_fh = open(self.curr_mtab_file, 'r')
|
|
||||||
prev_fh = open(self.prev_mtab_file, 'r')
|
|
||||||
|
|
||||||
diff = list(d.compare(prev_fh.readlines(), curr_fh.readlines()))
|
|
||||||
added_mount_points = []
|
|
||||||
removed_mount_points = []
|
|
||||||
|
|
||||||
for dir in diff:
|
|
||||||
info = dir.split(' ')
|
|
||||||
if info[0] == '+':
|
|
||||||
added_mount_points.append(info[2])
|
|
||||||
elif info[0] == '-':
|
|
||||||
removed_mount_points.append(info[2])
|
|
||||||
|
|
||||||
self.logger.info("added: %s", added_mount_points)
|
|
||||||
self.logger.info("removed: %s", removed_mount_points)
|
|
||||||
|
|
||||||
# send current mount information to Airtime
|
|
||||||
self.api_client.update_file_system_mount(added_mount_points, removed_mount_points);
|
|
||||||
|
|
||||||
def handle_watched_dir_missing(self, dir):
|
|
||||||
self.api_client.handle_watched_dir_missing(dir);
|
|
||||||
|
|
||||||
#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
|
|
||||||
#callback is only called if the destination of the file is also in a watched
|
|
||||||
#directory.
|
|
||||||
def process_IN_MOVED_FROM(self, event):
|
|
||||||
|
|
||||||
#we don't care about files that have been moved from problem_directory
|
|
||||||
if event.path in self.config.problem_directory:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.info("process_IN_MOVED_FROM: %s", event)
|
|
||||||
if not event.dir:
|
|
||||||
if event.pathname in self.temp_files:
|
|
||||||
self.temp_files[event.cookie] = event.pathname
|
|
||||||
elif not self.mmc.is_parent_directory(event.pathname, self.config.organize_directory):
|
|
||||||
#we don't care about moved_from events from the organize dir.
|
|
||||||
if self.mmc.is_audio_file(event.name):
|
|
||||||
self.cookies_IN_MOVED_FROM[event.cookie] = (event, time.time())
|
|
||||||
else:
|
|
||||||
self.cookies_IN_MOVED_FROM[event.cookie] = (event, time.time())
|
|
||||||
|
|
||||||
def process_IN_MOVED_TO(self, event):
|
|
||||||
self.logger.info("process_IN_MOVED_TO: %s", event)
|
|
||||||
# if /etc/mtab is modified
|
|
||||||
filename = self.mount_file_dir + "/mtab"
|
|
||||||
if event.pathname in filename:
|
|
||||||
self.handle_mount_change()
|
|
||||||
|
|
||||||
if event.path in self.config.problem_directory:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not event.dir:
|
|
||||||
if self.mmc.is_audio_file(event.name):
|
|
||||||
if event.cookie in self.temp_files:
|
|
||||||
self.file_events.append({'filepath': event.pathname, 'mode': self.config.MODE_MODIFY})
|
|
||||||
del self.temp_files[event.cookie]
|
|
||||||
elif event.cookie in self.cookies_IN_MOVED_FROM:
|
|
||||||
#file's original location was also in a watched directory
|
|
||||||
del self.cookies_IN_MOVED_FROM[event.cookie]
|
|
||||||
if self.mmc.is_parent_directory(event.pathname, self.config.organize_directory):
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pathname = event.pathname
|
|
||||||
#file was created in /srv/airtime/stor/organize. Need to process and move
|
|
||||||
#to /srv/airtime/stor/imported
|
|
||||||
file_md = self.md_manager.get_md_from_file(pathname)
|
|
||||||
playable = self.mmc.test_file_playability(pathname)
|
|
||||||
|
|
||||||
if file_md and playable:
|
|
||||||
filepath = self.mmc.organize_new_file(pathname, file_md)
|
|
||||||
else:
|
|
||||||
#move to problem_files
|
|
||||||
self.mmc.move_to_problem_dir(pathname)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
filepath = event.pathname
|
|
||||||
|
|
||||||
if (filepath is not None):
|
|
||||||
self.file_events.append({'filepath': filepath, 'mode': self.config.MODE_MOVED})
|
|
||||||
else:
|
|
||||||
#file's original location is from outside an inotify watched dir.
|
|
||||||
pathname = event.pathname
|
|
||||||
if self.mmc.is_parent_directory(pathname, self.config.organize_directory):
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#file was created in /srv/airtime/stor/organize. Need to process and move
|
|
||||||
#to /srv/airtime/stor/imported
|
|
||||||
file_md = self.md_manager.get_md_from_file(pathname)
|
|
||||||
playable = self.mmc.test_file_playability(pathname)
|
|
||||||
|
|
||||||
if file_md and playable:
|
|
||||||
self.mmc.organize_new_file(pathname, file_md)
|
|
||||||
else:
|
|
||||||
#move to problem_files
|
|
||||||
self.mmc.move_to_problem_dir(pathname)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
#show moved from unwatched folder into a watched folder. Do not "organize".
|
|
||||||
is_recorded = self.mmc.is_parent_directory(event.pathname, self.config.recorded_directory)
|
|
||||||
self.file_events.append({'mode': self.config.MODE_CREATE, 'filepath': event.pathname, 'is_recorded_show': is_recorded})
|
|
||||||
else:
|
|
||||||
#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.
|
|
||||||
#need to scan the entire directory for files.
|
|
||||||
|
|
||||||
if event.cookie in self.cookies_IN_MOVED_FROM:
|
|
||||||
del self.cookies_IN_MOVED_FROM[event.cookie]
|
|
||||||
mode = self.config.MODE_MOVED
|
|
||||||
else:
|
|
||||||
mode = self.config.MODE_CREATE
|
|
||||||
|
|
||||||
files = self.mmc.scan_dir_for_new_files(event.pathname)
|
|
||||||
if self.mmc.is_parent_directory(event.pathname, self.config.organize_directory):
|
|
||||||
for pathname in files:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#file was created in /srv/airtime/stor/organize. Need to process and move
|
|
||||||
#to /srv/airtime/stor/imported
|
|
||||||
file_md = self.md_manager.get_md_from_file(pathname)
|
|
||||||
playable = self.mmc.test_file_playability(pathname)
|
|
||||||
|
|
||||||
if file_md and playable:
|
|
||||||
self.mmc.organize_new_file(pathname, file_md)
|
|
||||||
#self.file_events.append({'mode': mode, 'filepath': filepath, 'is_recorded_show': False})
|
|
||||||
else:
|
|
||||||
#move to problem_files
|
|
||||||
self.mmc.move_to_problem_dir(pathname)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
for file in files:
|
|
||||||
self.file_events.append({'mode': mode, 'filepath': file, 'is_recorded_show': False})
|
|
||||||
|
|
||||||
|
|
||||||
def process_IN_DELETE(self, event):
|
|
||||||
if event.path in self.mount_file_dir:
|
|
||||||
return
|
|
||||||
self.logger.info("process_IN_DELETE: %s", event)
|
|
||||||
self.handle_removed_file(event.dir, event.pathname)
|
|
||||||
|
|
||||||
def handle_removed_file(self, dir, pathname):
|
|
||||||
self.logger.info("Deleting %s", pathname)
|
|
||||||
if not dir:
|
|
||||||
if self.mmc.is_audio_file(pathname):
|
|
||||||
if pathname in self.ignore_event:
|
|
||||||
self.logger.info("pathname in ignore event")
|
|
||||||
self.ignore_event.remove(pathname)
|
|
||||||
elif not self.mmc.is_parent_directory(pathname, self.config.organize_directory):
|
|
||||||
self.logger.info("deleting a file not in organize")
|
|
||||||
#we don't care if a file was deleted from the organize directory.
|
|
||||||
self.file_events.append({'filepath': pathname, 'mode': self.config.MODE_DELETE})
|
|
||||||
|
|
||||||
|
|
||||||
def process_default(self, event):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def notifier_loop_callback(self, notifier):
|
|
||||||
if len(self.file_events) > 0:
|
|
||||||
for event in self.file_events:
|
|
||||||
self.multi_queue.put(event)
|
|
||||||
self.mmc.touch_index_file()
|
|
||||||
|
|
||||||
self.file_events = []
|
|
||||||
|
|
||||||
#yield to worker thread
|
|
||||||
time.sleep(0)
|
|
||||||
|
|
||||||
#use items() because we are going to be modifying this
|
|
||||||
#dictionary while iterating over it.
|
|
||||||
for k, pair in self.cookies_IN_MOVED_FROM.items():
|
|
||||||
event = pair[0]
|
|
||||||
timestamp = pair[1]
|
|
||||||
|
|
||||||
timestamp_now = time.time()
|
|
||||||
|
|
||||||
if timestamp_now - timestamp > 5:
|
|
||||||
#in_moved_from event didn't have a corresponding
|
|
||||||
#in_moved_to event in the last 5 seconds.
|
|
||||||
#This means the file was moved to outside of the
|
|
||||||
#watched directories. Let's handle this by deleting
|
|
||||||
#it from the Airtime directory.
|
|
||||||
del self.cookies_IN_MOVED_FROM[k]
|
|
||||||
self.handle_removed_file(False, event.pathname)
|
|
||||||
|
|
||||||
# we don't want create_dict grow infinitely
|
|
||||||
# this part is like a garbage collector
|
|
||||||
for k, t in self.create_dict.items():
|
|
||||||
now = time.time()
|
|
||||||
if now - t > 5:
|
|
||||||
# check if file exist
|
|
||||||
# When whole directory is copied to the organized dir,
|
|
||||||
# inotify doesn't fire IN_CLOSE_WRITE, hench we need special way of
|
|
||||||
# handling those cases. We are manully calling handle_created_file
|
|
||||||
# function.
|
|
||||||
if os.path.exists(k):
|
|
||||||
# check if file is open
|
|
||||||
try:
|
|
||||||
command = "lsof " + k
|
|
||||||
#f = os.popen(command)
|
|
||||||
f = Popen(command, shell=True, stdout=PIPE).stdout
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
self.logger.error("traceback: %s", traceback.format_exc())
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not f.readlines():
|
|
||||||
self.logger.info("Handling file: %s", k)
|
|
||||||
self.handle_created_file(False, k, os.path.basename(k))
|
|
||||||
del self.create_dict[k]
|
|
||||||
else:
|
|
||||||
del self.create_dict[k]
|
|
||||||
|
|
||||||
#check for any events received from Airtime.
|
|
||||||
try:
|
|
||||||
notifier.connection.drain_events(timeout=0.1)
|
|
||||||
#avoid logging a bunch of timeout messages.
|
|
||||||
except socket.timeout:
|
|
||||||
pass
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
self.logger.error("traceback: %s", traceback.format_exc())
|
|
||||||
time.sleep(3)
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from configobj import ConfigObj
|
|
||||||
|
|
||||||
class AirtimeMediaConfig:
|
|
||||||
|
|
||||||
MODE_CREATE = "create"
|
|
||||||
MODE_MODIFY = "modify"
|
|
||||||
MODE_MOVED = "moved"
|
|
||||||
MODE_DELETE = "delete"
|
|
||||||
MODE_DELETE_DIR = "delete_dir"
|
|
||||||
|
|
||||||
def __init__(self, logger):
|
|
||||||
|
|
||||||
# loading config file
|
|
||||||
try:
|
|
||||||
config = ConfigObj('/etc/airtime/airtime.conf')
|
|
||||||
self.cfg = config
|
|
||||||
except Exception, e:
|
|
||||||
logger.info('Error loading config: ', e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
self.storage_directory = None
|
|
||||||
|
|
||||||
|
|
|
@ -1,341 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import grp
|
|
||||||
import pwd
|
|
||||||
import logging
|
|
||||||
import stat
|
|
||||||
import subprocess
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
from airtimemetadata import AirtimeMetadata
|
|
||||||
import pyinotify
|
|
||||||
|
|
||||||
class MediaMonitorCommon:
|
|
||||||
|
|
||||||
timestamp_file = "/var/tmp/airtime/media-monitor/last_index"
|
|
||||||
supported_file_formats = ['mp3', 'ogg']
|
|
||||||
|
|
||||||
def __init__(self, airtime_config, wm=None):
|
|
||||||
self.logger = logging.getLogger()
|
|
||||||
self.config = airtime_config
|
|
||||||
self.md_manager = AirtimeMetadata()
|
|
||||||
self.wm = wm
|
|
||||||
|
|
||||||
|
|
||||||
def clean_dirty_file_paths(self, dirty_files):
|
|
||||||
""" clean dirty file paths by removing blanks and removing trailing/leading whitespace"""
|
|
||||||
return filter(lambda e: len(e) > 0, [ f.strip(" \n") for f in dirty_files ])
|
|
||||||
|
|
||||||
def find_command(self, directory, extra_arguments=""):
|
|
||||||
""" Builds a find command that respects supported_file_formats list
|
|
||||||
Note: Use single quotes to quote arguments """
|
|
||||||
ext_globs = [ "-iname '*.%s'" % ext for ext in self.supported_file_formats ]
|
|
||||||
find_glob = ' -o '.join(ext_globs)
|
|
||||||
return "find '%s' %s %s" % (directory, find_glob, extra_arguments)
|
|
||||||
|
|
||||||
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):
|
|
||||||
return info[-2].lower() in self.supported_file_formats
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_audio_file(self, filename):
|
|
||||||
info = filename.split(".")
|
|
||||||
if len(info) < 2: return False # handle cases like filename="mp3"
|
|
||||||
return info[-1].lower() in self.supported_file_formats
|
|
||||||
|
|
||||||
#check if file is readable by "nobody"
|
|
||||||
def is_user_readable(self, filepath, euid='nobody', egid='nogroup'):
|
|
||||||
f = None
|
|
||||||
try:
|
|
||||||
uid = pwd.getpwnam(euid)[2]
|
|
||||||
gid = grp.getgrnam(egid)[2]
|
|
||||||
#drop root permissions and become "nobody"
|
|
||||||
os.setegid(gid)
|
|
||||||
os.seteuid(uid)
|
|
||||||
f = open(filepath)
|
|
||||||
readable = True
|
|
||||||
except IOError:
|
|
||||||
self.logger.warn("File does not have correct permissions: '%s'", filepath)
|
|
||||||
readable = False
|
|
||||||
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
|
|
||||||
if f: f.close()
|
|
||||||
os.seteuid(0)
|
|
||||||
os.setegid(0)
|
|
||||||
return readable
|
|
||||||
|
|
||||||
# the function only changes the permission if its not readable by www-data
|
|
||||||
def is_readable(self, item, is_dir):
|
|
||||||
try:
|
|
||||||
return self.is_user_readable(item, 'www-data', 'www-data')
|
|
||||||
except Exception:
|
|
||||||
self.logger.warn(u"Failed to check owner/group/permissions for %s", item)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def make_file_readable(self, pathname, is_dir):
|
|
||||||
if is_dir:
|
|
||||||
#set to 755
|
|
||||||
os.chmod(pathname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
|
||||||
else:
|
|
||||||
#set to 644
|
|
||||||
os.chmod(pathname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
|
|
||||||
|
|
||||||
def make_readable(self, pathname):
|
|
||||||
"""
|
|
||||||
Should only call this function if is_readable() returns False. This function
|
|
||||||
will attempt to make the file world readable by modifying the file's permission's
|
|
||||||
as well as the file's parent directory permissions. We should only call this function
|
|
||||||
on files in Airtime's stor directory, not watched directories!
|
|
||||||
|
|
||||||
Returns True if we were able to make the file world readable. False otherwise.
|
|
||||||
"""
|
|
||||||
original_file = pathname
|
|
||||||
is_dir = False
|
|
||||||
try:
|
|
||||||
while not self.is_readable(original_file, is_dir):
|
|
||||||
#Not readable. Make appropriate permission changes.
|
|
||||||
self.make_file_readable(pathname, is_dir)
|
|
||||||
|
|
||||||
dirname = os.path.dirname(pathname)
|
|
||||||
if dirname == pathname:
|
|
||||||
#most likey reason for this is that we've hit '/'. Avoid infinite loop by terminating loop
|
|
||||||
raise Exception()
|
|
||||||
else:
|
|
||||||
pathname = dirname
|
|
||||||
is_dir = True
|
|
||||||
except Exception:
|
|
||||||
#something went wrong while we were trying to make world readable.
|
|
||||||
return False
|
|
||||||
|
|
||||||
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(u"path %s exists, but it is not a directory!!!", 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)
|
|
||||||
self.cleanup_empty_dirs(os.path.dirname(dir))
|
|
||||||
except Exception:
|
|
||||||
#non-critical exception because we probably tried to delete a non-empty dir.
|
|
||||||
#Don't need to log this, let's just "return"
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#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
|
|
||||||
try:
|
|
||||||
#will be in the format .ext
|
|
||||||
file_ext = os.path.splitext(original_path)[1].lower()
|
|
||||||
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] = u'unknown'
|
|
||||||
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'] == u"Airtime Show Recorder"):
|
|
||||||
#yyyy-mm-dd-hh-MM-ss
|
|
||||||
y = orig_md['MDATA_KEY_YEAR'].split("-")
|
|
||||||
filepath = u'%s/%s/%s/%s/%s-%s-%s%s' % (storage_directory, "recorded", 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'] = os.path.normpath(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'] == u'unknown'):
|
|
||||||
filepath = u'%s/%s/%s/%s/%s-%s%s' % (storage_directory, "imported", md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext)
|
|
||||||
else:
|
|
||||||
filepath = u'%s/%s/%s/%s/%s-%s-%s%s' % (storage_directory, "imported", 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)
|
|
||||||
self.logger.error("traceback: %s", traceback.format_exc())
|
|
||||||
|
|
||||||
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:
|
|
||||||
stdout = None
|
|
||||||
self.logger.error("Could not decode %s using UTF-8" % stdout)
|
|
||||||
|
|
||||||
return stdout
|
|
||||||
|
|
||||||
def scan_dir_for_new_files(self, dir):
|
|
||||||
command = self.find_command(directory=dir, extra_arguments="-type f -readable")
|
|
||||||
self.logger.debug(command)
|
|
||||||
stdout = self.exec_command(command)
|
|
||||||
|
|
||||||
if stdout is None:
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
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, file_md):
|
|
||||||
self.logger.info("Organizing new file: %s", pathname)
|
|
||||||
|
|
||||||
filepath = self.create_file_path(pathname, file_md)
|
|
||||||
|
|
||||||
self.logger.debug(u"Moving from %s to %s", pathname, filepath)
|
|
||||||
self.move_file(pathname, filepath)
|
|
||||||
self.make_readable(filepath)
|
|
||||||
return filepath
|
|
||||||
|
|
||||||
def test_file_playability(self, pathname):
|
|
||||||
#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.
|
|
||||||
#We run the command as pypo because otherwise the target file is opened with write permissions, and this causes an inotify ON_CLOSE_WRITE event
|
|
||||||
#to be fired :/
|
|
||||||
command = "sudo -u pypo airtime-liquidsoap -c 'output.dummy(audio_to_stereo(single(\"%s\")))' > /dev/null 2>&1" % pathname.replace("'", "'\\''")
|
|
||||||
return_code = subprocess.call(command, shell=True)
|
|
||||||
if return_code != 0:
|
|
||||||
#print pathname for py-interpreter.log
|
|
||||||
print pathname
|
|
||||||
return (return_code == 0)
|
|
||||||
|
|
||||||
def move_to_problem_dir(self, source):
|
|
||||||
dest = os.path.join(self.config.problem_directory, os.path.basename(source))
|
|
||||||
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)
|
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
def get_process_output(command):
|
|
||||||
"""
|
|
||||||
Run subprocess and return stdout
|
|
||||||
"""
|
|
||||||
#logger.debug(command)
|
|
||||||
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 duplicate_file(file_path):
|
|
||||||
"""
|
|
||||||
Makes a duplicate of the file and returns the path of this duplicate file.
|
|
||||||
"""
|
|
||||||
fsrc = open(file_path, 'r')
|
|
||||||
fdst = tempfile.NamedTemporaryFile(delete=False)
|
|
||||||
|
|
||||||
#logger.info("Copying %s to %s" % (file_path, fdst.name))
|
|
||||||
|
|
||||||
shutil.copyfileobj(fsrc, fdst)
|
|
||||||
|
|
||||||
fsrc.close()
|
|
||||||
fdst.close()
|
|
||||||
|
|
||||||
return fdst.name
|
|
||||||
|
|
||||||
def get_file_type(file_path):
|
|
||||||
file_type = None
|
|
||||||
if re.search(r'mp3$', file_path, re.IGNORECASE):
|
|
||||||
file_type = 'mp3'
|
|
||||||
elif re.search(r'og(g|a)$', file_path, re.IGNORECASE):
|
|
||||||
file_type = 'vorbis'
|
|
||||||
elif re.search(r'flac$', file_path, re.IGNORECASE):
|
|
||||||
file_type = 'flac'
|
|
||||||
elif re.search(r'(mp4|m4a)$', file_path, re.IGNORECASE):
|
|
||||||
file_type = 'mp4'
|
|
||||||
else:
|
|
||||||
mime_type = get_mime_type(file_path) == "audio/mpeg"
|
|
||||||
if 'mpeg' in mime_type:
|
|
||||||
file_type = 'mp3'
|
|
||||||
elif 'ogg' in mime_type or "oga" in mime_type:
|
|
||||||
file_type = 'vorbis'
|
|
||||||
elif 'flac' in mime_type:
|
|
||||||
file_type = 'flac'
|
|
||||||
elif 'mp4' in mime_type or "m4a" in mime_type:
|
|
||||||
file_type = 'mp4'
|
|
||||||
|
|
||||||
return file_type
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_replay_gain(file_path):
|
|
||||||
"""
|
|
||||||
This function accepts files of type mp3/ogg/flac and returns a calculated
|
|
||||||
ReplayGain value in dB. If the value cannot be calculated for some reason,
|
|
||||||
then we default to 0 (Unity Gain).
|
|
||||||
|
|
||||||
http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
"""
|
|
||||||
Making a duplicate is required because the ReplayGain extraction
|
|
||||||
utilities we use make unwanted modifications to the file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
search = None
|
|
||||||
temp_file_path = duplicate_file(file_path)
|
|
||||||
|
|
||||||
file_type = get_file_type(file_path)
|
|
||||||
|
|
||||||
if file_type:
|
|
||||||
if file_type == 'mp3':
|
|
||||||
if run_process("which mp3gain > /dev/null") == 0:
|
|
||||||
out = get_process_output('mp3gain -q "%s" 2> /dev/null' % temp_file_path)
|
|
||||||
search = re.search(r'Recommended "Track" dB change: (.*)', out)
|
|
||||||
else:
|
|
||||||
logger.warn("mp3gain not found")
|
|
||||||
elif file_type == 'vorbis':
|
|
||||||
if run_process("which vorbisgain > /dev/null && which ogginfo > /dev/null") == 0:
|
|
||||||
run_process('vorbisgain -q -f "%s" 2>/dev/null >/dev/null' % temp_file_path)
|
|
||||||
out = get_process_output('ogginfo "%s"' % temp_file_path)
|
|
||||||
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
|
|
||||||
else:
|
|
||||||
logger.warn("vorbisgain/ogginfo not found")
|
|
||||||
elif file_type == 'flac':
|
|
||||||
if run_process("which metaflac > /dev/null") == 0:
|
|
||||||
out = get_process_output('metaflac --show-tag=REPLAYGAIN_TRACK_GAIN "%s"' % temp_file_path)
|
|
||||||
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
|
|
||||||
else:
|
|
||||||
logger.warn("metaflac not found")
|
|
||||||
elif file_type == 'mp4':
|
|
||||||
if run_process("which aacgain > /dev/null") == 0:
|
|
||||||
out = get_process_output('aacgain -q "%s" 2> /dev/null' % temp_file_path)
|
|
||||||
search = re.search(r'Recommended "Track" dB change: (.*)', out)
|
|
||||||
else:
|
|
||||||
logger.warn("aacgain not found")
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
#no longer need the temp, file simply remove it.
|
|
||||||
os.remove(temp_file_path)
|
|
||||||
except Exception, e:
|
|
||||||
logger.error(str(e))
|
|
||||||
|
|
||||||
replay_gain = 0
|
|
||||||
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])
|
|
|
@ -1,22 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
|
|
||||||
class MediaMonitorWorkerProcess:
|
|
||||||
|
|
||||||
def __init__(self, config, mmc):
|
|
||||||
self.config = config
|
|
||||||
self.mmc = mmc
|
|
||||||
|
|
||||||
#this function is run in its own process, and continuously
|
|
||||||
#checks the queue for any new file events.
|
|
||||||
def process_file_events(self, queue, notifier):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
event = queue.get()
|
|
||||||
notifier.logger.info("received event %s", event)
|
|
||||||
notifier.update_airtime(event)
|
|
||||||
except Exception, e:
|
|
||||||
notifier.logger.error(e)
|
|
||||||
notifier.logger.error("traceback: %s", traceback.format_exc())
|
|
|
@ -1,142 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import time
|
|
||||||
import logging.config
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
import locale
|
|
||||||
|
|
||||||
from configobj import ConfigObj
|
|
||||||
|
|
||||||
from api_clients import api_client as apc
|
|
||||||
from std_err_override import LogWriter
|
|
||||||
|
|
||||||
from multiprocessing import Queue as mpQueue
|
|
||||||
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from pyinotify import WatchManager
|
|
||||||
|
|
||||||
from airtimefilemonitor.airtimenotifier import AirtimeNotifier
|
|
||||||
from airtimefilemonitor.mediamonitorcommon import MediaMonitorCommon
|
|
||||||
from airtimefilemonitor.airtimeprocessevent import AirtimeProcessEvent
|
|
||||||
from airtimefilemonitor.mediaconfig import AirtimeMediaConfig
|
|
||||||
from airtimefilemonitor.workerprocess import MediaMonitorWorkerProcess
|
|
||||||
from airtimefilemonitor.airtimemediamonitorbootstrap import AirtimeMediaMonitorBootstrap
|
|
||||||
|
|
||||||
def configure_locale():
|
|
||||||
logger.debug("Before %s", locale.nl_langinfo(locale.CODESET))
|
|
||||||
current_locale = locale.getlocale()
|
|
||||||
|
|
||||||
if current_locale[1] is None:
|
|
||||||
logger.debug("No locale currently set. Attempting to get default locale.")
|
|
||||||
default_locale = locale.getdefaultlocale()
|
|
||||||
|
|
||||||
if default_locale[1] is None:
|
|
||||||
logger.debug("No default locale exists. Let's try loading from /etc/default/locale")
|
|
||||||
if os.path.exists("/etc/default/locale"):
|
|
||||||
config = ConfigObj('/etc/default/locale')
|
|
||||||
lang = config.get('LANG')
|
|
||||||
new_locale = lang
|
|
||||||
else:
|
|
||||||
logger.error("/etc/default/locale could not be found! Please run 'sudo update-locale' from command-line.")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
new_locale = default_locale
|
|
||||||
|
|
||||||
logger.info("New locale set to: %s", locale.setlocale(locale.LC_ALL, new_locale))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
reload(sys)
|
|
||||||
sys.setdefaultencoding("UTF-8")
|
|
||||||
current_locale_encoding = locale.getlocale()[1].lower()
|
|
||||||
logger.debug("sys default encoding %s", sys.getdefaultencoding())
|
|
||||||
logger.debug("After %s", locale.nl_langinfo(locale.CODESET))
|
|
||||||
|
|
||||||
if current_locale_encoding not in ['utf-8', 'utf8']:
|
|
||||||
logger.error("Need a UTF-8 locale. Currently '%s'. Exiting..." % current_locale_encoding)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# configure logging
|
|
||||||
try:
|
|
||||||
logging.config.fileConfig("%s/logging.cfg" % os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
|
|
||||||
#need to wait for Python 2.7 for this..
|
|
||||||
#logging.captureWarnings(True)
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
LogWriter.override_std_err(logger)
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
print 'Error configuring logging: ', e
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
logger.info("\n\n*** Media Monitor bootup ***\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
configure_locale()
|
|
||||||
|
|
||||||
config = AirtimeMediaConfig(logger)
|
|
||||||
api_client = apc.AirtimeApiClient()
|
|
||||||
api_client.register_component("media-monitor")
|
|
||||||
|
|
||||||
logger.info("Setting up monitor")
|
|
||||||
response = None
|
|
||||||
while response is None:
|
|
||||||
response = api_client.setup_media_monitor()
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
storage_directory = response["stor"]
|
|
||||||
watched_dirs = response["watched_dirs"]
|
|
||||||
logger.info("Storage Directory is: %s", storage_directory)
|
|
||||||
config.storage_directory = os.path.normpath(storage_directory)
|
|
||||||
config.imported_directory = os.path.normpath(os.path.join(storage_directory, 'imported'))
|
|
||||||
config.organize_directory = os.path.normpath(os.path.join(storage_directory, 'organize'))
|
|
||||||
config.recorded_directory = os.path.normpath(os.path.join(storage_directory, 'recorded'))
|
|
||||||
config.problem_directory = os.path.normpath(os.path.join(storage_directory, 'problem_files'))
|
|
||||||
|
|
||||||
dirs = [config.imported_directory, config.organize_directory, config.recorded_directory, config.problem_directory]
|
|
||||||
for d in dirs:
|
|
||||||
if not os.path.exists(d):
|
|
||||||
os.makedirs(d, 02775)
|
|
||||||
|
|
||||||
multi_queue = mpQueue()
|
|
||||||
logger.info("Initializing event processor")
|
|
||||||
|
|
||||||
wm = WatchManager()
|
|
||||||
mmc = MediaMonitorCommon(config, wm=wm)
|
|
||||||
pe = AirtimeProcessEvent(queue=multi_queue, airtime_config=config, wm=wm, mmc=mmc, api_client=api_client)
|
|
||||||
|
|
||||||
bootstrap = AirtimeMediaMonitorBootstrap(logger, pe, api_client, mmc, wm, config)
|
|
||||||
bootstrap.scan()
|
|
||||||
|
|
||||||
notifier = AirtimeNotifier(wm, pe, read_freq=0, timeout=0, airtime_config=config, api_client=api_client, bootstrap=bootstrap, mmc=mmc)
|
|
||||||
notifier.coalesce_events()
|
|
||||||
|
|
||||||
#create 5 worker threads
|
|
||||||
wp = MediaMonitorWorkerProcess(config, mmc)
|
|
||||||
for i in range(5):
|
|
||||||
threadName = "Thread #%d" % i
|
|
||||||
t = Thread(target=wp.process_file_events, name=threadName, args=(multi_queue, notifier))
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
wdd = notifier.watch_directory(storage_directory)
|
|
||||||
logger.info("Added watch to %s", storage_directory)
|
|
||||||
logger.info("wdd result %s", wdd[storage_directory])
|
|
||||||
|
|
||||||
for dir in watched_dirs:
|
|
||||||
wdd = notifier.watch_directory(dir)
|
|
||||||
logger.info("Added watch to %s", dir)
|
|
||||||
logger.info("wdd result %s", wdd[dir])
|
|
||||||
|
|
||||||
notifier.loop(callback=pe.notifier_loop_callback)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
notifier.stop()
|
|
||||||
logger.info("Keyboard Interrupt")
|
|
||||||
except Exception, e:
|
|
||||||
logger.error('Exception: %s', e)
|
|
||||||
logger.error("traceback: %s", traceback.format_exc())
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
from media.saas.launcher import setup_logger, setup_global, MM2
|
|
||||||
from media.saas.airtimeinstance import AirtimeInstance
|
|
||||||
from os.path import isdir, join, abspath, exists, dirname
|
|
||||||
from os import listdir
|
|
||||||
|
|
||||||
def list_dirs(d): return (x for x in listdir(d) if isdir(join(d,x)))
|
|
||||||
|
|
||||||
def filter_instance(d): return bool(re.match('.+\d+$',d))
|
|
||||||
|
|
||||||
def get_name(p): return re.match('.+/(\d+)$',p).group(1)
|
|
||||||
|
|
||||||
def filter_instances(l): return (x for x in l if filter_instance(x))
|
|
||||||
|
|
||||||
def autoscan_instances(main_cfg):
|
|
||||||
root = main_cfg['instance_root']
|
|
||||||
instances = []
|
|
||||||
for instance_machine in list_dirs(root):
|
|
||||||
instance_machine = join(root, instance_machine)
|
|
||||||
for instance_root in filter_instances(list_dirs(instance_machine)):
|
|
||||||
full_path = abspath(join(instance_machine,instance_root))
|
|
||||||
ai = AirtimeInstance.root_make(get_name(full_path), full_path)
|
|
||||||
instances.append(ai)
|
|
||||||
return instances
|
|
||||||
|
|
||||||
def verify_exists(p):
|
|
||||||
if not exists(p): raise Exception("%s must exist" % p)
|
|
||||||
|
|
||||||
def main(main_cfg):
|
|
||||||
log_config, log_path = main_cfg['log_config'], main_cfg['log_path']
|
|
||||||
verify_exists(log_config)
|
|
||||||
log = setup_logger(log_config, log_path)
|
|
||||||
setup_global(log)
|
|
||||||
for instance in autoscan_instances(main_cfg):
|
|
||||||
print("Launching instance: %s" % str(instance))
|
|
||||||
#MM2(instance).start()
|
|
||||||
print("Launched all instances")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pwd = dirname(__file__)
|
|
||||||
default = {
|
|
||||||
'log_path' : join(pwd, 'test.log'), # config for log
|
|
||||||
'log_config' : join(pwd, 'configs/logging.cfg'), # where to log
|
|
||||||
# root dir of all instances
|
|
||||||
'instance_root' : '/mnt/airtimepro/instances'
|
|
||||||
}
|
|
||||||
main(default)
|
|
|
@ -1,32 +0,0 @@
|
||||||
[loggers]
|
|
||||||
keys= root,notifier,metadata
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys=fileOutHandler
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys=simpleFormatter
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level=DEBUG
|
|
||||||
handlers=fileOutHandler
|
|
||||||
|
|
||||||
[logger_notifier]
|
|
||||||
level=DEBUG
|
|
||||||
handlers=fileOutHandler
|
|
||||||
qualname=notifier
|
|
||||||
|
|
||||||
[logger_metadata]
|
|
||||||
level=DEBUG
|
|
||||||
handlers=fileOutHandler
|
|
||||||
qualname=metadata
|
|
||||||
|
|
||||||
[handler_fileOutHandler]
|
|
||||||
class=logging.handlers.RotatingFileHandler
|
|
||||||
level=DEBUG
|
|
||||||
formatter=simpleFormatter
|
|
||||||
args=("/var/log/airtime/media-monitor/media-monitor.log", 'a', 10000000, 5,)
|
|
||||||
|
|
||||||
[formatter_simpleFormatter]
|
|
||||||
format=%(asctime)s %(levelname)s - [%(threadName)s] [%(filename)s : %(funcName)s()] : LINE %(lineno)d - %(message)s
|
|
||||||
datefmt=
|
|
|
@ -1,158 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import process as md
|
|
||||||
import re
|
|
||||||
from os.path import normpath
|
|
||||||
from ..monitor.pure import format_length, file_md5, is_airtime_recorded, \
|
|
||||||
no_extension_basename
|
|
||||||
|
|
||||||
defs_loaded = False
|
|
||||||
|
|
||||||
MAX_SIGNED_INT = 2**31-1
|
|
||||||
|
|
||||||
def is_defs_loaded():
|
|
||||||
global defs_loaded
|
|
||||||
return defs_loaded
|
|
||||||
|
|
||||||
def load_definitions():
|
|
||||||
with md.metadata('MDATA_KEY_DURATION') as t:
|
|
||||||
t.default(u'0.0')
|
|
||||||
t.depends('length')
|
|
||||||
t.translate(lambda k: format_length(k['length']))
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_CUE_IN') as t:
|
|
||||||
t.default(u'0.0')
|
|
||||||
t.depends('cuein')
|
|
||||||
t.translate(lambda k: format_length(k['cuein']))
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_CUE_OUT') as t:
|
|
||||||
t.default(u'0.0')
|
|
||||||
t.depends('cueout')
|
|
||||||
t.translate(lambda k: format_length(k['cueout']))
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_MIME') as t:
|
|
||||||
t.default(u'')
|
|
||||||
t.depends('mime')
|
|
||||||
# Is this necessary?
|
|
||||||
t.translate(lambda k: k['mime'].replace('audio/vorbis','audio/ogg'))
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_BITRATE') as t:
|
|
||||||
t.default(u'')
|
|
||||||
t.depends('bitrate')
|
|
||||||
t.translate(lambda k: k['bitrate'])
|
|
||||||
t.max_value(MAX_SIGNED_INT)
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_SAMPLERATE') as t:
|
|
||||||
t.default(u'0')
|
|
||||||
t.depends('sample_rate')
|
|
||||||
t.translate(lambda k: k['sample_rate'])
|
|
||||||
t.max_value(MAX_SIGNED_INT)
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_FTYPE') as t:
|
|
||||||
t.depends('ftype') # i don't think this field even exists
|
|
||||||
t.default(u'audioclip')
|
|
||||||
t.translate(lambda k: k['ftype']) # but just in case
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_CREATOR") as t:
|
|
||||||
t.depends("artist")
|
|
||||||
# A little kludge to make sure that we have some value for when we parse
|
|
||||||
# MDATA_KEY_TITLE
|
|
||||||
t.default(u"")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_SOURCE") as t:
|
|
||||||
t.depends("album")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_GENRE") as t:
|
|
||||||
t.depends("genre")
|
|
||||||
t.max_length(64)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_MOOD") as t:
|
|
||||||
t.depends("mood")
|
|
||||||
t.max_length(64)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_TRACKNUMBER") as t:
|
|
||||||
t.depends("tracknumber")
|
|
||||||
t.max_value(MAX_SIGNED_INT)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_BPM") as t:
|
|
||||||
t.depends("bpm")
|
|
||||||
t.max_value(MAX_SIGNED_INT)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_LABEL") as t:
|
|
||||||
t.depends("organization")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_COMPOSER") as t:
|
|
||||||
t.depends("composer")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_ENCODER") as t:
|
|
||||||
t.depends("encodedby")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_CONDUCTOR") as t:
|
|
||||||
t.depends("conductor")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_YEAR") as t:
|
|
||||||
t.depends("date")
|
|
||||||
t.max_length(16)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_URL") as t:
|
|
||||||
t.depends("website")
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_ISRC") as t:
|
|
||||||
t.depends("isrc")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_COPYRIGHT") as t:
|
|
||||||
t.depends("copyright")
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_ORIGINAL_PATH") as t:
|
|
||||||
t.depends('path')
|
|
||||||
t.translate(lambda k: unicode(normpath(k['path'])))
|
|
||||||
|
|
||||||
with md.metadata("MDATA_KEY_MD5") as t:
|
|
||||||
t.depends('path')
|
|
||||||
t.optional(False)
|
|
||||||
t.translate(lambda k: file_md5(k['path'], max_length=100))
|
|
||||||
|
|
||||||
# owner is handled differently by (by events.py)
|
|
||||||
|
|
||||||
# MDATA_KEY_TITLE is the annoying special case b/c we sometimes read it
|
|
||||||
# from file name
|
|
||||||
|
|
||||||
|
|
||||||
# must handle 3 cases:
|
|
||||||
# 1. regular case (not recorded + title is present)
|
|
||||||
# 2. title is absent (read from file)
|
|
||||||
# 3. recorded file
|
|
||||||
def tr_title(k):
|
|
||||||
#unicode_unknown = u"unknown"
|
|
||||||
new_title = u""
|
|
||||||
if is_airtime_recorded(k) or k['title'] != u"":
|
|
||||||
new_title = k['title']
|
|
||||||
else:
|
|
||||||
default_title = no_extension_basename(k['path'])
|
|
||||||
default_title = re.sub(r'__\d+\.',u'.', default_title)
|
|
||||||
|
|
||||||
# format is: track_number-title-123kbps.mp3
|
|
||||||
m = re.match(".+?-(?P<title>.+)-(\d+kbps|unknown)$", default_title)
|
|
||||||
if m: new_title = m.group('title')
|
|
||||||
else: new_title = re.sub(r'-\d+kbps$', u'', default_title)
|
|
||||||
|
|
||||||
return new_title
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_TITLE') as t:
|
|
||||||
# Need to know MDATA_KEY_CREATOR to know if show was recorded. Value is
|
|
||||||
# defaulted to "" from definitions above
|
|
||||||
t.depends('title','MDATA_KEY_CREATOR','path')
|
|
||||||
t.optional(False)
|
|
||||||
t.translate(tr_title)
|
|
||||||
t.max_length(512)
|
|
||||||
|
|
||||||
with md.metadata('MDATA_KEY_LABEL') as t:
|
|
||||||
t.depends('label')
|
|
||||||
t.max_length(512)
|
|
|
@ -1,237 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from ..monitor.pure import truncate_to_value, truncate_to_length, toposort
|
|
||||||
from os.path import normpath
|
|
||||||
from ..monitor.exceptions import BadSongFile
|
|
||||||
from ..monitor.log import Loggable
|
|
||||||
from ..monitor import pure as mmp
|
|
||||||
from collections import namedtuple
|
|
||||||
import mutagen
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
class FakeMutagen(dict):
|
|
||||||
"""
|
|
||||||
Need this fake mutagen object so that airtime_special functions
|
|
||||||
return a proper default value instead of throwing an exceptions for
|
|
||||||
files that mutagen doesn't recognize
|
|
||||||
"""
|
|
||||||
FakeInfo = namedtuple('FakeInfo','length bitrate')
|
|
||||||
def __init__(self,path):
|
|
||||||
self.path = path
|
|
||||||
self.mime = ['audio/wav']
|
|
||||||
self.info = FakeMutagen.FakeInfo(0.0, '')
|
|
||||||
dict.__init__(self)
|
|
||||||
def set_length(self,l):
|
|
||||||
old_bitrate = self.info.bitrate
|
|
||||||
self.info = FakeMutagen.FakeInfo(l, old_bitrate)
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataAbsent(Exception):
|
|
||||||
def __init__(self, name): self.name = name
|
|
||||||
def __str__(self): return "Could not obtain element '%s'" % self.name
|
|
||||||
|
|
||||||
class MetadataElement(Loggable):
|
|
||||||
|
|
||||||
def __init__(self,name):
|
|
||||||
self.name = name
|
|
||||||
# "Sane" defaults
|
|
||||||
self.__deps = set()
|
|
||||||
self.__normalizer = lambda x: x
|
|
||||||
self.__optional = True
|
|
||||||
self.__default = None
|
|
||||||
self.__is_normalized = lambda _ : True
|
|
||||||
self.__max_length = -1
|
|
||||||
self.__max_value = -1
|
|
||||||
self.__translator = None
|
|
||||||
|
|
||||||
def max_length(self,l):
|
|
||||||
self.__max_length = l
|
|
||||||
|
|
||||||
def max_value(self,v):
|
|
||||||
self.__max_value = v
|
|
||||||
|
|
||||||
def optional(self, setting):
|
|
||||||
self.__optional = setting
|
|
||||||
|
|
||||||
def is_optional(self):
|
|
||||||
return self.__optional
|
|
||||||
|
|
||||||
def depends(self, *deps):
|
|
||||||
self.__deps = set(deps)
|
|
||||||
|
|
||||||
def dependencies(self):
|
|
||||||
return self.__deps
|
|
||||||
|
|
||||||
def translate(self, f):
|
|
||||||
self.__translator = f
|
|
||||||
|
|
||||||
def is_normalized(self, f):
|
|
||||||
self.__is_normalized = f
|
|
||||||
|
|
||||||
def normalize(self, f):
|
|
||||||
self.__normalizer = f
|
|
||||||
|
|
||||||
def default(self,v):
|
|
||||||
self.__default = v
|
|
||||||
|
|
||||||
def get_default(self):
|
|
||||||
if hasattr(self.__default, '__call__'): return self.__default()
|
|
||||||
else: return self.__default
|
|
||||||
|
|
||||||
def has_default(self):
|
|
||||||
return self.__default is not None
|
|
||||||
|
|
||||||
def path(self):
|
|
||||||
return self.__path
|
|
||||||
|
|
||||||
def __slice_deps(self, d):
|
|
||||||
"""
|
|
||||||
returns a dictionary of all the key value pairs in d that are also
|
|
||||||
present in self.__deps
|
|
||||||
"""
|
|
||||||
return dict( (k,v) for k,v in d.iteritems() if k in self.__deps)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s(%s)" % (self.name, ' '.join(list(self.__deps)))
|
|
||||||
|
|
||||||
def read_value(self, path, original, running={}):
|
|
||||||
|
|
||||||
# If value is present and normalized then we only check if it's
|
|
||||||
# normalized or not. We normalize if it's not normalized already
|
|
||||||
|
|
||||||
if self.name in original:
|
|
||||||
v = original[self.name]
|
|
||||||
if self.__is_normalized(v): return v
|
|
||||||
else: return self.__normalizer(v)
|
|
||||||
|
|
||||||
# We slice out only the dependencies that are required for the metadata
|
|
||||||
# element.
|
|
||||||
dep_slice_orig = self.__slice_deps(original)
|
|
||||||
dep_slice_running = self.__slice_deps(running)
|
|
||||||
# TODO : remove this later
|
|
||||||
dep_slice_special = self.__slice_deps({'path' : path})
|
|
||||||
# We combine all required dependencies into a single dictionary
|
|
||||||
# that we will pass to the translator
|
|
||||||
full_deps = dict( dep_slice_orig.items()
|
|
||||||
+ dep_slice_running.items()
|
|
||||||
+ dep_slice_special.items())
|
|
||||||
|
|
||||||
# check if any dependencies are absent
|
|
||||||
# note: there is no point checking the case that len(full_deps) >
|
|
||||||
# len(self.__deps) because we make sure to "slice out" any supefluous
|
|
||||||
# dependencies above.
|
|
||||||
if len(full_deps) != len(self.dependencies()) or \
|
|
||||||
len(self.dependencies()) == 0:
|
|
||||||
# If we have a default value then use that. Otherwise throw an
|
|
||||||
# exception
|
|
||||||
if self.has_default(): return self.get_default()
|
|
||||||
else: raise MetadataAbsent(self.name)
|
|
||||||
|
|
||||||
# We have all dependencies. Now for actual for parsing
|
|
||||||
def def_translate(dep):
|
|
||||||
def wrap(k):
|
|
||||||
e = [ x for x in dep ][0]
|
|
||||||
return k[e]
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
# Only case where we can select a default translator
|
|
||||||
if self.__translator is None:
|
|
||||||
self.translate(def_translate(self.dependencies()))
|
|
||||||
if len(self.dependencies()) > 2: # dependencies include themselves
|
|
||||||
self.logger.info("Ignoring some dependencies in translate %s"
|
|
||||||
% self.name)
|
|
||||||
self.logger.info(self.dependencies())
|
|
||||||
|
|
||||||
r = self.__normalizer( self.__translator(full_deps) )
|
|
||||||
if self.__max_length != -1:
|
|
||||||
r = truncate_to_length(r, self.__max_length)
|
|
||||||
if self.__max_value != -1:
|
|
||||||
try: r = truncate_to_value(r, self.__max_value)
|
|
||||||
except ValueError, e: r = ''
|
|
||||||
return r
|
|
||||||
|
|
||||||
def normalize_mutagen(path):
|
|
||||||
"""
|
|
||||||
Consumes a path and reads the metadata using mutagen. normalizes some of
|
|
||||||
the metadata that isn't read through the mutagen hash
|
|
||||||
"""
|
|
||||||
if not mmp.file_playable(path): raise BadSongFile(path)
|
|
||||||
try : m = mutagen.File(path, easy=True)
|
|
||||||
except Exception : raise BadSongFile(path)
|
|
||||||
if m is None: m = FakeMutagen(path)
|
|
||||||
try:
|
|
||||||
if mmp.extension(path) == 'wav':
|
|
||||||
m.set_length(mmp.read_wave_duration(path))
|
|
||||||
except Exception: raise BadSongFile(path)
|
|
||||||
md = {}
|
|
||||||
for k,v in m.iteritems():
|
|
||||||
if type(v) is list:
|
|
||||||
if len(v) > 0: md[k] = v[0]
|
|
||||||
else: md[k] = v
|
|
||||||
# populate special metadata values
|
|
||||||
md['length'] = getattr(m.info, 'length', 0.0)
|
|
||||||
md['bitrate'] = getattr(m.info, 'bitrate', u'')
|
|
||||||
md['sample_rate'] = getattr(m.info, 'sample_rate', 0)
|
|
||||||
md['mime'] = m.mime[0] if len(m.mime) > 0 else u''
|
|
||||||
md['path'] = normpath(path)
|
|
||||||
|
|
||||||
# silence detect(set default cue in and out)
|
|
||||||
#try:
|
|
||||||
#command = ['silan', '-b', '-f', 'JSON', md['path']]
|
|
||||||
#proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
|
||||||
#out = proc.communicate()[0].strip('\r\n')
|
|
||||||
|
|
||||||
#info = json.loads(out)
|
|
||||||
#md['cuein'] = info['sound'][0][0]
|
|
||||||
#md['cueout'] = info['sound'][0][1]
|
|
||||||
#except Exception:
|
|
||||||
#self.logger.debug('silan is missing')
|
|
||||||
|
|
||||||
if 'title' not in md: md['title'] = u''
|
|
||||||
return md
|
|
||||||
|
|
||||||
|
|
||||||
class OverwriteMetadataElement(Exception):
|
|
||||||
def __init__(self, m): self.m = m
|
|
||||||
def __str__(self): return "Trying to overwrite: %s" % self.m
|
|
||||||
|
|
||||||
class MetadataReader(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.clear()
|
|
||||||
|
|
||||||
def register_metadata(self,m):
|
|
||||||
if m in self.__mdata_name_map:
|
|
||||||
raise OverwriteMetadataElement(m)
|
|
||||||
self.__mdata_name_map[m.name] = m
|
|
||||||
d = dict( (name,m.dependencies()) for name,m in
|
|
||||||
self.__mdata_name_map.iteritems() )
|
|
||||||
new_list = list( toposort(d) )
|
|
||||||
self.__metadata = [ self.__mdata_name_map[name] for name in new_list
|
|
||||||
if name in self.__mdata_name_map]
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.__mdata_name_map = {}
|
|
||||||
self.__metadata = []
|
|
||||||
|
|
||||||
def read(self, path, muta_hash):
|
|
||||||
normalized_metadata = {}
|
|
||||||
for mdata in self.__metadata:
|
|
||||||
try:
|
|
||||||
normalized_metadata[mdata.name] = mdata.read_value(
|
|
||||||
path, muta_hash, normalized_metadata)
|
|
||||||
except MetadataAbsent:
|
|
||||||
if not mdata.is_optional(): raise
|
|
||||||
return normalized_metadata
|
|
||||||
|
|
||||||
def read_mutagen(self, path):
|
|
||||||
return self.read(path, normalize_mutagen(path))
|
|
||||||
|
|
||||||
global_reader = MetadataReader()
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def metadata(name):
|
|
||||||
t = MetadataElement(name)
|
|
||||||
yield t
|
|
||||||
global_reader.register_metadata(t)
|
|
|
@ -1,215 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from kombu.messaging import Exchange, Queue, Consumer
|
|
||||||
from kombu.connection import BrokerConnection
|
|
||||||
from kombu.simple import SimpleQueue
|
|
||||||
from os.path import normpath
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import copy
|
|
||||||
import time
|
|
||||||
|
|
||||||
from exceptions import BadSongFile, InvalidMetadataElement, DirectoryIsNotListed
|
|
||||||
from metadata import Metadata
|
|
||||||
from log import Loggable
|
|
||||||
from syncdb import AirtimeDB
|
|
||||||
from bootstrap import Bootstrapper
|
|
||||||
|
|
||||||
from ..saas.thread import apc, user
|
|
||||||
|
|
||||||
class AirtimeNotifier(Loggable):
|
|
||||||
"""
|
|
||||||
AirtimeNotifier is responsible for interecepting RabbitMQ messages and
|
|
||||||
feeding them to the event_handler object it was initialized with. The only
|
|
||||||
thing it does to the messages is parse them from json
|
|
||||||
"""
|
|
||||||
def __init__(self, cfg, message_receiver):
|
|
||||||
self.cfg = cfg
|
|
||||||
self.handler = message_receiver
|
|
||||||
while not self.init_rabbit_mq():
|
|
||||||
self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
def init_rabbit_mq(self):
|
|
||||||
try:
|
|
||||||
self.logger.info("Initializing RabbitMQ message consumer...")
|
|
||||||
schedule_exchange = Exchange("airtime-media-monitor", "direct",
|
|
||||||
durable=True, auto_delete=True)
|
|
||||||
schedule_queue = Queue("media-monitor", exchange=schedule_exchange,
|
|
||||||
key="filesystem")
|
|
||||||
self.connection = BrokerConnection(self.cfg["rabbitmq"]["host"],
|
|
||||||
self.cfg["rabbitmq"]["user"], self.cfg["rabbitmq"]["password"],
|
|
||||||
self.cfg["rabbitmq"]["vhost"])
|
|
||||||
channel = self.connection.channel()
|
|
||||||
|
|
||||||
self.simple_queue = SimpleQueue(channel, schedule_queue)
|
|
||||||
|
|
||||||
self.logger.info("Initialized RabbitMQ consumer.")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.info("Failed to initialize RabbitMQ consumer")
|
|
||||||
self.logger.error(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def handle_message(self, message):
|
|
||||||
"""
|
|
||||||
Messages received from RabbitMQ are handled here. These messages
|
|
||||||
instruct media-monitor of events such as a new directory being watched,
|
|
||||||
file metadata has been changed, or any other changes to the config of
|
|
||||||
media-monitor via the web UI.
|
|
||||||
"""
|
|
||||||
self.logger.info("Received md from RabbitMQ: %s" % str(message))
|
|
||||||
m = json.loads(message)
|
|
||||||
# TODO : normalize any other keys that could be used to pass
|
|
||||||
# directories
|
|
||||||
if 'directory' in m: m['directory'] = normpath(m['directory'])
|
|
||||||
self.handler.message(m)
|
|
||||||
|
|
||||||
class AirtimeMessageReceiver(Loggable):
|
|
||||||
def __init__(self, cfg, manager):
|
|
||||||
self.dispatch_table = {
|
|
||||||
'md_update' : self.md_update,
|
|
||||||
'new_watch' : self.new_watch,
|
|
||||||
'remove_watch' : self.remove_watch,
|
|
||||||
'rescan_watch' : self.rescan_watch,
|
|
||||||
'change_stor' : self.change_storage,
|
|
||||||
'file_delete' : self.file_delete,
|
|
||||||
}
|
|
||||||
self.cfg = cfg
|
|
||||||
self.manager = manager
|
|
||||||
|
|
||||||
def message(self, msg):
|
|
||||||
"""
|
|
||||||
This method is called by an AirtimeNotifier instance that
|
|
||||||
consumes the Rabbit MQ events that trigger this. The method
|
|
||||||
return true when the event was executed and false when it wasn't.
|
|
||||||
"""
|
|
||||||
msg = copy.deepcopy(msg)
|
|
||||||
if msg['event_type'] in self.dispatch_table:
|
|
||||||
evt = msg['event_type']
|
|
||||||
del msg['event_type']
|
|
||||||
self.logger.info("Handling RabbitMQ message: '%s'" % evt)
|
|
||||||
self._execute_message(evt,msg)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.logger.info("Received invalid message with 'event_type': '%s'"
|
|
||||||
% msg['event_type'])
|
|
||||||
self.logger.info("Message details: %s" % str(msg))
|
|
||||||
return False
|
|
||||||
def _execute_message(self,evt,message):
|
|
||||||
self.dispatch_table[evt](message)
|
|
||||||
|
|
||||||
def __request_now_bootstrap(self, directory_id=None, directory=None,
|
|
||||||
all_files=True):
|
|
||||||
if (not directory_id) and (not directory):
|
|
||||||
raise ValueError("You must provide either directory_id or \
|
|
||||||
directory")
|
|
||||||
sdb = AirtimeDB(apc())
|
|
||||||
if directory : directory = os.path.normpath(directory)
|
|
||||||
if directory_id == None : directory_id = sdb.to_id(directory)
|
|
||||||
if directory == None : directory = sdb.to_directory(directory_id)
|
|
||||||
try:
|
|
||||||
bs = Bootstrapper( sdb, self.manager.watch_signal() )
|
|
||||||
bs.flush_watch( directory=directory, last_ran=self.cfg.last_ran() )
|
|
||||||
except Exception as e:
|
|
||||||
self.fatal_exception("Exception bootstrapping: (dir,id)=(%s,%s)" %
|
|
||||||
(directory, directory_id), e)
|
|
||||||
raise DirectoryIsNotListed(directory, cause=e)
|
|
||||||
|
|
||||||
def md_update(self, msg):
|
|
||||||
self.logger.info("Updating metadata for: '%s'" %
|
|
||||||
msg['MDATA_KEY_FILEPATH'])
|
|
||||||
md_path = msg['MDATA_KEY_FILEPATH']
|
|
||||||
try: Metadata.write_unsafe(path=md_path, md=msg)
|
|
||||||
except BadSongFile as e:
|
|
||||||
self.logger.info("Cannot find metadata file: '%s'" % e.path)
|
|
||||||
except InvalidMetadataElement as e:
|
|
||||||
self.logger.info("Metadata instance not supported for this file '%s'" \
|
|
||||||
% e.path)
|
|
||||||
self.logger.info(str(e))
|
|
||||||
except Exception as e:
|
|
||||||
# TODO : add md_path to problem path or something?
|
|
||||||
self.fatal_exception("Unknown error when writing metadata to: '%s'"
|
|
||||||
% md_path, e)
|
|
||||||
|
|
||||||
def new_watch(self, msg, restart=False):
|
|
||||||
msg['directory'] = normpath(msg['directory'])
|
|
||||||
self.logger.info("Creating watch for directory: '%s'" %
|
|
||||||
msg['directory'])
|
|
||||||
if not os.path.exists(msg['directory']):
|
|
||||||
try: os.makedirs(msg['directory'])
|
|
||||||
except Exception as e:
|
|
||||||
self.fatal_exception("Failed to create watched dir '%s'" %
|
|
||||||
msg['directory'],e)
|
|
||||||
else:
|
|
||||||
self.logger.info("Created new watch directory: '%s'" %
|
|
||||||
msg['directory'])
|
|
||||||
self.new_watch(msg)
|
|
||||||
else:
|
|
||||||
self.__request_now_bootstrap( directory=msg['directory'],
|
|
||||||
all_files=restart)
|
|
||||||
self.manager.add_watch_directory(msg['directory'])
|
|
||||||
|
|
||||||
def remove_watch(self, msg):
|
|
||||||
msg['directory'] = normpath(msg['directory'])
|
|
||||||
self.logger.info("Removing watch from directory: '%s'" %
|
|
||||||
msg['directory'])
|
|
||||||
self.manager.remove_watch_directory(msg['directory'])
|
|
||||||
|
|
||||||
def rescan_watch(self, msg):
|
|
||||||
self.logger.info("Trying to rescan watched directory: '%s'" %
|
|
||||||
msg['directory'])
|
|
||||||
try:
|
|
||||||
# id is always an integer but in the dictionary the key is always a
|
|
||||||
# string
|
|
||||||
self.__request_now_bootstrap( unicode(msg['id']) )
|
|
||||||
except DirectoryIsNotListed as e:
|
|
||||||
self.fatal_exception("Bad rescan request", e)
|
|
||||||
except Exception as e:
|
|
||||||
self.fatal_exception("Bad rescan request. Unknown error.", e)
|
|
||||||
else:
|
|
||||||
self.logger.info("Successfully re-scanned: '%s'" % msg['directory'])
|
|
||||||
|
|
||||||
def change_storage(self, msg):
|
|
||||||
new_storage_directory = msg['directory']
|
|
||||||
self.manager.change_storage_root(new_storage_directory)
|
|
||||||
for to_bootstrap in [ self.manager.get_recorded_path(),
|
|
||||||
self.manager.get_imported_path() ]:
|
|
||||||
self.__request_now_bootstrap( directory=to_bootstrap )
|
|
||||||
|
|
||||||
def file_delete(self, msg):
|
|
||||||
# Deletes should be requested only from imported folder but we
|
|
||||||
# don't verify that. Security risk perhaps?
|
|
||||||
# we only delete if we are passed the special delete flag that is
|
|
||||||
# necessary with every "delete_file" request
|
|
||||||
if not msg['delete']:
|
|
||||||
self.logger.info("No clippy confirmation, ignoring event. \
|
|
||||||
Out of curiousity we will print some details.")
|
|
||||||
self.logger.info(msg)
|
|
||||||
return
|
|
||||||
# TODO : Add validation that we are deleting a file that's under our
|
|
||||||
# surveillance. We don't to delete some random system file.
|
|
||||||
if os.path.exists(msg['filepath']):
|
|
||||||
try:
|
|
||||||
self.logger.info("Attempting to delete '%s'" %
|
|
||||||
msg['filepath'])
|
|
||||||
# We use FileMediator to ignore any paths with
|
|
||||||
# msg['filepath'] so that we do not send a duplicate delete
|
|
||||||
# request that we'd normally get form pyinotify. But right
|
|
||||||
# now event contractor would take care of this sort of
|
|
||||||
# thing anyway so this might not be necessary after all
|
|
||||||
#user().file_mediator.ignore(msg['filepath'])
|
|
||||||
os.unlink(msg['filepath'])
|
|
||||||
# Verify deletion:
|
|
||||||
if not os.path.exists(msg['filepath']):
|
|
||||||
self.logger.info("Successfully deleted: '%s'" %
|
|
||||||
msg['filepath'])
|
|
||||||
except Exception as e:
|
|
||||||
self.fatal_exception("Failed to delete '%s'" % msg['filepath'],
|
|
||||||
e)
|
|
||||||
else: # validation for filepath existence failed
|
|
||||||
self.logger.info("Attempting to delete file '%s' that does not \
|
|
||||||
exist. Full request:" % msg['filepath'])
|
|
||||||
self.logger.info(msg)
|
|
|
@ -1,63 +0,0 @@
|
||||||
import os
|
|
||||||
from pydispatch import dispatcher
|
|
||||||
from events import NewFile, DeleteFile, ModifyFile
|
|
||||||
from log import Loggable
|
|
||||||
from ..saas.thread import getsig
|
|
||||||
import pure as mmp
|
|
||||||
|
|
||||||
class Bootstrapper(Loggable):
|
|
||||||
"""
|
|
||||||
Bootstrapper reads all the info in the filesystem flushes organize events
|
|
||||||
and watch events
|
|
||||||
"""
|
|
||||||
def __init__(self,db,watch_signal):
|
|
||||||
"""
|
|
||||||
db - AirtimeDB object; small layer over api client
|
|
||||||
last_ran - last time the program was ran.
|
|
||||||
watch_signal - the signals should send events for every file on.
|
|
||||||
"""
|
|
||||||
self.db = db
|
|
||||||
self.watch_signal = getsig(watch_signal)
|
|
||||||
|
|
||||||
def flush_all(self, last_ran):
|
|
||||||
"""
|
|
||||||
bootstrap every single watched directory. only useful at startup note
|
|
||||||
that because of the way list_directories works we also flush the import
|
|
||||||
directory as well I think
|
|
||||||
"""
|
|
||||||
for d in self.db.list_storable_paths(): self.flush_watch(d, last_ran)
|
|
||||||
|
|
||||||
def flush_watch(self, directory, last_ran, all_files=False):
|
|
||||||
"""
|
|
||||||
flush a single watch/imported directory. useful when wanting to to
|
|
||||||
rescan, or add a watched/imported directory
|
|
||||||
"""
|
|
||||||
songs = set([])
|
|
||||||
added = modded = deleted = 0
|
|
||||||
for f in mmp.walk_supported(directory, clean_empties=False):
|
|
||||||
songs.add(f)
|
|
||||||
# We decide whether to update a file's metadata by checking its
|
|
||||||
# system modification date. If it's above the value self.last_ran
|
|
||||||
# which is passed to us that means media monitor wasn't aware when
|
|
||||||
# this changes occured in the filesystem hence it will send the
|
|
||||||
# correct events to sync the database with the filesystem
|
|
||||||
if os.path.getmtime(f) > last_ran:
|
|
||||||
modded += 1
|
|
||||||
dispatcher.send(signal=self.watch_signal, sender=self,
|
|
||||||
event=ModifyFile(f))
|
|
||||||
db_songs = set(( song for song in self.db.directory_get_files(directory,
|
|
||||||
all_files)
|
|
||||||
if mmp.sub_path(directory,song) ))
|
|
||||||
# Get all the files that are in the database but in the file
|
|
||||||
# system. These are the files marked for deletions
|
|
||||||
for to_delete in db_songs.difference(songs):
|
|
||||||
dispatcher.send(signal=self.watch_signal, sender=self,
|
|
||||||
event=DeleteFile(to_delete))
|
|
||||||
deleted += 1
|
|
||||||
for to_add in songs.difference(db_songs):
|
|
||||||
dispatcher.send(signal=self.watch_signal, sender=self,
|
|
||||||
event=NewFile(to_add))
|
|
||||||
added += 1
|
|
||||||
self.logger.info( "Flushed watch directory (%s). \
|
|
||||||
(added, modified, deleted) = (%d, %d, %d)"
|
|
||||||
% (directory, added, modded, deleted) )
|
|
|
@ -1,32 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import copy
|
|
||||||
from configobj import ConfigObj
|
|
||||||
|
|
||||||
from exceptions import NoConfigFile, ConfigAccessViolation
|
|
||||||
import pure as mmp
|
|
||||||
|
|
||||||
class MMConfig(object):
|
|
||||||
def __init__(self, path):
|
|
||||||
if not os.path.exists(path): raise NoConfigFile(path)
|
|
||||||
self.cfg = ConfigObj(path)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
""" We always return a copy of the config item to prevent
|
|
||||||
callers from doing any modifications through the returned
|
|
||||||
objects methods """
|
|
||||||
return copy.deepcopy(self.cfg[key])
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
""" We use this method not to allow anybody to mess around with
|
|
||||||
config file any settings made should be done through MMConfig's
|
|
||||||
instance methods """
|
|
||||||
raise ConfigAccessViolation(key)
|
|
||||||
|
|
||||||
def save(self): self.cfg.write()
|
|
||||||
|
|
||||||
def last_ran(self):
|
|
||||||
""" Returns the last time media monitor was ran by looking at
|
|
||||||
the time when the file at 'index_path' was modified """
|
|
||||||
return mmp.last_modified(self.cfg['media-monitor']['index_path'])
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
from log import Loggable
|
|
||||||
from events import DeleteFile
|
|
||||||
|
|
||||||
class EventContractor(Loggable):
|
|
||||||
def __init__(self):
|
|
||||||
self.store = {}
|
|
||||||
def event_registered(self, evt):
|
|
||||||
"""
|
|
||||||
returns true if the event is registered which means that there is
|
|
||||||
another "unpacked" event somewhere out there with the same path
|
|
||||||
"""
|
|
||||||
return evt.path in self.store
|
|
||||||
|
|
||||||
def get_old_event(self, evt):
|
|
||||||
"""
|
|
||||||
get the previously registered event with the same path as 'evt'
|
|
||||||
"""
|
|
||||||
return self.store[ evt.path ]
|
|
||||||
|
|
||||||
def register(self, evt):
|
|
||||||
if self.event_registered(evt):
|
|
||||||
ev_proxy = self.get_old_event(evt)
|
|
||||||
if ev_proxy.same_event(evt):
|
|
||||||
ev_proxy.merge_proxy(evt)
|
|
||||||
return False
|
|
||||||
# delete overrides any other event
|
|
||||||
elif evt.is_event(DeleteFile):
|
|
||||||
ev_proxy.merge_proxy(evt)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
ev_proxy.run_hook()
|
|
||||||
ev_proxy.reset_hook()
|
|
||||||
|
|
||||||
self.store[ evt.path ] = evt
|
|
||||||
evt.set_pack_hook( lambda : self.__unregister(evt) )
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __unregister(self, evt):
|
|
||||||
del self.store[evt.path]
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import socket
|
|
||||||
import time
|
|
||||||
from log import Loggable
|
|
||||||
from toucher import RepeatTimer
|
|
||||||
from amqplib.client_0_8.exceptions import AMQPConnectionException
|
|
||||||
|
|
||||||
class EventDrainer(Loggable):
|
|
||||||
"""
|
|
||||||
Flushes events from RabbitMQ that are sent from airtime every
|
|
||||||
certain amount of time
|
|
||||||
"""
|
|
||||||
def __init__(self, airtime_notifier, interval=1):
|
|
||||||
def cb():
|
|
||||||
try:
|
|
||||||
message = airtime_notifier.simple_queue.get(block=True)
|
|
||||||
airtime_notifier.handle_message(message.payload)
|
|
||||||
message.ack()
|
|
||||||
except (IOError, AttributeError, AMQPConnectionException), e:
|
|
||||||
self.logger.error('Exception: %s', e)
|
|
||||||
while not airtime_notifier.init_rabbit_mq():
|
|
||||||
self.logger.error("Error connecting to RabbitMQ Server. \
|
|
||||||
Trying again in few seconds")
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
t = RepeatTimer(interval, cb)
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
|
@ -1,261 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import abc
|
|
||||||
import re
|
|
||||||
import pure as mmp
|
|
||||||
from pure import LazyProperty
|
|
||||||
from metadata import Metadata
|
|
||||||
from log import Loggable
|
|
||||||
from exceptions import BadSongFile
|
|
||||||
from ..saas.thread import getsig, user
|
|
||||||
|
|
||||||
class PathChannel(object):
|
|
||||||
""" Simple struct to hold a 'signal' string and a related 'path'.
|
|
||||||
Basically used as a named tuple """
|
|
||||||
def __init__(self, signal, path):
|
|
||||||
self.signal = getsig(signal)
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
# TODO : Move this to it's file. Also possible unsingleton and use it as a
|
|
||||||
# simple module just like m.m.owners
|
|
||||||
class EventRegistry(object):
|
|
||||||
""" This class's main use is to keep track all events with a cookie
|
|
||||||
attribute. This is done mainly because some events must be 'morphed'
|
|
||||||
into other events because we later detect that they are move events
|
|
||||||
instead of delete events. """
|
|
||||||
def __init__(self):
|
|
||||||
self.registry = {}
|
|
||||||
def register(self,evt): self.registry[evt.cookie] = evt
|
|
||||||
def unregister(self,evt): del self.registry[evt.cookie]
|
|
||||||
def registered(self,evt): return evt.cookie in self.registry
|
|
||||||
def matching(self,evt):
|
|
||||||
event = self.registry[evt.cookie]
|
|
||||||
# Want to disallow accessing the same event twice
|
|
||||||
self.unregister(event)
|
|
||||||
return event
|
|
||||||
|
|
||||||
class EventProxy(Loggable):
|
|
||||||
""" A container object for instances of BaseEvent (or it's
|
|
||||||
subclasses) used for event contractor """
|
|
||||||
def __init__(self, orig_evt):
|
|
||||||
self.orig_evt = orig_evt
|
|
||||||
self.evt = orig_evt
|
|
||||||
self.reset_hook()
|
|
||||||
if hasattr(orig_evt, 'path'): self.path = orig_evt.path
|
|
||||||
|
|
||||||
def set_pack_hook(self, l):
|
|
||||||
self._pack_hook = l
|
|
||||||
|
|
||||||
def reset_hook(self):
|
|
||||||
self._pack_hook = lambda : None
|
|
||||||
|
|
||||||
def run_hook(self):
|
|
||||||
self._pack_hook()
|
|
||||||
|
|
||||||
def safe_pack(self):
|
|
||||||
self.run_hook()
|
|
||||||
# make sure that cleanup hook is never called twice for the same event
|
|
||||||
self.reset_hook()
|
|
||||||
return self.evt.safe_pack()
|
|
||||||
|
|
||||||
def merge_proxy(self, proxy):
|
|
||||||
self.evt = proxy.evt
|
|
||||||
|
|
||||||
def is_event(self, real_event):
|
|
||||||
return isinstance(self.evt, real_event)
|
|
||||||
|
|
||||||
def same_event(self, proxy):
|
|
||||||
return self.evt.__class__ == proxy.evt.__class__
|
|
||||||
|
|
||||||
|
|
||||||
class HasMetaData(object):
|
|
||||||
""" Any class that inherits from this class gains the metadata
|
|
||||||
attribute that loads metadata from the class's 'path' attribute.
|
|
||||||
This is done lazily so there is no performance penalty to inheriting
|
|
||||||
from this and subsequent calls to metadata are cached """
|
|
||||||
__metaclass__ = abc.ABCMeta
|
|
||||||
@LazyProperty
|
|
||||||
def metadata(self): return Metadata(self.path)
|
|
||||||
|
|
||||||
class BaseEvent(Loggable):
|
|
||||||
__metaclass__ = abc.ABCMeta
|
|
||||||
def __init__(self, raw_event):
|
|
||||||
# TODO : clean up this idiotic hack
|
|
||||||
# we should use keyword constructors instead of this behaviour checking
|
|
||||||
# bs to initialize BaseEvent
|
|
||||||
if hasattr(raw_event,"pathname"):
|
|
||||||
self._raw_event = raw_event
|
|
||||||
self.path = os.path.normpath(raw_event.pathname)
|
|
||||||
else: self.path = raw_event
|
|
||||||
self.owner = user().owner.get_owner(self.path)
|
|
||||||
owner_re = re.search('stor/imported/(?P<owner>\d+)/', self.path)
|
|
||||||
if owner_re:
|
|
||||||
self.logger.info("matched path: %s" % self.path)
|
|
||||||
self.owner = owner_re.group('owner')
|
|
||||||
else:
|
|
||||||
self.logger.info("did not match path: %s" % self.path)
|
|
||||||
self._pack_hook = lambda: None # no op
|
|
||||||
# into another event
|
|
||||||
|
|
||||||
# TODO : delete this method later
|
|
||||||
def reset_hook(self):
|
|
||||||
""" Resets the hook that is called after an event is packed.
|
|
||||||
Before resetting the hook we execute it to make sure that
|
|
||||||
whatever cleanup operations were queued are executed. """
|
|
||||||
self._pack_hook()
|
|
||||||
self._pack_hook = lambda: None
|
|
||||||
|
|
||||||
def exists(self): return os.path.exists(self.path)
|
|
||||||
|
|
||||||
@LazyProperty
|
|
||||||
def cookie(self): return getattr( self._raw_event, 'cookie', None )
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Event(%s). Path(%s)" % ( self.path, self.__class__.__name__)
|
|
||||||
|
|
||||||
# TODO : delete this method later
|
|
||||||
def add_safe_pack_hook(self,k):
|
|
||||||
""" adds a callable object (function) that will be called after
|
|
||||||
the event has been "safe_packed" """
|
|
||||||
self._pack_hook = k
|
|
||||||
|
|
||||||
def proxify(self):
|
|
||||||
return EventProxy(self)
|
|
||||||
|
|
||||||
# As opposed to unsafe_pack...
|
|
||||||
def safe_pack(self):
|
|
||||||
""" returns exceptions instead of throwing them to be consistent
|
|
||||||
with events that must catch their own BadSongFile exceptions
|
|
||||||
since generate a set of exceptions instead of a single one """
|
|
||||||
try:
|
|
||||||
self._pack_hook()
|
|
||||||
ret = self.pack()
|
|
||||||
# Remove owner of this file only after packing. Otherwise packing
|
|
||||||
# will not serialize the owner correctly into the airtime request
|
|
||||||
user().owner.remove_file_owner(self.path)
|
|
||||||
return ret
|
|
||||||
except BadSongFile as e: return [e]
|
|
||||||
except Exception as e:
|
|
||||||
self.unexpected_exception(e)
|
|
||||||
return[e]
|
|
||||||
|
|
||||||
# nothing to see here, please move along
|
|
||||||
def morph_into(self, evt):
|
|
||||||
self.logger.info("Morphing %s into %s" % ( str(self), str(evt) ) )
|
|
||||||
self._raw_event = evt._raw_event
|
|
||||||
self.path = evt.path
|
|
||||||
self.__class__ = evt.__class__
|
|
||||||
# Clean up old hook and transfer the new events hook
|
|
||||||
self.reset_hook()
|
|
||||||
self.add_safe_pack_hook( evt._pack_hook )
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assign_owner(self,req):
|
|
||||||
""" Packs self.owner to req if the owner is valid. I.e. it's not
|
|
||||||
-1. This method is used by various events that would like to
|
|
||||||
pass owner as a parameter. NewFile for example. """
|
|
||||||
if self.owner != -1: req['MDATA_KEY_OWNER_ID'] = self.owner
|
|
||||||
|
|
||||||
class FakePyinotify(object):
|
|
||||||
""" sometimes we must create our own pyinotify like objects to
|
|
||||||
instantiate objects from the classes below whenever we want to turn
|
|
||||||
a single event into multiple events """
|
|
||||||
def __init__(self, path): self.pathname = path
|
|
||||||
|
|
||||||
class OrganizeFile(BaseEvent, HasMetaData):
|
|
||||||
""" The only kind of event that does support the pack protocol. It's
|
|
||||||
used internally with mediamonitor to move files in the organize
|
|
||||||
directory. """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(OrganizeFile, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
raise AttributeError("You can't send organize events to airtime!!!")
|
|
||||||
|
|
||||||
class NewFile(BaseEvent, HasMetaData):
|
|
||||||
""" NewFile events are the only events that contain
|
|
||||||
MDATA_KEY_OWNER_ID metadata in them. """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(NewFile, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
""" packs turns an event into a media monitor request """
|
|
||||||
req_dict = self.metadata.extract()
|
|
||||||
req_dict['mode'] = u'create'
|
|
||||||
req_dict['is_record'] = self.metadata.is_recorded()
|
|
||||||
self.assign_owner(req_dict)
|
|
||||||
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path )
|
|
||||||
return [req_dict]
|
|
||||||
|
|
||||||
class DeleteFile(BaseEvent):
|
|
||||||
""" DeleteFile event only contains the path to be deleted. No other
|
|
||||||
metadata can be or is included. (This is because this event is fired
|
|
||||||
after the deletion occurs). """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DeleteFile, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
req_dict = {}
|
|
||||||
req_dict['mode'] = u'delete'
|
|
||||||
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path )
|
|
||||||
return [req_dict]
|
|
||||||
|
|
||||||
class MoveFile(BaseEvent, HasMetaData):
|
|
||||||
""" Path argument should be the new path of the file that was moved """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(MoveFile, self).__init__(*args, **kwargs)
|
|
||||||
def old_path(self):
|
|
||||||
return self._raw_event.src_pathname
|
|
||||||
def pack(self):
|
|
||||||
req_dict = {}
|
|
||||||
req_dict['mode'] = u'moved'
|
|
||||||
req_dict['MDATA_KEY_ORIGINAL_PATH'] = self.old_path()
|
|
||||||
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path )
|
|
||||||
req_dict['MDATA_KEY_MD5'] = self.metadata.extract()['MDATA_KEY_MD5']
|
|
||||||
return [req_dict]
|
|
||||||
|
|
||||||
class ModifyFile(BaseEvent, HasMetaData):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ModifyFile, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
req_dict = self.metadata.extract()
|
|
||||||
req_dict['mode'] = u'modify'
|
|
||||||
# path to directory that is to be removed
|
|
||||||
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path )
|
|
||||||
return [req_dict]
|
|
||||||
|
|
||||||
def map_events(directory, constructor):
|
|
||||||
""" Walks 'directory' and creates an event using 'constructor'.
|
|
||||||
Returns a list of the constructed events. """
|
|
||||||
# -unknown-path should not appear in the path here but more testing
|
|
||||||
# might be necessary
|
|
||||||
for f in mmp.walk_supported(directory, clean_empties=False):
|
|
||||||
try:
|
|
||||||
for e in constructor( FakePyinotify(f) ).pack(): yield e
|
|
||||||
except BadSongFile as e: yield e
|
|
||||||
|
|
||||||
class DeleteDir(BaseEvent):
|
|
||||||
""" A DeleteDir event unfolds itself into a list of DeleteFile
|
|
||||||
events for every file in the directory. """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DeleteDir, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
return map_events( self.path, DeleteFile )
|
|
||||||
|
|
||||||
class MoveDir(BaseEvent):
|
|
||||||
""" A MoveDir event unfolds itself into a list of MoveFile events
|
|
||||||
for every file in the directory. """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(MoveDir, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
return map_events( self.path, MoveFile )
|
|
||||||
|
|
||||||
class DeleteDirWatch(BaseEvent):
|
|
||||||
""" Deleting a watched directory is different from deleting any
|
|
||||||
other directory. Hence we must have a separate event to handle this
|
|
||||||
case """
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DeleteDirWatch, self).__init__(*args, **kwargs)
|
|
||||||
def pack(self):
|
|
||||||
req_dict = {}
|
|
||||||
req_dict['mode'] = u'delete_dir'
|
|
||||||
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path + "/" )
|
|
||||||
return [req_dict]
|
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
class BadSongFile(Exception):
|
|
||||||
def __init__(self, path): self.path = path
|
|
||||||
def __str__(self): return "Can't read %s" % self.path
|
|
||||||
|
|
||||||
class NoConfigFile(Exception):
|
|
||||||
def __init__(self, path): self.path = path
|
|
||||||
def __str__(self):
|
|
||||||
return "Path '%s' for config file does not exit" % self.path
|
|
||||||
|
|
||||||
class ConfigAccessViolation(Exception):
|
|
||||||
def __init__(self,key): self.key = key
|
|
||||||
def __str__(self): return "You must not access key '%s' directly" % self.key
|
|
||||||
|
|
||||||
class FailedToSetLocale(Exception):
|
|
||||||
def __str__(self): return "Failed to set locale"
|
|
||||||
|
|
||||||
class FailedToObtainLocale(Exception):
|
|
||||||
def __init__(self, path, cause):
|
|
||||||
self.path = path
|
|
||||||
self.cause = cause
|
|
||||||
def __str__(self): return "Failed to obtain locale from '%s'" % self.path
|
|
||||||
|
|
||||||
class CouldNotCreateIndexFile(Exception):
|
|
||||||
"""exception whenever index file cannot be created"""
|
|
||||||
def __init__(self, path, cause=None):
|
|
||||||
self.path = path
|
|
||||||
self.cause = cause
|
|
||||||
def __str__(self): return "Failed to create touch file '%s'" % self.path
|
|
||||||
|
|
||||||
class DirectoryIsNotListed(Exception):
|
|
||||||
def __init__(self,dir_id,cause=None):
|
|
||||||
self.dir_id = dir_id
|
|
||||||
self.cause = cause
|
|
||||||
def __str__(self):
|
|
||||||
return "%d was not listed as a directory in the database" % self.dir_id
|
|
||||||
|
|
||||||
class FailedToCreateDir(Exception):
|
|
||||||
def __init__(self,path, parent):
|
|
||||||
self.path = path
|
|
||||||
self.parent = parent
|
|
||||||
def __str__(self): return "Failed to create path '%s'" % self.path
|
|
||||||
|
|
||||||
class NoDirectoryInAirtime(Exception):
|
|
||||||
def __init__(self,path, does_exist):
|
|
||||||
self.path = path
|
|
||||||
self.does_exist = does_exist
|
|
||||||
def __str__(self):
|
|
||||||
return "Directory '%s' does not exist in Airtime.\n \
|
|
||||||
However: %s do exist." % (self.path, self.does_exist)
|
|
||||||
|
|
||||||
class InvalidMetadataElement(Exception):
|
|
||||||
def __init__(self, parent, key, path):
|
|
||||||
self.parent = parent
|
|
||||||
self.key = key
|
|
||||||
self.path = path
|
|
||||||
def __str__(self):
|
|
||||||
return "InvalidMetadataElement: (key,path) = (%s,%s)" \
|
|
||||||
% (self.key, self.path)
|
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from pydispatch import dispatcher
|
|
||||||
import abc
|
|
||||||
|
|
||||||
from log import Loggable
|
|
||||||
from ..saas.thread import getsig
|
|
||||||
import pure as mmp
|
|
||||||
|
|
||||||
# Defines the handle interface
|
|
||||||
class Handles(object):
|
|
||||||
__metaclass__ = abc.ABCMeta
|
|
||||||
@abc.abstractmethod
|
|
||||||
def handle(self, sender, event, *args, **kwargs): pass
|
|
||||||
|
|
||||||
# TODO : Investigate whether weak reffing in dispatcher.connect could possibly
|
|
||||||
# cause a memory leak
|
|
||||||
|
|
||||||
class ReportHandler(Handles):
|
|
||||||
"""
|
|
||||||
A handler that can also report problem files when things go wrong
|
|
||||||
through the report_problem_file routine
|
|
||||||
"""
|
|
||||||
__metaclass__ = abc.ABCMeta
|
|
||||||
def __init__(self, signal, weak=False):
|
|
||||||
self.signal = getsig(signal)
|
|
||||||
self.report_signal = getsig("badfile")
|
|
||||||
def dummy(sender, event): self.handle(sender,event)
|
|
||||||
dispatcher.connect(dummy, signal=self.signal, sender=dispatcher.Any,
|
|
||||||
weak=weak)
|
|
||||||
|
|
||||||
def report_problem_file(self, event, exception=None):
|
|
||||||
dispatcher.send(signal=self.report_signal, sender=self, event=event,
|
|
||||||
exception=exception)
|
|
||||||
|
|
||||||
class ProblemFileHandler(Handles, Loggable):
|
|
||||||
"""
|
|
||||||
Responsible for answering to events passed through the 'badfile'
|
|
||||||
signal. Moves the problem file passed to the designated directory.
|
|
||||||
"""
|
|
||||||
def __init__(self, channel, **kwargs):
|
|
||||||
self.channel = channel
|
|
||||||
self.signal = getsig(self.channel.signal)
|
|
||||||
self.problem_dir = self.channel.path
|
|
||||||
def dummy(sender, event, exception):
|
|
||||||
self.handle(sender, event, exception)
|
|
||||||
dispatcher.connect(dummy, signal=self.signal, sender=dispatcher.Any,
|
|
||||||
weak=False)
|
|
||||||
mmp.create_dir( self.problem_dir )
|
|
||||||
self.logger.info("Initialized problem file handler. Problem dir: '%s'" %
|
|
||||||
self.problem_dir)
|
|
||||||
|
|
||||||
def handle(self, sender, event, exception=None):
|
|
||||||
# TODO : use the exception parameter for something
|
|
||||||
self.logger.info("Received problem file: '%s'. Supposed to move it to \
|
|
||||||
problem dir", event.path)
|
|
||||||
try: mmp.move_to_dir(dir_path=self.problem_dir, file_path=event.path)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.info("Could not move file: '%s' to problem dir: '%s'" %
|
|
||||||
(event.path, self.problem_dir))
|
|
||||||
self.logger.info("Exception: %s" % str(e))
|
|
|
@ -1,138 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import pyinotify
|
|
||||||
from pydispatch import dispatcher
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
import pure as mmp
|
|
||||||
from pure import IncludeOnly
|
|
||||||
from events import OrganizeFile, NewFile, MoveFile, DeleteFile, \
|
|
||||||
DeleteDir, MoveDir,\
|
|
||||||
DeleteDirWatch
|
|
||||||
from log import Loggable
|
|
||||||
from ..saas.thread import getsig, user
|
|
||||||
# Note: Because of the way classes that inherit from pyinotify.ProcessEvent
|
|
||||||
# interact with constructors. you should only instantiate objects from them
|
|
||||||
# using keyword arguments. For example:
|
|
||||||
# OrganizeListener('watch_signal') <= wrong
|
|
||||||
# OrganizeListener(signal='watch_signal') <= right
|
|
||||||
|
|
||||||
class FileMediator(Loggable):
|
|
||||||
# TODO : this class is not actually used. remove all references to it
|
|
||||||
# everywhere (including tests).
|
|
||||||
""" FileMediator is used an intermediate mechanism that filters out
|
|
||||||
certain events. """
|
|
||||||
def __init__(self) : self.ignored_set = set([]) # for paths only
|
|
||||||
def is_ignored(self,path) : return path in self.ignored_set
|
|
||||||
def ignore(self, path) : self.ignored_set.add(path)
|
|
||||||
def unignore(self, path) : self.ignored_set.remove(path)
|
|
||||||
|
|
||||||
def mediate_ignored(fn):
|
|
||||||
@wraps(fn)
|
|
||||||
def wrapped(self, event, *args,**kwargs):
|
|
||||||
event.pathname = unicode(event.pathname, "utf-8")
|
|
||||||
if user().file_mediator.is_ignored(event.pathname):
|
|
||||||
user().file_mediator.logger.info("Ignoring: '%s' (once)" % event.pathname)
|
|
||||||
user().file_mediator.unignore(event.pathname)
|
|
||||||
else: return fn(self, event, *args, **kwargs)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
class BaseListener(object):
|
|
||||||
def __str__(self):
|
|
||||||
return "Listener(%s), Signal(%s)" % \
|
|
||||||
(self.__class__.__name__, self. signal)
|
|
||||||
def my_init(self, signal): self.signal = getsig(signal)
|
|
||||||
|
|
||||||
class OrganizeListener(BaseListener, pyinotify.ProcessEvent, Loggable):
|
|
||||||
def process_IN_CLOSE_WRITE(self, event):
|
|
||||||
#self.logger.info("===> handling: '%s'" % str(event))
|
|
||||||
self.process_to_organize(event)
|
|
||||||
|
|
||||||
def process_IN_MOVED_TO(self, event):
|
|
||||||
#self.logger.info("===> handling: '%s'" % str(event))
|
|
||||||
self.process_to_organize(event)
|
|
||||||
|
|
||||||
def flush_events(self, path):
|
|
||||||
"""
|
|
||||||
organize the whole directory at path. (pretty much by doing what
|
|
||||||
handle does to every file
|
|
||||||
"""
|
|
||||||
flushed = 0
|
|
||||||
for f in mmp.walk_supported(path, clean_empties=True):
|
|
||||||
self.logger.info("Bootstrapping: File in 'organize' directory: \
|
|
||||||
'%s'" % f)
|
|
||||||
if not mmp.file_locked(f):
|
|
||||||
dispatcher.send(signal=getsig(self.signal), sender=self,
|
|
||||||
event=OrganizeFile(f))
|
|
||||||
flushed += 1
|
|
||||||
#self.logger.info("Flushed organized directory with %d files" % flushed)
|
|
||||||
|
|
||||||
@IncludeOnly(mmp.supported_extensions)
|
|
||||||
def process_to_organize(self, event):
|
|
||||||
dispatcher.send(signal=getsig(self.signal), sender=self,
|
|
||||||
event=OrganizeFile(event))
|
|
||||||
|
|
||||||
class StoreWatchListener(BaseListener, Loggable, pyinotify.ProcessEvent):
|
|
||||||
def process_IN_CLOSE_WRITE(self, event):
|
|
||||||
self.process_create(event)
|
|
||||||
def process_IN_MOVED_TO(self, event):
|
|
||||||
if user().event_registry.registered(event):
|
|
||||||
# We need this trick because we don't how to "expand" dir events
|
|
||||||
# into file events until we know for sure if we deleted or moved
|
|
||||||
morph = MoveDir(event) if event.dir else MoveFile(event)
|
|
||||||
user().event_registry.matching(event).morph_into(morph)
|
|
||||||
else: self.process_create(event)
|
|
||||||
def process_IN_MOVED_FROM(self, event):
|
|
||||||
# Is either delete dir or delete file
|
|
||||||
evt = self.process_delete(event)
|
|
||||||
# evt can be none whenever event points that a file that would be
|
|
||||||
# ignored by @IncludeOnly
|
|
||||||
if hasattr(event,'cookie') and (evt != None):
|
|
||||||
user().event_registry.register(evt)
|
|
||||||
def process_IN_DELETE(self,event): self.process_delete(event)
|
|
||||||
def process_IN_MOVE_SELF(self, event):
|
|
||||||
if '-unknown-path' in event.pathname:
|
|
||||||
event.pathname = event.pathname.replace('-unknown-path','')
|
|
||||||
self.delete_watch_dir(event)
|
|
||||||
|
|
||||||
def delete_watch_dir(self, event):
|
|
||||||
e = DeleteDirWatch(event)
|
|
||||||
dispatcher.send(signal=getsig('watch_move'), sender=self, event=e)
|
|
||||||
dispatcher.send(signal=getsig(self.signal), sender=self, event=e)
|
|
||||||
|
|
||||||
@mediate_ignored
|
|
||||||
@IncludeOnly(mmp.supported_extensions)
|
|
||||||
def process_create(self, event):
|
|
||||||
evt = NewFile(event)
|
|
||||||
dispatcher.send(signal=getsig(self.signal), sender=self, event=evt)
|
|
||||||
return evt
|
|
||||||
|
|
||||||
@mediate_ignored
|
|
||||||
@IncludeOnly(mmp.supported_extensions)
|
|
||||||
def process_delete(self, event):
|
|
||||||
evt = None
|
|
||||||
if event.dir : evt = DeleteDir(event)
|
|
||||||
else : evt = DeleteFile(event)
|
|
||||||
dispatcher.send(signal=getsig(self.signal), sender=self, event=evt)
|
|
||||||
return evt
|
|
||||||
|
|
||||||
@mediate_ignored
|
|
||||||
def process_delete_dir(self, event):
|
|
||||||
evt = DeleteDir(event)
|
|
||||||
dispatcher.send(signal=getsig(self.signal), sender=self, event=evt)
|
|
||||||
return evt
|
|
||||||
|
|
||||||
def flush_events(self, path):
|
|
||||||
"""
|
|
||||||
walk over path and send a NewFile event for every file in this
|
|
||||||
directory. Not to be confused with bootstrapping which is a more
|
|
||||||
careful process that involved figuring out what's in the database
|
|
||||||
first.
|
|
||||||
"""
|
|
||||||
# Songs is a dictionary where every key is the watched the directory
|
|
||||||
# and the value is a set with all the files in that directory.
|
|
||||||
added = 0
|
|
||||||
for f in mmp.walk_supported(path, clean_empties=False):
|
|
||||||
added += 1
|
|
||||||
dispatcher.send( signal=getsig(self.signal), sender=self, event=NewFile(f) )
|
|
||||||
self.logger.info( "Flushed watch directory. added = %d" % added )
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import logging
|
|
||||||
import abc
|
|
||||||
import traceback
|
|
||||||
from pure import LazyProperty
|
|
||||||
|
|
||||||
appname = 'root'
|
|
||||||
|
|
||||||
def setup_logging(log_path):
|
|
||||||
""" Setup logging by writing log to 'log_path' """
|
|
||||||
#logger = logging.getLogger(appname)
|
|
||||||
logging.basicConfig(filename=log_path, level=logging.DEBUG)
|
|
||||||
|
|
||||||
def get_logger():
|
|
||||||
""" in case we want to use the common logger from a procedural
|
|
||||||
interface """
|
|
||||||
return logging.getLogger()
|
|
||||||
|
|
||||||
class Loggable(object):
|
|
||||||
""" Any class that wants to log can inherit from this class and
|
|
||||||
automatically get a logger attribute that can be used like:
|
|
||||||
self.logger.info(...) etc. """
|
|
||||||
__metaclass__ = abc.ABCMeta
|
|
||||||
@LazyProperty
|
|
||||||
def logger(self): return get_logger()
|
|
||||||
|
|
||||||
def unexpected_exception(self,e):
|
|
||||||
""" Default message for 'unexpected' exceptions """
|
|
||||||
self.fatal_exception("'Unexpected' exception has occured:", e)
|
|
||||||
|
|
||||||
def fatal_exception(self, message, e):
|
|
||||||
""" Prints an exception 'e' with 'message'. Also outputs the
|
|
||||||
traceback. """
|
|
||||||
self.logger.error( message )
|
|
||||||
self.logger.error( str(e) )
|
|
||||||
self.logger.error( traceback.format_exc() )
|
|
||||||
|
|
|
@ -1,236 +0,0 @@
|
||||||
import pyinotify
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
from pydispatch import dispatcher
|
|
||||||
|
|
||||||
from os.path import normpath
|
|
||||||
from events import PathChannel
|
|
||||||
from log import Loggable
|
|
||||||
from listeners import StoreWatchListener, OrganizeListener
|
|
||||||
from handler import ProblemFileHandler
|
|
||||||
from organizer import Organizer
|
|
||||||
from ..saas.thread import InstanceInheritingThread, getsig
|
|
||||||
import pure as mmp
|
|
||||||
|
|
||||||
|
|
||||||
class ManagerTimeout(InstanceInheritingThread,Loggable):
|
|
||||||
""" The purpose of this class is to flush the organize directory
|
|
||||||
every 3 secnods. This used to be just a work around for cc-4235
|
|
||||||
but recently became a permanent solution because it's "cheap" and
|
|
||||||
reliable """
|
|
||||||
def __init__(self, manager, interval=1.5):
|
|
||||||
# TODO : interval should be read from config and passed here instead
|
|
||||||
# of just using the hard coded value
|
|
||||||
super(ManagerTimeout, self).__init__()
|
|
||||||
self.manager = manager
|
|
||||||
self.interval = interval
|
|
||||||
def run(self):
|
|
||||||
while True:
|
|
||||||
time.sleep(self.interval)
|
|
||||||
self.manager.flush_organize()
|
|
||||||
|
|
||||||
class Manager(Loggable):
|
|
||||||
# NOTE : this massive class is a source of many problems of mm and
|
|
||||||
# is in dire need of breaking up and refactoring.
|
|
||||||
""" An abstraction over media monitors core pyinotify functions.
|
|
||||||
These include adding watched,store, organize directories, etc.
|
|
||||||
Basically composes over WatchManager from pyinotify """
|
|
||||||
def __init__(self):
|
|
||||||
self.wm = pyinotify.WatchManager()
|
|
||||||
# These two instance variables are assumed to be constant
|
|
||||||
self.watch_channel = getsig('watch')
|
|
||||||
self.organize_channel = getsig('organize')
|
|
||||||
self.watch_listener = StoreWatchListener(signal = self.watch_channel)
|
|
||||||
self.__timeout_thread = ManagerTimeout(self)
|
|
||||||
self.__timeout_thread.daemon = True
|
|
||||||
self.__timeout_thread.start()
|
|
||||||
self.organize = {
|
|
||||||
'organize_path' : None,
|
|
||||||
'imported_path' : None,
|
|
||||||
'recorded_path' : None,
|
|
||||||
'problem_files_path' : None,
|
|
||||||
'organizer' : None,
|
|
||||||
'problem_handler' : None,
|
|
||||||
'organize_listener' : OrganizeListener(signal=
|
|
||||||
self.organize_channel),
|
|
||||||
}
|
|
||||||
def dummy(sender, event): self.watch_move( event.path, sender=sender )
|
|
||||||
dispatcher.connect(dummy, signal=getsig('watch_move'),
|
|
||||||
sender=dispatcher.Any, weak=False)
|
|
||||||
def subwatch_add(sender, directory):
|
|
||||||
self.__add_watch(directory, self.watch_listener)
|
|
||||||
dispatcher.connect(subwatch_add, signal=getsig('add_subwatch'),
|
|
||||||
sender=dispatcher.Any, weak=False)
|
|
||||||
# A private mapping path => watch_descriptor
|
|
||||||
# we use the same dictionary for organize, watch, store wd events.
|
|
||||||
# this is a little hacky because we are unable to have multiple wd's
|
|
||||||
# on the same path.
|
|
||||||
self.__wd_path = {}
|
|
||||||
# The following set isn't really necessary anymore. Should be
|
|
||||||
# removed...
|
|
||||||
self.watched_directories = set([])
|
|
||||||
|
|
||||||
# This is the only event that we are unable to process "normally". I.e.
|
|
||||||
# through dedicated handler objects. Because we must have access to a
|
|
||||||
# manager instance. Hence we must slightly break encapsulation.
|
|
||||||
def watch_move(self, watch_dir, sender=None):
|
|
||||||
""" handle 'watch move' events directly sent from listener """
|
|
||||||
self.logger.info("Watch dir '%s' has been renamed (hence removed)" %
|
|
||||||
watch_dir)
|
|
||||||
self.remove_watch_directory(normpath(watch_dir))
|
|
||||||
|
|
||||||
def watch_signal(self):
|
|
||||||
""" Return the signal string our watch_listener is reading
|
|
||||||
events from """
|
|
||||||
return getsig(self.watch_listener.signal)
|
|
||||||
|
|
||||||
def __remove_watch(self,path):
|
|
||||||
""" Remove path from being watched (first will check if 'path'
|
|
||||||
is watched) """
|
|
||||||
# only delete if dir is actually being watched
|
|
||||||
if path in self.__wd_path:
|
|
||||||
wd = self.__wd_path[path]
|
|
||||||
self.wm.rm_watch(wd, rec=True)
|
|
||||||
del(self.__wd_path[path])
|
|
||||||
|
|
||||||
def __add_watch(self,path,listener):
|
|
||||||
""" Start watching 'path' using 'listener'. First will check if
|
|
||||||
directory is being watched before adding another watch """
|
|
||||||
|
|
||||||
self.logger.info("Attempting to add listener to path '%s'" % path)
|
|
||||||
self.logger.info( 'Listener: %s' % str(listener) )
|
|
||||||
|
|
||||||
if not self.has_watch(path):
|
|
||||||
wd = self.wm.add_watch(path, pyinotify.ALL_EVENTS, rec=True,
|
|
||||||
auto_add=True, proc_fun=listener)
|
|
||||||
if wd: self.__wd_path[path] = wd.values()[0]
|
|
||||||
|
|
||||||
def __create_organizer(self, target_path, recorded_path):
|
|
||||||
""" creates an organizer at new destination path or modifies the
|
|
||||||
old one """
|
|
||||||
# TODO : find a proper fix for the following hack
|
|
||||||
# We avoid creating new instances of organize because of the way
|
|
||||||
# it interacts with pydispatch. We must be careful to never have
|
|
||||||
# more than one instance of OrganizeListener but this is not so
|
|
||||||
# easy. (The singleton hack in Organizer) doesn't work. This is
|
|
||||||
# the only thing that seems to work.
|
|
||||||
if self.organize['organizer']:
|
|
||||||
o = self.organize['organizer']
|
|
||||||
o.channel = self.organize_channel
|
|
||||||
o.target_path = target_path
|
|
||||||
o.recorded_path = recorded_path
|
|
||||||
else:
|
|
||||||
self.organize['organizer'] = Organizer(channel=
|
|
||||||
self.organize_channel, target_path=target_path,
|
|
||||||
recorded_path=recorded_path)
|
|
||||||
|
|
||||||
def get_problem_files_path(self):
|
|
||||||
""" returns the path where problem files should go """
|
|
||||||
return self.organize['problem_files_path']
|
|
||||||
|
|
||||||
def set_problem_files_path(self, new_path):
|
|
||||||
""" Set the path where problem files should go """
|
|
||||||
self.organize['problem_files_path'] = new_path
|
|
||||||
self.organize['problem_handler'] = \
|
|
||||||
ProblemFileHandler( PathChannel(signal=getsig('badfile'),
|
|
||||||
path=new_path) )
|
|
||||||
|
|
||||||
def get_recorded_path(self):
|
|
||||||
""" returns the path of the recorded directory """
|
|
||||||
return self.organize['recorded_path']
|
|
||||||
|
|
||||||
def set_recorded_path(self, new_path):
|
|
||||||
self.__remove_watch(self.organize['recorded_path'])
|
|
||||||
self.organize['recorded_path'] = new_path
|
|
||||||
self.__create_organizer( self.organize['imported_path'], new_path)
|
|
||||||
self.__add_watch(new_path, self.watch_listener)
|
|
||||||
|
|
||||||
def get_organize_path(self):
|
|
||||||
""" returns the current path that is being watched for
|
|
||||||
organization """
|
|
||||||
return self.organize['organize_path']
|
|
||||||
|
|
||||||
def set_organize_path(self, new_path):
|
|
||||||
""" sets the organize path to be new_path. Under the current
|
|
||||||
scheme there is only one organize path but there is no reason
|
|
||||||
why more cannot be supported """
|
|
||||||
# if we are already organizing a particular directory we remove the
|
|
||||||
# watch from it first before organizing another directory
|
|
||||||
self.__remove_watch(self.organize['organize_path'])
|
|
||||||
self.organize['organize_path'] = new_path
|
|
||||||
# the OrganizeListener instance will walk path and dispatch an organize
|
|
||||||
# event for every file in that directory
|
|
||||||
self.organize['organize_listener'].flush_events(new_path)
|
|
||||||
#self.__add_watch(new_path, self.organize['organize_listener'])
|
|
||||||
|
|
||||||
def flush_organize(self):
|
|
||||||
path = self.organize['organize_path']
|
|
||||||
self.organize['organize_listener'].flush_events(path)
|
|
||||||
|
|
||||||
def get_imported_path(self):
|
|
||||||
return self.organize['imported_path']
|
|
||||||
|
|
||||||
def set_imported_path(self,new_path):
|
|
||||||
""" set the directory where organized files go to. """
|
|
||||||
self.__remove_watch(self.organize['imported_path'])
|
|
||||||
self.organize['imported_path'] = new_path
|
|
||||||
self.__create_organizer( new_path, self.organize['recorded_path'])
|
|
||||||
self.__add_watch(new_path, self.watch_listener)
|
|
||||||
|
|
||||||
def change_storage_root(self, store):
|
|
||||||
""" hooks up all the directories for you. Problem, recorded,
|
|
||||||
imported, organize. """
|
|
||||||
store_paths = mmp.expand_storage(store)
|
|
||||||
# First attempt to make sure that all paths exist before adding any
|
|
||||||
# watches
|
|
||||||
for path_type, path in store_paths.iteritems():
|
|
||||||
try: mmp.create_dir(path)
|
|
||||||
except mmp.FailedToCreateDir as e: self.unexpected_exception(e)
|
|
||||||
|
|
||||||
os.chmod(store_paths['organize'], 0775)
|
|
||||||
|
|
||||||
self.set_problem_files_path(store_paths['problem_files'])
|
|
||||||
self.set_imported_path(store_paths['imported'])
|
|
||||||
self.set_recorded_path(store_paths['recorded'])
|
|
||||||
self.set_organize_path(store_paths['organize'])
|
|
||||||
|
|
||||||
def has_watch(self, path):
|
|
||||||
""" returns true if the path is being watched or not. Any kind
|
|
||||||
of watch: organize, store, watched. """
|
|
||||||
return path in self.__wd_path
|
|
||||||
|
|
||||||
def add_watch_directory(self, new_dir):
|
|
||||||
""" adds a directory to be "watched". "watched" directories are
|
|
||||||
those that are being monitored by media monitor for airtime in
|
|
||||||
this context and not directories pyinotify calls watched """
|
|
||||||
if self.has_watch(new_dir):
|
|
||||||
self.logger.info("Cannot add '%s' to watched directories. It's \
|
|
||||||
already being watched" % new_dir)
|
|
||||||
else:
|
|
||||||
self.logger.info("Adding watched directory: '%s'" % new_dir)
|
|
||||||
self.__add_watch(new_dir, self.watch_listener)
|
|
||||||
|
|
||||||
def remove_watch_directory(self, watch_dir):
|
|
||||||
""" removes a directory from being "watched". Undoes
|
|
||||||
add_watch_directory """
|
|
||||||
if self.has_watch(watch_dir):
|
|
||||||
self.logger.info("Removing watched directory: '%s'", watch_dir)
|
|
||||||
self.__remove_watch(watch_dir)
|
|
||||||
else:
|
|
||||||
self.logger.info("'%s' is not being watched, hence cannot be \
|
|
||||||
removed" % watch_dir)
|
|
||||||
self.logger.info("The directories we are watching now are:")
|
|
||||||
self.logger.info( self.__wd_path )
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
""" block until we receive pyinotify events """
|
|
||||||
notifier = pyinotify.Notifier(self.wm)
|
|
||||||
notifier.coalesce_events()
|
|
||||||
notifier.loop()
|
|
||||||
#notifier = pyinotify.ThreadedNotifier(self.wm, read_freq=1)
|
|
||||||
#notifier.coalesce_events()
|
|
||||||
#notifier.start()
|
|
||||||
#return notifier
|
|
||||||
#import asyncore
|
|
||||||
#notifier = pyinotify.AsyncNotifier(self.wm)
|
|
||||||
#asyncore.loop()
|
|
|
@ -1,155 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import mutagen
|
|
||||||
import os
|
|
||||||
import copy
|
|
||||||
from mutagen.easymp4 import EasyMP4KeyError
|
|
||||||
from mutagen.easyid3 import EasyID3KeyError
|
|
||||||
|
|
||||||
from exceptions import BadSongFile, InvalidMetadataElement
|
|
||||||
from log import Loggable
|
|
||||||
from pure import format_length
|
|
||||||
import pure as mmp
|
|
||||||
|
|
||||||
# emf related stuff
|
|
||||||
from ..metadata.process import global_reader
|
|
||||||
from ..metadata import definitions as defs
|
|
||||||
defs.load_definitions()
|
|
||||||
|
|
||||||
"""
|
|
||||||
list of supported easy tags in mutagen version 1.20
|
|
||||||
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry',
|
|
||||||
'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby',
|
|
||||||
'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid',
|
|
||||||
'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort',
|
|
||||||
'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor',
|
|
||||||
'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid',
|
|
||||||
'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid',
|
|
||||||
'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm',
|
|
||||||
'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak',
|
|
||||||
'organization']
|
|
||||||
"""
|
|
||||||
|
|
||||||
airtime2mutagen = {
|
|
||||||
"MDATA_KEY_TITLE" : "title",
|
|
||||||
"MDATA_KEY_CREATOR" : "artist",
|
|
||||||
"MDATA_KEY_SOURCE" : "album",
|
|
||||||
"MDATA_KEY_GENRE" : "genre",
|
|
||||||
"MDATA_KEY_MOOD" : "mood",
|
|
||||||
"MDATA_KEY_TRACKNUMBER" : "tracknumber",
|
|
||||||
"MDATA_KEY_BPM" : "bpm",
|
|
||||||
"MDATA_KEY_LABEL" : "label",
|
|
||||||
"MDATA_KEY_COMPOSER" : "composer",
|
|
||||||
"MDATA_KEY_ENCODER" : "encodedby",
|
|
||||||
"MDATA_KEY_CONDUCTOR" : "conductor",
|
|
||||||
"MDATA_KEY_YEAR" : "date",
|
|
||||||
"MDATA_KEY_URL" : "website",
|
|
||||||
"MDATA_KEY_ISRC" : "isrc",
|
|
||||||
"MDATA_KEY_COPYRIGHT" : "copyright",
|
|
||||||
"MDATA_KEY_CUE_IN" : "cuein",
|
|
||||||
"MDATA_KEY_CUE_OUT" : "cueout",
|
|
||||||
}
|
|
||||||
|
|
||||||
#doesn't make sense for us to write these values to a track's metadata
|
|
||||||
mutagen_do_not_write = ["MDATA_KEY_CUE_IN", "MDATA_KEY_CUE_OUT"]
|
|
||||||
|
|
||||||
# Some airtime attributes are special because they must use the mutagen object
|
|
||||||
# itself to calculate the value that they need. The lambda associated with each
|
|
||||||
# key should attempt to extract the corresponding value from the mutagen object
|
|
||||||
# itself pass as 'm'. In the case when nothing can be extracted the lambda
|
|
||||||
# should return some default value to be assigned anyway or None so that the
|
|
||||||
# airtime metadata object will skip the attribute outright.
|
|
||||||
|
|
||||||
airtime_special = {
|
|
||||||
"MDATA_KEY_DURATION" :
|
|
||||||
lambda m: format_length(getattr(m.info, u'length', 0.0)),
|
|
||||||
"MDATA_KEY_BITRATE" :
|
|
||||||
lambda m: getattr(m.info, "bitrate", ''),
|
|
||||||
"MDATA_KEY_SAMPLERATE" :
|
|
||||||
lambda m: getattr(m.info, u'sample_rate', 0),
|
|
||||||
"MDATA_KEY_MIME" :
|
|
||||||
lambda m: m.mime[0] if len(m.mime) > 0 else u'',
|
|
||||||
}
|
|
||||||
mutagen2airtime = dict( (v,k) for k,v in airtime2mutagen.iteritems()
|
|
||||||
if isinstance(v, str) )
|
|
||||||
|
|
||||||
truncate_table = {
|
|
||||||
'MDATA_KEY_GENRE' : 64,
|
|
||||||
'MDATA_KEY_TITLE' : 512,
|
|
||||||
'MDATA_KEY_CREATOR' : 512,
|
|
||||||
'MDATA_KEY_SOURCE' : 512,
|
|
||||||
'MDATA_KEY_MOOD' : 64,
|
|
||||||
'MDATA_KEY_LABEL' : 512,
|
|
||||||
'MDATA_KEY_COMPOSER' : 512,
|
|
||||||
'MDATA_KEY_ENCODER' : 255,
|
|
||||||
'MDATA_KEY_CONDUCTOR' : 512,
|
|
||||||
'MDATA_KEY_YEAR' : 16,
|
|
||||||
'MDATA_KEY_URL' : 512,
|
|
||||||
'MDATA_KEY_ISRC' : 512,
|
|
||||||
'MDATA_KEY_COPYRIGHT' : 512,
|
|
||||||
}
|
|
||||||
|
|
||||||
class Metadata(Loggable):
|
|
||||||
# TODO : refactor the way metadata is being handled. Right now things are a
|
|
||||||
# little bit messy. Some of the handling is in m.m.pure while the rest is
|
|
||||||
# here. Also interface is not very consistent
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def fix_title(path):
|
|
||||||
# If we have no title in path we will format it
|
|
||||||
# TODO : this is very hacky so make sure to fix it
|
|
||||||
m = mutagen.File(path, easy=True)
|
|
||||||
if u'title' not in m:
|
|
||||||
new_title = unicode( mmp.no_extension_basename(path) )
|
|
||||||
m[u'title'] = new_title
|
|
||||||
m.save()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_unsafe(path,md):
|
|
||||||
"""
|
|
||||||
Writes 'md' metadata into 'path' through mutagen. Converts all
|
|
||||||
dictionary values to strings because mutagen will not write anything
|
|
||||||
else
|
|
||||||
"""
|
|
||||||
if not os.path.exists(path): raise BadSongFile(path)
|
|
||||||
song_file = mutagen.File(path, easy=True)
|
|
||||||
exceptions = [] # for bad keys
|
|
||||||
for airtime_k, airtime_v in md.iteritems():
|
|
||||||
if airtime_k in airtime2mutagen and \
|
|
||||||
airtime_k not in mutagen_do_not_write:
|
|
||||||
# The unicode cast here is mostly for integers that need to be
|
|
||||||
# strings
|
|
||||||
if airtime_v is None: continue
|
|
||||||
try:
|
|
||||||
song_file[ airtime2mutagen[airtime_k] ] = unicode(airtime_v)
|
|
||||||
except (EasyMP4KeyError, EasyID3KeyError) as e:
|
|
||||||
exceptions.append(InvalidMetadataElement(e, airtime_k,
|
|
||||||
path))
|
|
||||||
song_file.save()
|
|
||||||
# bubble dem up so that user knows that something is wrong
|
|
||||||
for e in exceptions: raise e
|
|
||||||
|
|
||||||
def __init__(self, fpath):
|
|
||||||
# Forcing the unicode through
|
|
||||||
try : fpath = fpath.decode("utf-8")
|
|
||||||
except : pass
|
|
||||||
self.__metadata = global_reader.read_mutagen(fpath)
|
|
||||||
|
|
||||||
def is_recorded(self):
|
|
||||||
"""
|
|
||||||
returns true if the file has been created by airtime through recording
|
|
||||||
"""
|
|
||||||
return mmp.is_airtime_recorded( self.__metadata )
|
|
||||||
|
|
||||||
def extract(self):
|
|
||||||
"""
|
|
||||||
returns a copy of the metadata that was loaded when object was
|
|
||||||
constructed
|
|
||||||
"""
|
|
||||||
return copy.deepcopy(self.__metadata)
|
|
||||||
|
|
||||||
def utf8(self):
|
|
||||||
"""
|
|
||||||
Returns a unicode aware representation of the data that is compatible
|
|
||||||
with what is spent to airtime
|
|
||||||
"""
|
|
||||||
return mmp.convert_dict_value_to_utf8(self.extract())
|
|
|
@ -1,87 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import pure as mmp
|
|
||||||
from handler import ReportHandler
|
|
||||||
from log import Loggable
|
|
||||||
from exceptions import BadSongFile
|
|
||||||
from events import OrganizeFile
|
|
||||||
from pydispatch import dispatcher
|
|
||||||
from os.path import dirname
|
|
||||||
from ..saas.thread import getsig, user
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
class Organizer(ReportHandler,Loggable):
|
|
||||||
""" Organizer is responsible to to listening to OrganizeListener
|
|
||||||
events and committing the appropriate changes to the filesystem.
|
|
||||||
It does not in any interact with WatchSyncer's even when the the
|
|
||||||
WatchSyncer is a "storage directory". The "storage" directory picks
|
|
||||||
up all of its events through pyinotify. (These events are fed to it
|
|
||||||
through StoreWatchListener) """
|
|
||||||
|
|
||||||
# Commented out making this class a singleton because it's just a band aid
|
|
||||||
# for the real issue. The real issue being making multiple Organizer
|
|
||||||
# instances with pydispatch
|
|
||||||
|
|
||||||
#_instance = None
|
|
||||||
#def __new__(cls, channel, target_path, recorded_path):
|
|
||||||
#if cls._instance:
|
|
||||||
#cls._instance.channel = channel
|
|
||||||
#cls._instance.target_path = target_path
|
|
||||||
#cls._instance.recorded_path = recorded_path
|
|
||||||
#else:
|
|
||||||
#cls._instance = super(Organizer, cls).__new__( cls, channel,
|
|
||||||
#target_path, recorded_path)
|
|
||||||
#return cls._instance
|
|
||||||
|
|
||||||
def __init__(self, channel, target_path, recorded_path):
|
|
||||||
self.channel = channel
|
|
||||||
self.target_path = target_path
|
|
||||||
self.recorded_path = recorded_path
|
|
||||||
super(Organizer, self).__init__(signal=getsig(self.channel), weak=False)
|
|
||||||
|
|
||||||
def handle(self, sender, event):
|
|
||||||
""" Intercept events where a new file has been added to the
|
|
||||||
organize directory and place it in the correct path (starting
|
|
||||||
with self.target_path) """
|
|
||||||
# Only handle this event type
|
|
||||||
assert isinstance(event, OrganizeFile), \
|
|
||||||
"Organizer can only handle OrganizeFile events.Given '%s'" % event
|
|
||||||
try:
|
|
||||||
# We must select the target_path based on whether file was recorded
|
|
||||||
# by airtime or not.
|
|
||||||
# Do we need to "massage" the path using mmp.organized_path?
|
|
||||||
target_path = self.recorded_path if event.metadata.is_recorded() \
|
|
||||||
else self.target_path
|
|
||||||
# nasty hack do this properly
|
|
||||||
owner_id = mmp.owner_id(event.path)
|
|
||||||
if owner_id != -1:
|
|
||||||
target_path = os.path.join(target_path, unicode(owner_id))
|
|
||||||
|
|
||||||
mdata = event.metadata.extract()
|
|
||||||
new_path = mmp.organized_path(event.path, target_path, mdata)
|
|
||||||
|
|
||||||
# See hack in mmp.magic_move
|
|
||||||
def new_dir_watch(d):
|
|
||||||
# TODO : rewrite as return lambda : dispatcher.send(...
|
|
||||||
def cb():
|
|
||||||
dispatcher.send(signal=getsig("add_subwatch"), sender=self,
|
|
||||||
directory=d)
|
|
||||||
return cb
|
|
||||||
|
|
||||||
mmp.magic_move(event.path, new_path,
|
|
||||||
after_dir_make=new_dir_watch(dirname(new_path)))
|
|
||||||
|
|
||||||
# The reason we need to go around saving the owner in this
|
|
||||||
# backwards way is because we are unable to encode the owner id
|
|
||||||
# into the file itself so that the StoreWatchListener listener can
|
|
||||||
# detect it from the file
|
|
||||||
user().owner.add_file_owner(new_path, owner_id )
|
|
||||||
|
|
||||||
self.logger.info('Organized: "%s" into "%s"' %
|
|
||||||
(event.path, new_path))
|
|
||||||
except BadSongFile as e:
|
|
||||||
self.report_problem_file(event=event, exception=e)
|
|
||||||
# probably general error in mmp.magic.move...
|
|
||||||
except Exception as e:
|
|
||||||
self.unexpected_exception( e )
|
|
||||||
self.report_problem_file(event=event, exception=e)
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from log import Loggable
|
|
||||||
|
|
||||||
class Owner(Loggable):
|
|
||||||
def __init__(self):
|
|
||||||
# hash: 'filepath' => owner_id
|
|
||||||
self.owners = {}
|
|
||||||
|
|
||||||
def get_owner(self,f):
|
|
||||||
""" Get the owner id of the file 'f' """
|
|
||||||
o = self.owners[f] if f in self.owners else -1
|
|
||||||
self.logger.info("Received owner for %s. Owner: %s" % (f, o))
|
|
||||||
return o
|
|
||||||
|
|
||||||
|
|
||||||
def add_file_owner(self,f,owner):
|
|
||||||
""" Associate file f with owner. If owner is -1 then do we will not record
|
|
||||||
it because -1 means there is no owner. Returns True if f is being stored
|
|
||||||
after the function. False otherwise. """
|
|
||||||
if owner == -1: return False
|
|
||||||
if f in self.owners:
|
|
||||||
if owner != self.owners[f]: # check for fishiness
|
|
||||||
self.logger.info("Warning ownership of file '%s' changed from '%d' to '%d'"
|
|
||||||
% (f, self.owners[f], owner))
|
|
||||||
else: return True
|
|
||||||
self.owners[f] = owner
|
|
||||||
return True
|
|
||||||
|
|
||||||
def has_owner(self,f):
|
|
||||||
""" True if f is owned by somebody. False otherwise. """
|
|
||||||
return f in self.owners
|
|
||||||
|
|
||||||
def remove_file_owner(self,f):
|
|
||||||
""" Try and delete any association made with file f. Returns true if
|
|
||||||
the the association was actually deleted. False otherwise. """
|
|
||||||
if f in self.owners:
|
|
||||||
del self.owners[f]
|
|
||||||
return True
|
|
||||||
else: return False
|
|
||||||
|
|
|
@ -1,508 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import copy
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import wave
|
|
||||||
import contextlib
|
|
||||||
import shutil, pipes
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import stat
|
|
||||||
import hashlib
|
|
||||||
import locale
|
|
||||||
import operator as op
|
|
||||||
|
|
||||||
from os.path import normpath
|
|
||||||
from itertools import takewhile
|
|
||||||
# you need to import reduce in python 3
|
|
||||||
try: from functools import reduce
|
|
||||||
except: pass
|
|
||||||
from configobj import ConfigObj
|
|
||||||
|
|
||||||
from exceptions import FailedToSetLocale, FailedToCreateDir
|
|
||||||
|
|
||||||
supported_extensions = [u"mp3", u"ogg", u"oga", u"flac", u"wav",
|
|
||||||
u'm4a', u'mp4', 'opus']
|
|
||||||
|
|
||||||
unicode_unknown = u'unknown'
|
|
||||||
|
|
||||||
path_md = ['MDATA_KEY_TITLE', 'MDATA_KEY_CREATOR', 'MDATA_KEY_SOURCE',
|
|
||||||
'MDATA_KEY_TRACKNUMBER', 'MDATA_KEY_BITRATE']
|
|
||||||
|
|
||||||
class LazyProperty(object):
|
|
||||||
"""
|
|
||||||
meant to be used for lazy evaluation of an object attribute.
|
|
||||||
property should represent non-mutable data, as it replaces itself.
|
|
||||||
"""
|
|
||||||
def __init__(self,fget):
|
|
||||||
self.fget = fget
|
|
||||||
self.func_name = fget.__name__
|
|
||||||
|
|
||||||
def __get__(self,obj,cls):
|
|
||||||
if obj is None: return None
|
|
||||||
value = self.fget(obj)
|
|
||||||
setattr(obj,self.func_name,value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
class IncludeOnly(object):
|
|
||||||
"""
|
|
||||||
A little decorator to help listeners only be called on extensions
|
|
||||||
they support
|
|
||||||
NOTE: this decorator only works on methods and not functions. Maybe
|
|
||||||
fix this?
|
|
||||||
"""
|
|
||||||
def __init__(self, *deco_args):
|
|
||||||
self.exts = set([])
|
|
||||||
for arg in deco_args:
|
|
||||||
if isinstance(arg,str): self.add(arg)
|
|
||||||
elif hasattr(arg, '__iter__'):
|
|
||||||
for x in arg: self.exts.add(x)
|
|
||||||
def __call__(self, func):
|
|
||||||
def _wrap(moi, event, *args, **kwargs):
|
|
||||||
ext = extension(event.pathname)
|
|
||||||
# Checking for emptiness b/c we don't want to skip direcotries
|
|
||||||
if (ext.lower() in self.exts) or event.dir:
|
|
||||||
return func(moi, event, *args, **kwargs)
|
|
||||||
return _wrap
|
|
||||||
|
|
||||||
def partition(f, alist):
|
|
||||||
"""
|
|
||||||
Partition is very similar to filter except that it also returns the
|
|
||||||
elements for which f return false but in a tuple.
|
|
||||||
>>> partition(lambda x : x > 3, [1,2,3,4,5,6])
|
|
||||||
([4, 5, 6], [1, 2, 3])
|
|
||||||
"""
|
|
||||||
return (filter(f, alist), filter(lambda x: not f(x), alist))
|
|
||||||
|
|
||||||
def is_file_supported(path):
|
|
||||||
"""
|
|
||||||
Checks if a file's path(filename) extension matches the kind that we
|
|
||||||
support note that this is case insensitive.
|
|
||||||
>>> is_file_supported("test.mp3")
|
|
||||||
True
|
|
||||||
>>> is_file_supported("/bs/path/test.mP3")
|
|
||||||
True
|
|
||||||
>>> is_file_supported("test.txt")
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
return extension(path).lower() in supported_extensions
|
|
||||||
|
|
||||||
# TODO : In the future we would like a better way to find out whether a show
|
|
||||||
# has been recorded
|
|
||||||
def is_airtime_recorded(md):
|
|
||||||
""" Takes a metadata dictionary and returns True if it belongs to a
|
|
||||||
file that was recorded by Airtime. """
|
|
||||||
if not 'MDATA_KEY_CREATOR' in md: return False
|
|
||||||
return md['MDATA_KEY_CREATOR'] == u'Airtime Show Recorder'
|
|
||||||
|
|
||||||
def read_wave_duration(path):
|
|
||||||
""" Read the length of .wav file (mutagen does not handle this) """
|
|
||||||
with contextlib.closing(wave.open(path,'r')) as f:
|
|
||||||
frames = f.getnframes()
|
|
||||||
rate = f.getframerate()
|
|
||||||
duration = frames/float(rate)
|
|
||||||
return duration
|
|
||||||
|
|
||||||
def clean_empty_dirs(path):
|
|
||||||
""" walks path and deletes every empty directory it finds """
|
|
||||||
# TODO : test this function
|
|
||||||
if path.endswith('/'): clean_empty_dirs(path[0:-1])
|
|
||||||
else:
|
|
||||||
for root, dirs, _ in os.walk(path, topdown=False):
|
|
||||||
full_paths = ( os.path.join(root, d) for d in dirs )
|
|
||||||
for d in full_paths:
|
|
||||||
if os.path.exists(d):
|
|
||||||
#Try block avoids a race condition where a file is added AFTER listdir
|
|
||||||
#is run but before removedirs. (Dir is not empty and removedirs throws
|
|
||||||
#an exception in that case then.)
|
|
||||||
try:
|
|
||||||
if not os.listdir(d): os.rmdir(d)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def extension(path):
|
|
||||||
"""
|
|
||||||
return extension of path, empty string otherwise. Prefer to return empty
|
|
||||||
string instead of None because of bad handling of "maybe" types in python.
|
|
||||||
I.e. interpreter won't enforce None checks on the programmer
|
|
||||||
>>> extension("testing.php")
|
|
||||||
'php'
|
|
||||||
>>> extension("a.b.c.d.php")
|
|
||||||
'php'
|
|
||||||
>>> extension('/no/extension')
|
|
||||||
''
|
|
||||||
>>> extension('/path/extension.ml')
|
|
||||||
'ml'
|
|
||||||
"""
|
|
||||||
ext = path.split(".")
|
|
||||||
if len(ext) < 2: return ""
|
|
||||||
else: return ext[-1]
|
|
||||||
|
|
||||||
def no_extension_basename(path):
|
|
||||||
"""
|
|
||||||
returns the extensionsless basename of a filepath
|
|
||||||
>>> no_extension_basename("/home/test.mp3")
|
|
||||||
u'test'
|
|
||||||
>>> no_extension_basename("/home/test")
|
|
||||||
u'test'
|
|
||||||
>>> no_extension_basename('blah.ml')
|
|
||||||
u'blah'
|
|
||||||
>>> no_extension_basename('a.b.c.d.mp3')
|
|
||||||
u'a.b.c.d'
|
|
||||||
"""
|
|
||||||
base = unicode(os.path.basename(path))
|
|
||||||
if extension(base) == "": return base
|
|
||||||
else: return '.'.join(base.split(".")[0:-1])
|
|
||||||
|
|
||||||
def walk_supported(directory, clean_empties=False):
|
|
||||||
""" A small generator wrapper around os.walk to only give us files
|
|
||||||
that support the extensions we are considering. When clean_empties
|
|
||||||
is True we recursively delete empty directories left over in
|
|
||||||
directory after the walk. """
|
|
||||||
if directory is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk(directory):
|
|
||||||
full_paths = ( os.path.join(root, name) for name in files
|
|
||||||
if is_file_supported(name) )
|
|
||||||
for fp in full_paths: yield fp
|
|
||||||
if clean_empties: clean_empty_dirs(directory)
|
|
||||||
|
|
||||||
|
|
||||||
def file_locked(path):
|
|
||||||
#Capture stderr to avoid polluting py-interpreter.log
|
|
||||||
proc = Popen(["lsof", path], stdout=PIPE, stderr=PIPE)
|
|
||||||
out = proc.communicate()[0].strip('\r\n')
|
|
||||||
return bool(out)
|
|
||||||
|
|
||||||
def magic_move(old, new, after_dir_make=lambda : None):
|
|
||||||
""" Moves path old to new and constructs the necessary to
|
|
||||||
directories for new along the way """
|
|
||||||
new_dir = os.path.dirname(new)
|
|
||||||
if not os.path.exists(new_dir): os.makedirs(new_dir)
|
|
||||||
# We need this crusty hack because anytime a directory is created we must
|
|
||||||
# re-add it with add_watch otherwise putting files in it will not trigger
|
|
||||||
# pyinotify events
|
|
||||||
after_dir_make()
|
|
||||||
shutil.move(old,new)
|
|
||||||
|
|
||||||
def move_to_dir(dir_path,file_path):
|
|
||||||
""" moves a file at file_path into dir_path/basename(filename) """
|
|
||||||
bs = os.path.basename(file_path)
|
|
||||||
magic_move(file_path, os.path.join(dir_path, bs))
|
|
||||||
|
|
||||||
def apply_rules_dict(d, rules):
|
|
||||||
""" Consumes a dictionary of rules that maps some keys to lambdas
|
|
||||||
which it applies to every matching element in d and returns a new
|
|
||||||
dictionary with the rules applied. If a rule returns none then it's
|
|
||||||
not applied """
|
|
||||||
new_d = copy.deepcopy(d)
|
|
||||||
for k, rule in rules.iteritems():
|
|
||||||
if k in d:
|
|
||||||
new_val = rule(d[k])
|
|
||||||
if new_val is not None: new_d[k] = new_val
|
|
||||||
return new_d
|
|
||||||
|
|
||||||
def default_to_f(dictionary, keys, default, condition):
|
|
||||||
new_d = copy.deepcopy(dictionary)
|
|
||||||
for k in keys:
|
|
||||||
if condition(dictionary=new_d, key=k): new_d[k] = default
|
|
||||||
return new_d
|
|
||||||
|
|
||||||
def default_to(dictionary, keys, default):
|
|
||||||
""" Checks if the list of keys 'keys' exists in 'dictionary'. If
|
|
||||||
not then it returns a new dictionary with all those missing keys
|
|
||||||
defaults to 'default' """
|
|
||||||
cnd = lambda dictionary, key: key not in dictionary
|
|
||||||
return default_to_f(dictionary, keys, default, cnd)
|
|
||||||
|
|
||||||
def remove_whitespace(dictionary):
|
|
||||||
""" Remove values that empty whitespace in the dictionary """
|
|
||||||
nd = copy.deepcopy(dictionary)
|
|
||||||
bad_keys = []
|
|
||||||
for k,v in nd.iteritems():
|
|
||||||
if hasattr(v,'strip'):
|
|
||||||
stripped = v.strip()
|
|
||||||
# ghetto and maybe unnecessary
|
|
||||||
if stripped == '' or stripped == u'': bad_keys.append(k)
|
|
||||||
for bad_key in bad_keys: del nd[bad_key]
|
|
||||||
return nd
|
|
||||||
|
|
||||||
def parse_int(s):
|
|
||||||
# TODO : this function isn't used anywhere yet but it may useful for emf
|
|
||||||
"""
|
|
||||||
Tries very hard to get some sort of integer result from s. Defaults to 0
|
|
||||||
when it fails
|
|
||||||
>>> parse_int("123")
|
|
||||||
'123'
|
|
||||||
>>> parse_int("123saf")
|
|
||||||
'123'
|
|
||||||
>>> parse_int("asdf")
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
if s.isdigit(): return s
|
|
||||||
else:
|
|
||||||
try : return str(reduce(op.add, takewhile(lambda x: x.isdigit(), s)))
|
|
||||||
except: return None
|
|
||||||
|
|
||||||
|
|
||||||
def organized_path(old_path, root_path, orig_md):
|
|
||||||
"""
|
|
||||||
old_path - path where file is store at the moment <= maybe not necessary?
|
|
||||||
root_path - the parent directory where all organized files go
|
|
||||||
orig_md - original meta data of the file as given by mutagen AFTER being
|
|
||||||
normalized
|
|
||||||
return value: new file path
|
|
||||||
"""
|
|
||||||
filepath = None
|
|
||||||
ext = extension(old_path)
|
|
||||||
def default_f(dictionary, key):
|
|
||||||
if key in dictionary: return len(str(dictionary[key])) == 0
|
|
||||||
else: return True
|
|
||||||
# We set some metadata elements to a default "unknown" value because we use
|
|
||||||
# these fields to create a path hence they cannot be empty Here "normal"
|
|
||||||
# means normalized only for organized path
|
|
||||||
|
|
||||||
# MDATA_KEY_BITRATE is in bytes/second i.e. (256000) we want to turn this
|
|
||||||
# into 254kbps
|
|
||||||
|
|
||||||
# Some metadata elements cannot be empty, hence we default them to some
|
|
||||||
# value just so that we can create a correct path
|
|
||||||
normal_md = default_to_f(orig_md, path_md, unicode_unknown, default_f)
|
|
||||||
try:
|
|
||||||
formatted = str(int(normal_md['MDATA_KEY_BITRATE']) / 1000)
|
|
||||||
normal_md['MDATA_KEY_BITRATE'] = formatted + 'kbps'
|
|
||||||
except:
|
|
||||||
normal_md['MDATA_KEY_BITRATE'] = unicode_unknown
|
|
||||||
|
|
||||||
if is_airtime_recorded(normal_md):
|
|
||||||
# normal_md['MDATA_KEY_TITLE'] = 'show_name-yyyy-mm-dd-hh:mm:ss'
|
|
||||||
r = "(?P<show>.+)-(?P<date>\d+-\d+-\d+)-(?P<time>\d+:\d+:\d+)$"
|
|
||||||
title_re = re.match(r, normal_md['MDATA_KEY_TITLE'])
|
|
||||||
show_name = title_re.group('show')
|
|
||||||
#date = title_re.group('date')
|
|
||||||
yyyy, mm, dd = normal_md['MDATA_KEY_YEAR'].split('-',2)
|
|
||||||
fname_base = '%s-%s-%s.%s' % \
|
|
||||||
(title_re.group('time'), show_name,
|
|
||||||
normal_md['MDATA_KEY_BITRATE'], ext)
|
|
||||||
filepath = os.path.join(root_path, yyyy, mm, dd, fname_base)
|
|
||||||
elif len(normal_md['MDATA_KEY_TRACKNUMBER']) == 0:
|
|
||||||
fname = u'%s-%s.%s' % (normal_md['MDATA_KEY_TITLE'],
|
|
||||||
normal_md['MDATA_KEY_BITRATE'], ext)
|
|
||||||
path = os.path.join(root_path, normal_md['MDATA_KEY_CREATOR'],
|
|
||||||
normal_md['MDATA_KEY_SOURCE'] )
|
|
||||||
filepath = os.path.join(path, fname)
|
|
||||||
else: # The "normal" case
|
|
||||||
fname = u'%s-%s-%s.%s' % (normal_md['MDATA_KEY_TRACKNUMBER'],
|
|
||||||
normal_md['MDATA_KEY_TITLE'],
|
|
||||||
normal_md['MDATA_KEY_BITRATE'], ext)
|
|
||||||
path = os.path.join(root_path, normal_md['MDATA_KEY_CREATOR'],
|
|
||||||
normal_md['MDATA_KEY_SOURCE'])
|
|
||||||
filepath = os.path.join(path, fname)
|
|
||||||
return filepath
|
|
||||||
|
|
||||||
# TODO : Get rid of this function and every one of its uses. We no longer use
|
|
||||||
# the md5 signature of a song for anything
|
|
||||||
def file_md5(path,max_length=100):
|
|
||||||
""" Get md5 of file path (if it exists). Use only max_length
|
|
||||||
characters to save time and memory. Pass max_length=-1 to read the
|
|
||||||
whole file (like in mm1) """
|
|
||||||
if os.path.exists(path):
|
|
||||||
with open(path, 'rb') as f:
|
|
||||||
m = hashlib.md5()
|
|
||||||
# If a file is shorter than "max_length" python will just return
|
|
||||||
# whatever it was able to read which is acceptable behaviour
|
|
||||||
m.update(f.read(max_length))
|
|
||||||
return m.hexdigest()
|
|
||||||
else: raise ValueError("'%s' must exist to find its md5" % path)
|
|
||||||
|
|
||||||
def encode_to(obj, encoding='utf-8'):
|
|
||||||
# TODO : add documentation + unit tests for this function
|
|
||||||
if isinstance(obj, unicode): obj = obj.encode(encoding)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def convert_dict_value_to_utf8(md):
|
|
||||||
""" formats a dictionary to send as a request to api client """
|
|
||||||
return dict([(item[0], encode_to(item[1], "utf-8")) for item in md.items()])
|
|
||||||
|
|
||||||
def get_system_locale(locale_path='/etc/default/locale'):
|
|
||||||
""" Returns the configuration object for the system's default
|
|
||||||
locale. Normally requires root access. """
|
|
||||||
if os.path.exists(locale_path):
|
|
||||||
try:
|
|
||||||
config = ConfigObj(locale_path)
|
|
||||||
return config
|
|
||||||
except Exception as e: raise FailedToSetLocale(locale_path,cause=e)
|
|
||||||
else: raise ValueError("locale path '%s' does not exist. \
|
|
||||||
permissions issue?" % locale_path)
|
|
||||||
|
|
||||||
def configure_locale(config):
|
|
||||||
""" sets the locale according to the system's locale. """
|
|
||||||
current_locale = locale.getlocale()
|
|
||||||
if current_locale[1] is None:
|
|
||||||
default_locale = locale.getdefaultlocale()
|
|
||||||
if default_locale[1] is None:
|
|
||||||
lang = config.get('LANG')
|
|
||||||
new_locale = lang
|
|
||||||
else: new_locale = default_locale
|
|
||||||
locale.setlocale(locale.LC_ALL, new_locale)
|
|
||||||
reload(sys)
|
|
||||||
sys.setdefaultencoding("UTF-8")
|
|
||||||
current_locale_encoding = locale.getlocale()[1].lower()
|
|
||||||
if current_locale_encoding not in ['utf-8', 'utf8']:
|
|
||||||
raise FailedToSetLocale()
|
|
||||||
|
|
||||||
def fondle(path,times=None):
|
|
||||||
# TODO : write unit tests for this
|
|
||||||
""" touch a file to change the last modified date. Beware of calling
|
|
||||||
this function on the same file from multiple threads. """
|
|
||||||
with file(path, 'a'): os.utime(path, times)
|
|
||||||
|
|
||||||
def last_modified(path):
|
|
||||||
""" return the time of the last time mm2 was ran. path refers to the
|
|
||||||
index file whose date modified attribute contains this information.
|
|
||||||
In the case when the file does not exist we set this time 0 so that
|
|
||||||
any files on the filesystem were modified after it """
|
|
||||||
if os.path.exists(path): return os.path.getmtime(path)
|
|
||||||
else: return 0
|
|
||||||
|
|
||||||
def expand_storage(store):
|
|
||||||
""" A storage directory usually consists of 4 different
|
|
||||||
subdirectories. This function returns their paths """
|
|
||||||
store = os.path.normpath(store)
|
|
||||||
return {
|
|
||||||
'organize' : os.path.join(store, 'organize'),
|
|
||||||
'recorded' : os.path.join(store, 'recorded'),
|
|
||||||
'problem_files' : os.path.join(store, 'problem_files'),
|
|
||||||
'imported' : os.path.join(store, 'imported'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_dir(path):
|
|
||||||
""" will try and make sure that path exists at all costs. raises an
|
|
||||||
exception if it fails at this task. """
|
|
||||||
if not os.path.exists(path):
|
|
||||||
try : os.makedirs(path)
|
|
||||||
except Exception as e : raise FailedToCreateDir(path, e)
|
|
||||||
else: # if no error occurs we still need to check that dir exists
|
|
||||||
if not os.path.exists: raise FailedToCreateDir(path)
|
|
||||||
|
|
||||||
def sub_path(directory,f):
|
|
||||||
"""
|
|
||||||
returns true if 'f' is in the tree of files under directory.
|
|
||||||
NOTE: does not look at any symlinks or anything like that, just looks at
|
|
||||||
the paths.
|
|
||||||
"""
|
|
||||||
normalized = normpath(directory)
|
|
||||||
common = os.path.commonprefix([ normalized, normpath(f) ])
|
|
||||||
return common == normalized
|
|
||||||
|
|
||||||
def owner_id(original_path):
|
|
||||||
""" Given 'original_path' return the file name of the of
|
|
||||||
'identifier' file. return the id that is contained in it. If no file
|
|
||||||
is found or nothing is read then -1 is returned. File is deleted
|
|
||||||
after the number has been read """
|
|
||||||
fname = "%s.identifier" % original_path
|
|
||||||
owner_id = -1
|
|
||||||
try:
|
|
||||||
f = open(fname)
|
|
||||||
for line in f:
|
|
||||||
owner_id = int(line)
|
|
||||||
break
|
|
||||||
f.close()
|
|
||||||
except Exception: pass
|
|
||||||
else:
|
|
||||||
try: os.unlink(fname)
|
|
||||||
except Exception: raise
|
|
||||||
return owner_id
|
|
||||||
|
|
||||||
def file_playable(pathname):
|
|
||||||
""" Returns True if 'pathname' is playable by liquidsoap. False
|
|
||||||
otherwise. """
|
|
||||||
|
|
||||||
#currently disabled because this confuses inotify....
|
|
||||||
return True
|
|
||||||
#remove all write permissions. This is due to stupid taglib library bug
|
|
||||||
#where all files are opened in write mode. The only way around this is to
|
|
||||||
#modify the file permissions
|
|
||||||
os.chmod(pathname, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
|
|
||||||
|
|
||||||
# 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 = ("airtime-liquidsoap -c 'output.dummy" + \
|
|
||||||
"(audio_to_stereo(single(\"%s\")))' > /dev/null 2>&1") % \
|
|
||||||
pathname.replace("'", "'\\''")
|
|
||||||
|
|
||||||
return_code = subprocess.call(command, shell=True)
|
|
||||||
|
|
||||||
#change/restore permissions to acceptable
|
|
||||||
os.chmod(pathname, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | \
|
|
||||||
stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
|
|
||||||
return (return_code == 0)
|
|
||||||
|
|
||||||
def toposort(data):
|
|
||||||
"""
|
|
||||||
Topological sort on 'data' where 'data' is of the form:
|
|
||||||
data = [
|
|
||||||
'one' : set('two','three'),
|
|
||||||
'two' : set('three'),
|
|
||||||
'three' : set()
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
for k, v in data.items():
|
|
||||||
v.discard(k) # Ignore self dependencies
|
|
||||||
extra_items_in_deps = reduce(set.union, data.values()) - set(data.keys())
|
|
||||||
data.update(dict((item,set()) for item in extra_items_in_deps))
|
|
||||||
while True:
|
|
||||||
ordered = set(item for item,dep in data.items() if not dep)
|
|
||||||
if not ordered: break
|
|
||||||
for e in sorted(ordered): yield e
|
|
||||||
data = dict((item,(dep - ordered)) for item,dep in data.items()
|
|
||||||
if item not in ordered)
|
|
||||||
assert not data, "A cyclic dependency exists amongst %r" % data
|
|
||||||
|
|
||||||
def truncate_to_length(item, length):
|
|
||||||
""" Truncates 'item' to 'length' """
|
|
||||||
if isinstance(item, int): item = str(item)
|
|
||||||
if isinstance(item, basestring):
|
|
||||||
if len(item) > length: return item[0:length]
|
|
||||||
else: return item
|
|
||||||
|
|
||||||
def truncate_to_value(item, value):
|
|
||||||
""" Truncates 'item' to 'value' """
|
|
||||||
if isinstance(item, basestring): item = int(item)
|
|
||||||
if isinstance(item, int):
|
|
||||||
item = abs(item)
|
|
||||||
if item > value: item = value
|
|
||||||
return str(item)
|
|
||||||
|
|
||||||
def format_length(mutagen_length):
|
|
||||||
if convert_format(mutagen_length):
|
|
||||||
""" Convert mutagen length to airtime length """
|
|
||||||
t = float(mutagen_length)
|
|
||||||
h = int(math.floor(t / 3600))
|
|
||||||
t = t % 3600
|
|
||||||
m = int(math.floor(t / 60))
|
|
||||||
s = t % 60
|
|
||||||
# will be ss.uuu
|
|
||||||
s = str('{0:f}'.format(s))
|
|
||||||
seconds = s.split(".")
|
|
||||||
s = seconds[0]
|
|
||||||
# have a maximum of 6 subseconds.
|
|
||||||
if len(seconds[1]) >= 6: ss = seconds[1][0:6]
|
|
||||||
else: ss = seconds[1][0:]
|
|
||||||
return "%s:%s:%s.%s" % (h, m, s, ss)
|
|
||||||
|
|
||||||
def convert_format(value):
|
|
||||||
regCompiled = re.compile("^[0-9][0-9]:[0-9][0-9]:[0-9][0-9](\.\d+)?$")
|
|
||||||
if re.search(regCompiled, str(value)) is None:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import doctest
|
|
||||||
doctest.testmod()
|
|
|
@ -1,56 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from exceptions import BadSongFile
|
|
||||||
from log import Loggable
|
|
||||||
from ..saas.thread import apc, InstanceInheritingThread
|
|
||||||
|
|
||||||
class ThreadedRequestSync(InstanceInheritingThread, Loggable):
|
|
||||||
def __init__(self, rs):
|
|
||||||
super(ThreadedRequestSync, self).__init__()
|
|
||||||
self.rs = rs
|
|
||||||
self.daemon = True
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.rs.run_request()
|
|
||||||
|
|
||||||
class RequestSync(Loggable):
|
|
||||||
""" This class is responsible for making the api call to send a
|
|
||||||
request to airtime. In the process it packs the requests and retries
|
|
||||||
for some number of times """
|
|
||||||
@classmethod
|
|
||||||
def create_with_api_client(cls, watcher, requests):
|
|
||||||
apiclient = apc()
|
|
||||||
self = cls(watcher, requests, apiclient)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __init__(self, watcher, requests, apiclient):
|
|
||||||
self.watcher = watcher
|
|
||||||
self.requests = requests
|
|
||||||
self.apiclient = apiclient
|
|
||||||
|
|
||||||
def run_request(self):
|
|
||||||
self.logger.info("Attempting request with %d items." %
|
|
||||||
len(self.requests))
|
|
||||||
packed_requests = []
|
|
||||||
for request_event in self.requests:
|
|
||||||
try:
|
|
||||||
for request in request_event.safe_pack():
|
|
||||||
if isinstance(request, BadSongFile):
|
|
||||||
self.logger.info("Bad song file: '%s'" % request.path)
|
|
||||||
else: packed_requests.append(request)
|
|
||||||
except Exception as e:
|
|
||||||
self.unexpected_exception( e )
|
|
||||||
if hasattr(request_event, 'path'):
|
|
||||||
self.logger.info("Possibly related to path: '%s'" %
|
|
||||||
request_event.path)
|
|
||||||
try: self.apiclient.send_media_monitor_requests( packed_requests )
|
|
||||||
# most likely we did not get json response as we expected
|
|
||||||
except ValueError:
|
|
||||||
self.logger.info("ApiController.php probably crashed, we \
|
|
||||||
diagnose this from the fact that it did not return \
|
|
||||||
valid json")
|
|
||||||
except Exception as e: self.unexpected_exception(e)
|
|
||||||
else: self.logger.info("Request was successful")
|
|
||||||
self.watcher.flag_done() # poor man's condition variable
|
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
from log import Loggable
|
|
||||||
from exceptions import NoDirectoryInAirtime
|
|
||||||
from ..saas.thread import user
|
|
||||||
from os.path import normpath, join
|
|
||||||
import pure as mmp
|
|
||||||
|
|
||||||
class AirtimeDB(Loggable):
|
|
||||||
def __init__(self, apc, reload_now=True):
|
|
||||||
self.apc = apc
|
|
||||||
if reload_now: self.reload_directories()
|
|
||||||
|
|
||||||
def reload_directories(self):
|
|
||||||
""" this is the 'real' constructor, should be called if you ever
|
|
||||||
want the class reinitialized. there's not much point to doing
|
|
||||||
it yourself however, you should just create a new AirtimeDB
|
|
||||||
instance. """
|
|
||||||
|
|
||||||
saas = user().root_path
|
|
||||||
|
|
||||||
try:
|
|
||||||
# dirs_setup is a dict with keys:
|
|
||||||
# u'watched_dirs' and u'stor' which point to lists of corresponding
|
|
||||||
# dirs
|
|
||||||
dirs_setup = self.apc.setup_media_monitor()
|
|
||||||
dirs_setup[u'stor'] = normpath( join(saas, dirs_setup[u'stor'] ) )
|
|
||||||
dirs_setup[u'watched_dirs'] = map(lambda p: normpath(join(saas,p)),
|
|
||||||
dirs_setup[u'watched_dirs'])
|
|
||||||
dirs_with_id = dict([ (k,normpath(v)) for k,v in
|
|
||||||
self.apc.list_all_watched_dirs()['dirs'].iteritems() ])
|
|
||||||
|
|
||||||
self.id_to_dir = dirs_with_id
|
|
||||||
self.dir_to_id = dict([ (v,k) for k,v in dirs_with_id.iteritems() ])
|
|
||||||
|
|
||||||
self.base_storage = dirs_setup[u'stor']
|
|
||||||
self.storage_paths = mmp.expand_storage( self.base_storage )
|
|
||||||
self.base_id = self.dir_to_id[self.base_storage]
|
|
||||||
|
|
||||||
# hack to get around annoying schema of airtime db
|
|
||||||
self.dir_to_id[ self.recorded_path() ] = self.base_id
|
|
||||||
self.dir_to_id[ self.import_path() ] = self.base_id
|
|
||||||
|
|
||||||
# We don't know from the x_to_y dict which directory is watched or
|
|
||||||
# store...
|
|
||||||
self.watched_directories = set([ os.path.normpath(p) for p in
|
|
||||||
dirs_setup[u'watched_dirs'] ])
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.info(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
def to_id(self, directory):
|
|
||||||
""" directory path -> id """
|
|
||||||
return self.dir_to_id[ directory ]
|
|
||||||
|
|
||||||
def to_directory(self, dir_id):
|
|
||||||
""" id -> directory path """
|
|
||||||
return self.id_to_dir[ dir_id ]
|
|
||||||
|
|
||||||
def storage_path(self) : return self.base_storage
|
|
||||||
def organize_path(self) : return self.storage_paths['organize']
|
|
||||||
def problem_path(self) : return self.storage_paths['problem_files']
|
|
||||||
def import_path(self) : return self.storage_paths['imported']
|
|
||||||
def recorded_path(self) : return self.storage_paths['recorded']
|
|
||||||
|
|
||||||
def list_watched(self):
|
|
||||||
""" returns all watched directories as a list """
|
|
||||||
return list(self.watched_directories)
|
|
||||||
|
|
||||||
def list_storable_paths(self):
|
|
||||||
""" returns a list of all the watched directories in the
|
|
||||||
datatabase. (Includes the imported directory and the recorded
|
|
||||||
directory) """
|
|
||||||
l = self.list_watched()
|
|
||||||
l.append(self.import_path())
|
|
||||||
l.append(self.recorded_path())
|
|
||||||
return l
|
|
||||||
|
|
||||||
def dir_id_get_files(self, dir_id, all_files=True):
|
|
||||||
""" Get all files in a directory with id dir_id """
|
|
||||||
base_dir = self.id_to_dir[ dir_id ]
|
|
||||||
return set(( join(base_dir,p) for p in
|
|
||||||
self.apc.list_all_db_files( dir_id, all_files ) ))
|
|
||||||
|
|
||||||
def directory_get_files(self, directory, all_files=True):
|
|
||||||
""" returns all the files(recursively) in a directory. a
|
|
||||||
directory is an "actual" directory path instead of its id. This
|
|
||||||
is super hacky because you create one request for the recorded
|
|
||||||
directory and one for the imported directory even though they're
|
|
||||||
the same dir in the database so you get files for both dirs in 1
|
|
||||||
request... """
|
|
||||||
normal_dir = os.path.normpath(unicode(directory))
|
|
||||||
if normal_dir not in self.dir_to_id:
|
|
||||||
raise NoDirectoryInAirtime( normal_dir, self.dir_to_id )
|
|
||||||
all_files = self.dir_id_get_files( self.dir_to_id[normal_dir],
|
|
||||||
all_files )
|
|
||||||
if normal_dir == self.recorded_path():
|
|
||||||
all_files = [ p for p in all_files if
|
|
||||||
mmp.sub_path( self.recorded_path(), p ) ]
|
|
||||||
elif normal_dir == self.import_path():
|
|
||||||
all_files = [ p for p in all_files if
|
|
||||||
mmp.sub_path( self.import_path(), p ) ]
|
|
||||||
elif normal_dir == self.storage_path():
|
|
||||||
self.logger.info("Warning, you're getting all files in '%s' which \
|
|
||||||
includes imported + record" % normal_dir)
|
|
||||||
return set(all_files)
|
|
||||||
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import pure as mmp
|
|
||||||
import os
|
|
||||||
from log import Loggable
|
|
||||||
from exceptions import CouldNotCreateIndexFile
|
|
||||||
from ..saas.thread import InstanceInheritingThread
|
|
||||||
|
|
||||||
class Toucher(Loggable):
|
|
||||||
"""
|
|
||||||
Class responsible for touching a file at a certain path when called
|
|
||||||
"""
|
|
||||||
def __init__(self,path):
|
|
||||||
self.path = path
|
|
||||||
def __call__(self):
|
|
||||||
try: mmp.fondle(self.path)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.info("Failed to touch file: '%s'. Logging exception." %
|
|
||||||
self.path)
|
|
||||||
self.logger.info(str(e))
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
class RepeatTimer(InstanceInheritingThread):
|
|
||||||
def __init__(self, interval, callable, *args, **kwargs):
|
|
||||||
super(RepeatTimer, self).__init__()
|
|
||||||
self.interval = interval
|
|
||||||
self.callable = callable
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
def run(self):
|
|
||||||
while True:
|
|
||||||
time.sleep(self.interval)
|
|
||||||
self.callable(*self.args, **self.kwargs)
|
|
||||||
|
|
||||||
class ToucherThread(Loggable):
|
|
||||||
""" Creates a thread that touches a file 'path' every 'interval'
|
|
||||||
seconds """
|
|
||||||
def __init__(self, path, interval=5):
|
|
||||||
if not os.path.exists(path):
|
|
||||||
try:
|
|
||||||
# TODO : rewrite using with?
|
|
||||||
f = open(path,'w')
|
|
||||||
f.write('')
|
|
||||||
f.close()
|
|
||||||
except Exception as e:
|
|
||||||
raise CouldNotCreateIndexFile(path,e)
|
|
||||||
cb = Toucher(path)
|
|
||||||
t = RepeatTimer(interval, cb)
|
|
||||||
t.daemon = True # thread terminates once process is done
|
|
||||||
t.start()
|
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import time
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from handler import ReportHandler
|
|
||||||
from log import Loggable
|
|
||||||
from exceptions import BadSongFile
|
|
||||||
from eventcontractor import EventContractor
|
|
||||||
from events import EventProxy
|
|
||||||
from request import ThreadedRequestSync, RequestSync
|
|
||||||
from ..saas.thread import InstanceInheritingThread, getsig
|
|
||||||
|
|
||||||
class TimeoutWatcher(InstanceInheritingThread,Loggable):
|
|
||||||
"""
|
|
||||||
The job of this thread is to keep an eye on WatchSyncer and force a
|
|
||||||
request whenever the requests go over time out
|
|
||||||
"""
|
|
||||||
def __init__(self, watcher, timeout=5):
|
|
||||||
self.logger.info("Created timeout thread...")
|
|
||||||
super(TimeoutWatcher, self).__init__()
|
|
||||||
self.watcher = watcher
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# We try to launch a new thread every self.timeout seconds
|
|
||||||
# so that the people do not have to wait for the queue to fill up
|
|
||||||
while True:
|
|
||||||
time.sleep(self.timeout)
|
|
||||||
# If there is any requests left we launch em. Note that this
|
|
||||||
# isn't strictly necessary since RequestSync threads already
|
|
||||||
# chain themselves
|
|
||||||
if self.watcher.requests_in_queue():
|
|
||||||
self.logger.info("We have %d requests waiting to be launched" %
|
|
||||||
self.watcher.requests_left_count())
|
|
||||||
self.watcher.request_do()
|
|
||||||
# Same for events, this behaviour is mandatory however.
|
|
||||||
if self.watcher.events_in_queue():
|
|
||||||
self.logger.info("We have %d events that are unflushed" %
|
|
||||||
self.watcher.events_left_count())
|
|
||||||
self.watcher.flush_events()
|
|
||||||
|
|
||||||
class WatchSyncer(ReportHandler,Loggable):
|
|
||||||
def __init__(self, signal, chunking_number = 100, timeout=15):
|
|
||||||
self.timeout = float(timeout)
|
|
||||||
self.chunking_number = int(chunking_number)
|
|
||||||
self.request_running = False
|
|
||||||
self.__current_thread = None
|
|
||||||
self.__requests = []
|
|
||||||
self.contractor = EventContractor()
|
|
||||||
self.__reset_queue()
|
|
||||||
|
|
||||||
tc = TimeoutWatcher(self, self.timeout)
|
|
||||||
tc.daemon = True
|
|
||||||
tc.start()
|
|
||||||
super(WatchSyncer, self).__init__(signal=getsig(signal))
|
|
||||||
|
|
||||||
def handle(self, sender, event):
|
|
||||||
"""
|
|
||||||
We implement this abstract method from ReportHandler
|
|
||||||
"""
|
|
||||||
if hasattr(event, 'pack'):
|
|
||||||
# We push this event into queue
|
|
||||||
self.logger.info("Received event '%s'. Path: '%s'" % \
|
|
||||||
( event.__class__.__name__,
|
|
||||||
getattr(event,'path','No path exists') ))
|
|
||||||
try:
|
|
||||||
# If there is a strange bug anywhere in the code the next line
|
|
||||||
# should be a suspect
|
|
||||||
ev = EventProxy(event)
|
|
||||||
if self.contractor.register(ev): self.push_queue(ev)
|
|
||||||
#self.push_queue( event )
|
|
||||||
except BadSongFile as e:
|
|
||||||
self.fatal_exception("Received bas song file '%s'" % e.path, e)
|
|
||||||
except Exception as e: self.unexpected_exception(e)
|
|
||||||
else:
|
|
||||||
self.logger.info("Received event that does not implement packing.\
|
|
||||||
Printing its representation:")
|
|
||||||
self.logger.info( repr(event) )
|
|
||||||
|
|
||||||
def requests_left_count(self):
|
|
||||||
"""
|
|
||||||
returns the number of requests left in the queue. requests are
|
|
||||||
functions that create RequestSync threads
|
|
||||||
"""
|
|
||||||
return len(self.__requests)
|
|
||||||
def events_left_count(self):
|
|
||||||
"""
|
|
||||||
Returns the number of events left in the queue to create a request
|
|
||||||
"""
|
|
||||||
return len(self.__queue)
|
|
||||||
|
|
||||||
def push_queue(self, elem):
|
|
||||||
"""
|
|
||||||
Added 'elem' to the event queue and launch a request if we are
|
|
||||||
over the the chunking number
|
|
||||||
"""
|
|
||||||
self.logger.info("Added event into queue")
|
|
||||||
if self.events_left_count() >= self.chunking_number:
|
|
||||||
self.push_request()
|
|
||||||
self.request_do() # Launch the request if nothing is running
|
|
||||||
self.__queue.append(elem)
|
|
||||||
|
|
||||||
def flush_events(self):
|
|
||||||
"""
|
|
||||||
Force flush the current events held in the queue
|
|
||||||
"""
|
|
||||||
self.logger.info("Force flushing events...")
|
|
||||||
self.push_request()
|
|
||||||
self.request_do()
|
|
||||||
|
|
||||||
def events_in_queue(self):
|
|
||||||
"""
|
|
||||||
returns true if there are events in the queue that haven't been
|
|
||||||
processed yet
|
|
||||||
"""
|
|
||||||
return len(self.__queue) > 0
|
|
||||||
|
|
||||||
def requests_in_queue(self):
|
|
||||||
"""
|
|
||||||
Returns true if there are any requests in the queue. False otherwise.
|
|
||||||
"""
|
|
||||||
return len(self.__requests) > 0
|
|
||||||
|
|
||||||
def flag_done(self):
|
|
||||||
"""
|
|
||||||
called by request thread when it finishes operating
|
|
||||||
"""
|
|
||||||
self.request_running = False
|
|
||||||
self.__current_thread = None
|
|
||||||
# This call might not be necessary but we would like to get the
|
|
||||||
# ball running with the requests as soon as possible
|
|
||||||
if self.requests_in_queue() > 0: self.request_do()
|
|
||||||
|
|
||||||
def request_do(self):
|
|
||||||
"""
|
|
||||||
launches a request thread only if one is not running right now
|
|
||||||
"""
|
|
||||||
if not self.request_running:
|
|
||||||
self.request_running = True
|
|
||||||
self.__requests.pop()()
|
|
||||||
|
|
||||||
def push_request(self):
|
|
||||||
"""
|
|
||||||
Create a request from the current events in the queue and schedule it
|
|
||||||
"""
|
|
||||||
self.logger.info("WatchSyncer : Unleashing request")
|
|
||||||
# want to do request asyncly and empty the queue
|
|
||||||
requests = copy.copy(self.__queue)
|
|
||||||
def launch_request():
|
|
||||||
# Need shallow copy here
|
|
||||||
t = ThreadedRequestSync( RequestSync.create_with_api_client(
|
|
||||||
watcher=self, requests=requests) )
|
|
||||||
self.__current_thread = t
|
|
||||||
self.__requests.append(launch_request)
|
|
||||||
self.__reset_queue()
|
|
||||||
|
|
||||||
def __reset_queue(self): self.__queue = []
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
#this destructor is completely untested and it's unclear whether
|
|
||||||
#it's even doing anything useful. consider removing it
|
|
||||||
if self.events_in_queue():
|
|
||||||
self.logger.warn("Terminating with events still in the queue...")
|
|
||||||
if self.requests_in_queue():
|
|
||||||
self.logger.warn("Terminating with http requests still pending...")
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import os
|
|
||||||
from os.path import join, basename, dirname
|
|
||||||
|
|
||||||
from ..monitor.exceptions import NoConfigFile
|
|
||||||
from ..monitor.pure import LazyProperty
|
|
||||||
from ..monitor.config import MMConfig
|
|
||||||
from ..monitor.owners import Owner
|
|
||||||
from ..monitor.events import EventRegistry
|
|
||||||
from ..monitor.listeners import FileMediator
|
|
||||||
from api_clients.api_client import AirtimeApiClient
|
|
||||||
|
|
||||||
# poor man's phantom types...
|
|
||||||
class SignalString(str): pass
|
|
||||||
|
|
||||||
class AirtimeInstance(object):
|
|
||||||
""" AirtimeInstance is a class that abstracts away every airtime
|
|
||||||
instance by providing all the necessary objects required to interact
|
|
||||||
with the instance. ApiClient, configs, root_directory """
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def root_make(cls, name, root):
|
|
||||||
cfg = {
|
|
||||||
'api_client' : join(root, 'etc/airtime/api_client.cfg'),
|
|
||||||
'media_monitor' : join(root, 'etc/airtime/airtime.conf'),
|
|
||||||
}
|
|
||||||
return cls(name, root, cfg)
|
|
||||||
|
|
||||||
def __init__(self,name, root_path, config_paths):
|
|
||||||
""" name is an internal name only """
|
|
||||||
for cfg in ['api_client','media_monitor']:
|
|
||||||
if cfg not in config_paths: raise NoConfigFile(config_paths)
|
|
||||||
elif not os.path.exists(config_paths[cfg]):
|
|
||||||
raise NoConfigFile(config_paths[cfg])
|
|
||||||
self.name = name
|
|
||||||
self.config_paths = config_paths
|
|
||||||
self.root_path = root_path
|
|
||||||
|
|
||||||
def signal(self, sig):
|
|
||||||
if isinstance(sig, SignalString): return sig
|
|
||||||
else: return SignalString("%s_%s" % (self.name, sig))
|
|
||||||
|
|
||||||
def touch_file_path(self):
|
|
||||||
""" Get the path of the touch file for every instance """
|
|
||||||
touch_base_path = self.mm_config['media-monitor']['index_path']
|
|
||||||
touch_base_name = basename(touch_base_path)
|
|
||||||
new_base_name = self.name + touch_base_name
|
|
||||||
return join(dirname(touch_base_path), new_base_name)
|
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s,%s(%s)" % (self.name, self.root_path, self.config_paths)
|
|
||||||
|
|
||||||
@LazyProperty
|
|
||||||
def api_client(self):
|
|
||||||
return AirtimeApiClient(config_path=self.config_paths['api_client'])
|
|
||||||
|
|
||||||
@LazyProperty
|
|
||||||
def mm_config(self):
|
|
||||||
return MMConfig(self.config_paths['media_monitor'])
|
|
||||||
|
|
||||||
# I'm well aware that I'm using the service locator pattern
|
|
||||||
# instead of normal constructor injection as I should be.
|
|
||||||
# It's recommended to rewrite this using proper constructor injection
|
|
||||||
|
|
||||||
@LazyProperty
|
|
||||||
def owner(self): return Owner()
|
|
||||||
|
|
||||||
@LazyProperty
|
|
||||||
def event_registry(self): return EventRegistry()
|
|
||||||
|
|
||||||
@LazyProperty
|
|
||||||
def file_mediator(self): return FileMediator()
|
|
||||||
|
|
|
@ -1,133 +0,0 @@
|
||||||
import os, sys
|
|
||||||
import logging
|
|
||||||
import logging.config
|
|
||||||
|
|
||||||
from ..monitor import pure as mmp
|
|
||||||
|
|
||||||
from ..monitor.exceptions import FailedToObtainLocale, FailedToSetLocale
|
|
||||||
from ..monitor.log import get_logger, setup_logging
|
|
||||||
from std_err_override import LogWriter
|
|
||||||
from ..saas.thread import InstanceThread, user, apc, getsig
|
|
||||||
from ..monitor.log import Loggable
|
|
||||||
from ..monitor.exceptions import CouldNotCreateIndexFile
|
|
||||||
from ..monitor.toucher import ToucherThread
|
|
||||||
from ..monitor.airtime import AirtimeNotifier, AirtimeMessageReceiver
|
|
||||||
from ..monitor.watchersyncer import WatchSyncer
|
|
||||||
from ..monitor.eventdrainer import EventDrainer
|
|
||||||
from ..monitor.manager import Manager
|
|
||||||
from ..monitor.syncdb import AirtimeDB
|
|
||||||
from airtimeinstance import AirtimeInstance
|
|
||||||
|
|
||||||
class MM2(InstanceThread, Loggable):
|
|
||||||
|
|
||||||
def index_create(self, index_create_attempt=False):
|
|
||||||
config = user().mm_config
|
|
||||||
if not index_create_attempt:
|
|
||||||
if not os.path.exists(config['media-monitor']['index_path']):
|
|
||||||
self.logger.info("Attempting to create index file:...")
|
|
||||||
try:
|
|
||||||
with open(config['media-monitor']['index_path'], 'w') as f: f.write(" ")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.info("Failed to create index file with exception: %s" \
|
|
||||||
% str(e))
|
|
||||||
else:
|
|
||||||
self.logger.info("Created index file, reloading configuration:")
|
|
||||||
self.index_create(index_create_attempt=True)
|
|
||||||
else:
|
|
||||||
self.logger.info("Already tried to create index. Will not try again ")
|
|
||||||
|
|
||||||
if not os.path.exists(config['media-monitor']['index_path']):
|
|
||||||
raise CouldNotCreateIndexFile(config['media-monitor']['index_path'])
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.index_create()
|
|
||||||
manager = Manager()
|
|
||||||
apiclient = apc()
|
|
||||||
config = user().mm_config
|
|
||||||
WatchSyncer(signal=getsig('watch'),
|
|
||||||
chunking_number=config['media-monitor']['chunking_number'],
|
|
||||||
timeout=config['media-monitor']['request_max_wait'])
|
|
||||||
airtime_receiver = AirtimeMessageReceiver(config,manager)
|
|
||||||
airtime_notifier = AirtimeNotifier(config, airtime_receiver)
|
|
||||||
|
|
||||||
|
|
||||||
adb = AirtimeDB(apiclient)
|
|
||||||
store = {
|
|
||||||
u'stor' : adb.storage_path(),
|
|
||||||
u'watched_dirs' : adb.list_watched(),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.logger.info("initializing mm with directories: %s" % str(store))
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
"Initing with the following airtime response:%s" % str(store))
|
|
||||||
|
|
||||||
airtime_receiver.change_storage({ 'directory':store[u'stor'] })
|
|
||||||
|
|
||||||
for watch_dir in store[u'watched_dirs']:
|
|
||||||
if not os.path.exists(watch_dir):
|
|
||||||
# Create the watch_directory here
|
|
||||||
try: os.makedirs(watch_dir)
|
|
||||||
except Exception:
|
|
||||||
self.logger.error("Could not create watch directory: '%s' \
|
|
||||||
(given from the database)." % watch_dir)
|
|
||||||
if os.path.exists(watch_dir):
|
|
||||||
airtime_receiver.new_watch({ 'directory':watch_dir }, restart=True)
|
|
||||||
else: self.logger.info("Failed to add watch on %s" % str(watch_dir))
|
|
||||||
|
|
||||||
EventDrainer(airtime_notifier,
|
|
||||||
interval=float(config['media-monitor']['rmq_event_wait']))
|
|
||||||
|
|
||||||
# Launch the toucher that updates the last time when the script was
|
|
||||||
# ran every n seconds.
|
|
||||||
# TODO : verify that this does not interfere with bootstrapping because the
|
|
||||||
# toucher thread might update the last_ran variable too fast
|
|
||||||
ToucherThread(path=user().touch_file_path(),
|
|
||||||
interval=int(config['media-monitor']['touch_interval']))
|
|
||||||
|
|
||||||
success = False
|
|
||||||
while not success:
|
|
||||||
try:
|
|
||||||
apiclient.register_component('media-monitor')
|
|
||||||
success = True
|
|
||||||
except Exception, e:
|
|
||||||
self.logger.error(str(e))
|
|
||||||
import time
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
manager.loop()
|
|
||||||
|
|
||||||
def launch_instance(name, root, global_cfg):
|
|
||||||
cfg = {
|
|
||||||
'api_client' : global_cfg,
|
|
||||||
'media_monitor' : global_cfg,
|
|
||||||
}
|
|
||||||
ai = AirtimeInstance(name, root, cfg)
|
|
||||||
MM2(ai).start()
|
|
||||||
|
|
||||||
def setup_global(log):
|
|
||||||
""" setup unicode and other stuff """
|
|
||||||
log.info("Attempting to set the locale...")
|
|
||||||
try: mmp.configure_locale(mmp.get_system_locale())
|
|
||||||
except FailedToSetLocale as e:
|
|
||||||
log.info("Failed to set the locale...")
|
|
||||||
sys.exit(1)
|
|
||||||
except FailedToObtainLocale as e:
|
|
||||||
log.info("Failed to obtain the locale form the default path: \
|
|
||||||
'/etc/default/locale'")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
log.info("Failed to set the locale for unknown reason. \
|
|
||||||
Logging exception.")
|
|
||||||
log.info(str(e))
|
|
||||||
|
|
||||||
def setup_logger(log_config, logpath):
|
|
||||||
logging.config.fileConfig(log_config)
|
|
||||||
#need to wait for Python 2.7 for this..
|
|
||||||
#logging.captureWarnings(True)
|
|
||||||
logger = logging.getLogger()
|
|
||||||
LogWriter.override_std_err(logger)
|
|
||||||
logfile = unicode(logpath)
|
|
||||||
setup_logging(logfile)
|
|
||||||
log = get_logger()
|
|
||||||
return log
|
|
|
@ -1,28 +0,0 @@
|
||||||
import threading
|
|
||||||
|
|
||||||
class UserlessThread(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Current thread: %s is not an instance of InstanceThread \
|
|
||||||
of InstanceInheritingThread" % str(threading.current_thread())
|
|
||||||
|
|
||||||
class HasUser(object):
|
|
||||||
def user(self): return self._user
|
|
||||||
def assign_user(self): self._user = threading.current_thread().user()
|
|
||||||
|
|
||||||
class InstanceThread(threading.Thread, HasUser):
|
|
||||||
def __init__(self,user, *args, **kwargs):
|
|
||||||
super(InstanceThread, self).__init__(*args, **kwargs)
|
|
||||||
self._user = user
|
|
||||||
|
|
||||||
class InstanceInheritingThread(threading.Thread, HasUser):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.assign_user()
|
|
||||||
super(InstanceInheritingThread, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def user():
|
|
||||||
try: return threading.current_thread().user()
|
|
||||||
except AttributeError: raise UserlessThread()
|
|
||||||
|
|
||||||
def apc(): return user().api_client
|
|
||||||
|
|
||||||
def getsig(s): return user().signal(s)
|
|
|
@ -1,34 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from media.saas.launcher import setup_global, launch_instance, setup_logger
|
|
||||||
from media.monitor.config import MMConfig
|
|
||||||
|
|
||||||
def main(global_config, log_config):
|
|
||||||
""" function to run hosted install """
|
|
||||||
mm_config = MMConfig(global_config)
|
|
||||||
log = setup_logger( log_config, mm_config['media-monitor']['logpath'] )
|
|
||||||
setup_global(log)
|
|
||||||
launch_instance('hosted_install', '/', global_config)
|
|
||||||
|
|
||||||
__doc__ = """
|
|
||||||
Usage:
|
|
||||||
mm2.py --config=<path> --apiclient=<path> --log=<path>
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h --help Show this screen
|
|
||||||
--config=<path> path to mm2 config
|
|
||||||
--apiclient=<path> path to apiclient config
|
|
||||||
--log=<path> log config at <path>
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from docopt import docopt
|
|
||||||
args = docopt(__doc__,version="mm1.99")
|
|
||||||
for k in ['--apiclient','--config','--log']:
|
|
||||||
if not os.path.exists(args[k]):
|
|
||||||
print("'%s' must exist" % args[k])
|
|
||||||
sys.exit(0)
|
|
||||||
print("Running mm1.99")
|
|
||||||
main(args['--config'],args['--apiclient'],args['--log'])
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import getopt
|
|
||||||
import pyinotify
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
# a little script to test out pyinotify events
|
|
||||||
|
|
||||||
class AT(pyinotify.ProcessEvent):
|
|
||||||
def process_default(self, event):
|
|
||||||
pprint.pprint(event)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
optlist, arguments = getopt.getopt(sys.argv[1:], '', ["dir="])
|
|
||||||
ldir = ""
|
|
||||||
for k,v in optlist:
|
|
||||||
if k == '--dir':
|
|
||||||
ldir = v
|
|
||||||
break
|
|
||||||
if not os.path.exists(ldir):
|
|
||||||
print("can't pyinotify dir: '%s'. it don't exist" % ldir)
|
|
||||||
sys.exit(0)
|
|
||||||
wm = pyinotify.WatchManager()
|
|
||||||
notifier = pyinotify.Notifier(wm)
|
|
||||||
print("Watching: '%s'" % ldir)
|
|
||||||
wm.add_watch(ldir, pyinotify.ALL_EVENTS, auto_add=True, rec=True, proc_fun=AT())
|
|
||||||
notifier.loop()
|
|
||||||
|
|
||||||
if __name__ == '__main__': main()
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
bin_dir = "/usr/lib/airtime/api_clients"
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Common
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# Value needed to access the API
|
|
||||||
api_key = '3MP2IUR45E6KYQ01CUYK'
|
|
||||||
|
|
||||||
# Path to the base of the API
|
|
||||||
api_base = 'api'
|
|
||||||
|
|
||||||
# URL to get the version number of the server API
|
|
||||||
version_url = 'version/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
#URL to register a components IP Address with the central web server
|
|
||||||
register_component = 'register-component/format/json/api_key/%%api_key%%/component/%%component%%'
|
|
||||||
|
|
||||||
# Hostname
|
|
||||||
base_url = 'localhost'
|
|
||||||
base_port = 80
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Config for Media Monitor
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# URL to setup the media monitor
|
|
||||||
media_setup_url = 'media-monitor-setup/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# Tell Airtime the file id associated with a show instance.
|
|
||||||
upload_recorded = 'upload-recorded/format/json/api_key/%%api_key%%/fileid/%%fileid%%/showinstanceid/%%showinstanceid%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime to update file's meta data
|
|
||||||
update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%/mode/%%mode%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want a listing of all files it knows about
|
|
||||||
list_all_db_files = 'list-all-files/format/json/api_key/%%api_key%%/dir_id/%%dir_id%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want a listing of all dirs its watching (including the stor dir)
|
|
||||||
list_all_watched_dirs = 'list-all-watched-dirs/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to add watched directory
|
|
||||||
add_watched_dir = 'add-watched-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to add watched directory
|
|
||||||
remove_watched_dir = 'remove-watched-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to add watched directory
|
|
||||||
set_storage_dir = 'set-storage-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime about file system mount change
|
|
||||||
update_fs_mount = 'update-file-system-mount/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime about file system mount change
|
|
||||||
handle_watched_dir_missing = 'handle-watched-dir-missing/format/json/api_key/%%api_key%%/dir/%%dir%%'
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Config for Recorder
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# URL to get the schedule of shows set to record
|
|
||||||
show_schedule_url = 'recorded-shows/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to upload the recorded show's file to Airtime
|
|
||||||
upload_file_url = 'upload-file/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to commit multiple updates from media monitor at the same time
|
|
||||||
|
|
||||||
reload_metadata_group = 'reload-metadata-group/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
#number of retries to upload file if connection problem
|
|
||||||
upload_retries = 3
|
|
||||||
|
|
||||||
#time to wait between attempts to upload file if connection problem (in seconds)
|
|
||||||
upload_wait = 60
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Uncomment *one of the sets* of values from the API clients below, and comment
|
|
||||||
# out all the others.
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Config for Pypo
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# Schedule export path.
|
|
||||||
# %%from%% - starting date/time in the form YYYY-MM-DD-hh-mm
|
|
||||||
# %%to%% - starting date/time in the form YYYY-MM-DD-hh-mm
|
|
||||||
export_url = 'schedule/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
get_media_url = 'get-media/file/%%file%%/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# Update whether a schedule group has begun playing.
|
|
||||||
update_item_url = 'notify-schedule-group-play/api_key/%%api_key%%/schedule_id/%%schedule_id%%'
|
|
||||||
|
|
||||||
# Update whether an audio clip is currently playing.
|
|
||||||
update_start_playing_url = 'notify-media-item-start-play/api_key/%%api_key%%/media_id/%%media_id%%/schedule_id/%%schedule_id%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to get stream setting
|
|
||||||
get_stream_setting = 'get-stream-setting/format/json/api_key/%%api_key%%/'
|
|
||||||
|
|
||||||
#URL to update liquidsoap status
|
|
||||||
update_liquidsoap_status = 'update-liquidsoap-status/format/json/api_key/%%api_key%%/msg/%%msg%%/stream_id/%%stream_id%%/boot_time/%%boot_time%%'
|
|
||||||
|
|
||||||
#URL to check live stream auth
|
|
||||||
check_live_stream_auth = 'check-live-stream-auth/format/json/api_key/%%api_key%%/username/%%username%%/password/%%password%%/djtype/%%djtype%%'
|
|
||||||
|
|
||||||
#URL to update source status
|
|
||||||
update_source_status = 'update-source-status/format/json/api_key/%%api_key%%/sourcename/%%sourcename%%/status/%%status%%'
|
|
||||||
|
|
||||||
get_bootstrap_info = 'get-bootstrap-info/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
get_files_without_replay_gain = 'get-files-without-replay-gain/api_key/%%api_key%%/dir_id/%%dir_id%%'
|
|
||||||
|
|
||||||
update_replay_gain_value = 'update-replay-gain-value/api_key/%%api_key%%'
|
|
|
@ -1,138 +0,0 @@
|
||||||
bin_dir = "/usr/lib/airtime/api_clients"
|
|
||||||
|
|
||||||
############################################
|
|
||||||
# RabbitMQ settings #
|
|
||||||
############################################
|
|
||||||
rabbitmq_host = 'localhost'
|
|
||||||
rabbitmq_user = 'guest'
|
|
||||||
rabbitmq_password = 'guest'
|
|
||||||
rabbitmq_vhost = '/'
|
|
||||||
|
|
||||||
############################################
|
|
||||||
# Media-Monitor preferences #
|
|
||||||
############################################
|
|
||||||
check_filesystem_events = 5 #how long to queue up events performed on the files themselves.
|
|
||||||
check_airtime_events = 30 #how long to queue metadata input from airtime.
|
|
||||||
|
|
||||||
touch_interval = 5
|
|
||||||
chunking_number = 450
|
|
||||||
request_max_wait = 3.0
|
|
||||||
rmq_event_wait = 0.5
|
|
||||||
logpath = '/home/rudi/throwaway/mm2.log'
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Common
|
|
||||||
#############################
|
|
||||||
|
|
||||||
|
|
||||||
index_path = '/home/rudi/Airtime/python_apps/media-monitor2/sample_post.txt'
|
|
||||||
|
|
||||||
# Value needed to access the API
|
|
||||||
api_key = '5LF5D953RNS3KJSHN6FF'
|
|
||||||
|
|
||||||
# Path to the base of the API
|
|
||||||
api_base = 'api'
|
|
||||||
|
|
||||||
# URL to get the version number of the server API
|
|
||||||
version_url = 'version/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
#URL to register a components IP Address with the central web server
|
|
||||||
register_component = 'register-component/format/json/api_key/%%api_key%%/component/%%component%%'
|
|
||||||
|
|
||||||
# Hostname
|
|
||||||
base_url = 'localhost'
|
|
||||||
base_port = 80
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Config for Media Monitor
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# URL to setup the media monitor
|
|
||||||
media_setup_url = 'media-monitor-setup/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# Tell Airtime the file id associated with a show instance.
|
|
||||||
upload_recorded = 'upload-recorded/format/json/api_key/%%api_key%%/fileid/%%fileid%%/showinstanceid/%%showinstanceid%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime to update file's meta data
|
|
||||||
update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%/mode/%%mode%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want a listing of all files it knows about
|
|
||||||
list_all_db_files = 'list-all-files/format/json/api_key/%%api_key%%/dir_id/%%dir_id%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want a listing of all dirs its watching (including the stor dir)
|
|
||||||
list_all_watched_dirs = 'list-all-watched-dirs/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to add watched directory
|
|
||||||
add_watched_dir = 'add-watched-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to add watched directory
|
|
||||||
remove_watched_dir = 'remove-watched-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to add watched directory
|
|
||||||
set_storage_dir = 'set-storage-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime about file system mount change
|
|
||||||
update_fs_mount = 'update-file-system-mount/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime about file system mount change
|
|
||||||
handle_watched_dir_missing = 'handle-watched-dir-missing/format/json/api_key/%%api_key%%/dir/%%dir%%'
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Config for Recorder
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# URL to get the schedule of shows set to record
|
|
||||||
show_schedule_url = 'recorded-shows/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to upload the recorded show's file to Airtime
|
|
||||||
upload_file_url = 'upload-file/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# URL to commit multiple updates from media monitor at the same time
|
|
||||||
|
|
||||||
reload_metadata_group = 'reload-metadata-group/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
#number of retries to upload file if connection problem
|
|
||||||
upload_retries = 3
|
|
||||||
|
|
||||||
#time to wait between attempts to upload file if connection problem (in seconds)
|
|
||||||
upload_wait = 60
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Uncomment *one of the sets* of values from the API clients below, and comment
|
|
||||||
# out all the others.
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## Config for Pypo
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# Schedule export path.
|
|
||||||
# %%from%% - starting date/time in the form YYYY-MM-DD-hh-mm
|
|
||||||
# %%to%% - starting date/time in the form YYYY-MM-DD-hh-mm
|
|
||||||
export_url = 'schedule/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
get_media_url = 'get-media/file/%%file%%/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
# Update whether a schedule group has begun playing.
|
|
||||||
update_item_url = 'notify-schedule-group-play/api_key/%%api_key%%/schedule_id/%%schedule_id%%'
|
|
||||||
|
|
||||||
# Update whether an audio clip is currently playing.
|
|
||||||
update_start_playing_url = 'notify-media-item-start-play/api_key/%%api_key%%/media_id/%%media_id%%/schedule_id/%%schedule_id%%'
|
|
||||||
|
|
||||||
# URL to tell Airtime we want to get stream setting
|
|
||||||
get_stream_setting = 'get-stream-setting/format/json/api_key/%%api_key%%/'
|
|
||||||
|
|
||||||
#URL to update liquidsoap status
|
|
||||||
update_liquidsoap_status = 'update-liquidsoap-status/format/json/api_key/%%api_key%%/msg/%%msg%%/stream_id/%%stream_id%%/boot_time/%%boot_time%%'
|
|
||||||
|
|
||||||
#URL to check live stream auth
|
|
||||||
check_live_stream_auth = 'check-live-stream-auth/format/json/api_key/%%api_key%%/username/%%username%%/password/%%password%%/djtype/%%djtype%%'
|
|
||||||
|
|
||||||
#URL to update source status
|
|
||||||
update_source_status = 'update-source-status/format/json/api_key/%%api_key%%/sourcename/%%sourcename%%/status/%%status%%'
|
|
||||||
|
|
||||||
get_bootstrap_info = 'get-bootstrap-info/format/json/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
get_files_without_replay_gain = 'get-files-without-replay-gain/api_key/%%api_key%%/dir_id/%%dir_id%%'
|
|
||||||
|
|
||||||
update_replay_gain_value = 'update-replay-gain-value/api_key/%%api_key%%'
|
|
|
@ -1,14 +0,0 @@
|
||||||
# The tests rely on a lot of absolute paths so this file
|
|
||||||
# configures all of that
|
|
||||||
|
|
||||||
music_folder = u'/home/rudi/music'
|
|
||||||
o_path = u'/home/rudi/throwaway/ACDC_-_Back_In_Black-sample-64kbps.ogg'
|
|
||||||
watch_path = u'/home/rudi/throwaway/watch/',
|
|
||||||
real_path1 = u'/home/rudi/throwaway/watch/unknown/unknown/ACDC_-_Back_In_Black-sample-64kbps-64kbps.ogg'
|
|
||||||
opath = u"/home/rudi/Airtime/python_apps/media-monitor2/tests/"
|
|
||||||
ppath = u"/home/rudi/Airtime/python_apps/media-monitor2/media/"
|
|
||||||
|
|
||||||
api_client_path = '/etc/airtime/airtime.conf'
|
|
||||||
# holdover from the time we had a special config for testing
|
|
||||||
sample_config = api_client_path
|
|
||||||
real_config = api_client_path
|
|
|
@ -1,7 +0,0 @@
|
||||||
#!/usr/bin/perl
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
|
|
||||||
foreach my $file (glob "*.py") {
|
|
||||||
system("python $file") unless $file =~ /prepare_tests.py/;
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from api_clients import api_client as apc
|
|
||||||
|
|
||||||
|
|
||||||
import prepare_tests
|
|
||||||
|
|
||||||
class TestApiClient(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
test_path = prepare_tests.api_client_path
|
|
||||||
print("Running from api_config: %s" % test_path)
|
|
||||||
if not os.path.exists(test_path):
|
|
||||||
print("path for config does not exist: '%s' % test_path")
|
|
||||||
# TODO : is there a cleaner way to exit the unit testing?
|
|
||||||
sys.exit(1)
|
|
||||||
self.apc = apc.AirtimeApiClient(config_path=test_path)
|
|
||||||
self.apc.register_component("api-client-tester")
|
|
||||||
# All of the following requests should error out in some way
|
|
||||||
self.bad_requests = [
|
|
||||||
{ 'mode' : 'foo', 'is_record' : 0 },
|
|
||||||
{ 'mode' : 'bar', 'is_record' : 1 },
|
|
||||||
{ 'no_mode' : 'at_all' }, ]
|
|
||||||
|
|
||||||
def test_bad_requests(self):
|
|
||||||
responses = self.apc.send_media_monitor_requests(self.bad_requests, dry=True)
|
|
||||||
for response in responses:
|
|
||||||
self.assertTrue( 'key' in response )
|
|
||||||
self.assertTrue( 'error' in response )
|
|
||||||
print( "Response: '%s'" % response )
|
|
||||||
|
|
||||||
# We don't actually test any well formed requests because it is more
|
|
||||||
# involved
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
api_client = 'airtime'
|
|
||||||
|
|
||||||
# where the binary files live
|
|
||||||
bin_dir = '/usr/lib/airtime/media-monitor'
|
|
||||||
|
|
||||||
# where the logging files live
|
|
||||||
log_dir = '/var/log/airtime/media-monitor'
|
|
||||||
|
|
||||||
|
|
||||||
############################################
|
|
||||||
# RabbitMQ settings #
|
|
||||||
############################################
|
|
||||||
rabbitmq_host = 'localhost'
|
|
||||||
rabbitmq_user = 'guest'
|
|
||||||
rabbitmq_password = 'guest'
|
|
||||||
rabbitmq_vhost = '/'
|
|
||||||
|
|
||||||
############################################
|
|
||||||
# Media-Monitor preferences #
|
|
||||||
############################################
|
|
||||||
check_filesystem_events = '5'
|
|
||||||
check_airtime_events = '30'
|
|
||||||
|
|
||||||
list_value_testing = 'val1', 'val2', 'val3'
|
|
|
@ -1,28 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
from media.monitor.config import MMConfig
|
|
||||||
from media.monitor.exceptions import NoConfigFile, ConfigAccessViolation
|
|
||||||
|
|
||||||
pp = pprint.PrettyPrinter(indent=4)
|
|
||||||
|
|
||||||
class TestMMConfig(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.real_config = MMConfig("./test_config.cfg")
|
|
||||||
#pp.pprint(self.real_config.cfg.dict)
|
|
||||||
|
|
||||||
def test_bad_config(self):
|
|
||||||
self.assertRaises( NoConfigFile, lambda : MMConfig("/fake/stuff/here") )
|
|
||||||
|
|
||||||
def test_no_set(self):
|
|
||||||
def myf(): self.real_config['bad'] = 'change'
|
|
||||||
self.assertRaises( ConfigAccessViolation, myf )
|
|
||||||
|
|
||||||
def test_copying(self):
|
|
||||||
k = 'list_value_testing'
|
|
||||||
mycopy = self.real_config[k]
|
|
||||||
mycopy.append("another element")
|
|
||||||
self.assertTrue( len(mycopy) , len(self.real_config[k]) + 1 )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,31 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
#from pprint import pprint as pp
|
|
||||||
|
|
||||||
from media.metadata.process import global_reader
|
|
||||||
from media.monitor.metadata import Metadata
|
|
||||||
|
|
||||||
import media.metadata.definitions as defs
|
|
||||||
defs.load_definitions()
|
|
||||||
|
|
||||||
class TestMMP(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.maxDiff = None
|
|
||||||
|
|
||||||
def metadatas(self,f):
|
|
||||||
return global_reader.read_mutagen(f), Metadata(f).extract()
|
|
||||||
|
|
||||||
def test_old_metadata(self):
|
|
||||||
path = "/home/rudi/music/Nightingale.mp3"
|
|
||||||
m = global_reader.read_mutagen(path)
|
|
||||||
self.assertTrue( len(m) > 0 )
|
|
||||||
n = Metadata(path)
|
|
||||||
self.assertEqual(n.extract(), m)
|
|
||||||
|
|
||||||
def test_recorded(self):
|
|
||||||
recorded_file = "./15:15:00-Untitled Show-256kbps.ogg"
|
|
||||||
emf, old = self.metadatas(recorded_file)
|
|
||||||
self.assertEqual(emf, old)
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,58 +0,0 @@
|
||||||
import unittest
|
|
||||||
from media.monitor.eventcontractor import EventContractor
|
|
||||||
#from media.monitor.exceptions import BadSongFile
|
|
||||||
from media.monitor.events import FakePyinotify, NewFile, MoveFile, \
|
|
||||||
DeleteFile
|
|
||||||
|
|
||||||
class TestMMP(unittest.TestCase):
|
|
||||||
def test_event_registered(self):
|
|
||||||
ev = EventContractor()
|
|
||||||
e1 = NewFile( FakePyinotify('bull.mp3') ).proxify()
|
|
||||||
e2 = MoveFile( FakePyinotify('bull.mp3') ).proxify()
|
|
||||||
ev.register(e1)
|
|
||||||
self.assertTrue( ev.event_registered(e2) )
|
|
||||||
|
|
||||||
def test_get_old_event(self):
|
|
||||||
ev = EventContractor()
|
|
||||||
e1 = NewFile( FakePyinotify('bull.mp3') ).proxify()
|
|
||||||
e2 = MoveFile( FakePyinotify('bull.mp3') ).proxify()
|
|
||||||
ev.register(e1)
|
|
||||||
self.assertEqual( ev.get_old_event(e2), e1 )
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
ev = EventContractor()
|
|
||||||
e1 = NewFile( FakePyinotify('bull.mp3') ).proxify()
|
|
||||||
e2 = DeleteFile( FakePyinotify('bull.mp3') ).proxify()
|
|
||||||
self.assertTrue( ev.register(e1) )
|
|
||||||
|
|
||||||
self.assertFalse( ev.register(e2) )
|
|
||||||
|
|
||||||
self.assertEqual( len(ev.store.keys()), 1 )
|
|
||||||
|
|
||||||
delete_ev = e1.safe_pack()[0]
|
|
||||||
self.assertEqual( delete_ev['mode'], u'delete')
|
|
||||||
self.assertEqual( len(ev.store.keys()), 0 )
|
|
||||||
|
|
||||||
e3 = DeleteFile( FakePyinotify('horse.mp3') ).proxify()
|
|
||||||
self.assertTrue( ev.register(e3) )
|
|
||||||
self.assertTrue( ev.register(e2) )
|
|
||||||
|
|
||||||
|
|
||||||
def test_register2(self):
|
|
||||||
ev = EventContractor()
|
|
||||||
p = 'bull.mp3'
|
|
||||||
events = [
|
|
||||||
NewFile( FakePyinotify(p) ),
|
|
||||||
NewFile( FakePyinotify(p) ),
|
|
||||||
DeleteFile( FakePyinotify(p) ),
|
|
||||||
NewFile( FakePyinotify(p) ),
|
|
||||||
NewFile( FakePyinotify(p) ), ]
|
|
||||||
events = map(lambda x: x.proxify(), events)
|
|
||||||
actual_events = []
|
|
||||||
for e in events:
|
|
||||||
if ev.register(e):
|
|
||||||
actual_events.append(e)
|
|
||||||
self.assertEqual( len(ev.store.keys()), 1 )
|
|
||||||
#packed = [ x.safe_pack() for x in actual_events ]
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,21 +0,0 @@
|
||||||
import unittest
|
|
||||||
from copy import deepcopy
|
|
||||||
from media.saas.airtimeinstance import AirtimeInstance, NoConfigFile
|
|
||||||
|
|
||||||
class TestAirtimeInstance(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.cfg = {
|
|
||||||
'api_client' : 'tests/test_instance.py',
|
|
||||||
'media_monitor' : 'tests/test_instance.py',
|
|
||||||
'logging' : 'tests/test_instance.py',
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_init_good(self):
|
|
||||||
AirtimeInstance("/root", self.cfg)
|
|
||||||
self.assertTrue(True)
|
|
||||||
|
|
||||||
def test_init_bad(self):
|
|
||||||
cfg = deepcopy(self.cfg)
|
|
||||||
cfg['api_client'] = 'bs'
|
|
||||||
with self.assertRaises(NoConfigFile):
|
|
||||||
AirtimeInstance("/root", cfg)
|
|
|
@ -1,77 +0,0 @@
|
||||||
import os, shutil
|
|
||||||
import time
|
|
||||||
import pyinotify
|
|
||||||
import unittest
|
|
||||||
from pydispatch import dispatcher
|
|
||||||
|
|
||||||
from media.monitor.listeners import OrganizeListener
|
|
||||||
from media.monitor.events import OrganizeFile
|
|
||||||
|
|
||||||
from os.path import join, normpath, abspath
|
|
||||||
|
|
||||||
def create_file(p):
|
|
||||||
with open(p, 'w') as f: f.write(" ")
|
|
||||||
|
|
||||||
class TestOrganizeListener(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.organize_path = 'test_o'
|
|
||||||
self.sig = 'org'
|
|
||||||
def my_abs_path(x):
|
|
||||||
return normpath(join(os.getcwd(), x))
|
|
||||||
self.sample_files = [ my_abs_path(join(self.organize_path, f))
|
|
||||||
for f in [ "gogi.mp3",
|
|
||||||
"gio.mp3",
|
|
||||||
"mimino.ogg" ] ]
|
|
||||||
os.mkdir(self.organize_path)
|
|
||||||
|
|
||||||
def test_flush_events(self):
|
|
||||||
org = self.create_org()
|
|
||||||
self.create_sample_files()
|
|
||||||
received = [0]
|
|
||||||
def pass_event(sender, event):
|
|
||||||
if isinstance(event, OrganizeFile):
|
|
||||||
received[0] += 1
|
|
||||||
self.assertTrue( abspath(event.path) in self.sample_files )
|
|
||||||
dispatcher.connect(pass_event, signal=self.sig, sender=dispatcher.Any,
|
|
||||||
weak=True)
|
|
||||||
org.flush_events( self.organize_path )
|
|
||||||
self.assertEqual( received[0], len(self.sample_files) )
|
|
||||||
self.delete_sample_files()
|
|
||||||
|
|
||||||
def test_process(self):
|
|
||||||
org = self.create_org()
|
|
||||||
received = [0]
|
|
||||||
def pass_event(sender, event):
|
|
||||||
if isinstance(event, OrganizeFile):
|
|
||||||
self.assertTrue( event.path in self.sample_files )
|
|
||||||
received[0] += 1
|
|
||||||
dispatcher.connect(pass_event, signal=self.sig, sender=dispatcher.Any,
|
|
||||||
weak=True)
|
|
||||||
wm = pyinotify.WatchManager()
|
|
||||||
def stopper(notifier):
|
|
||||||
return received[0] == len(self.sample_files)
|
|
||||||
tn = pyinotify.ThreadedNotifier(wm, default_proc_fun=org)
|
|
||||||
tn.daemon = True
|
|
||||||
tn.start()
|
|
||||||
wm.add_watch(self.organize_path, pyinotify.ALL_EVENTS, rec=True,
|
|
||||||
auto_add=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
self.create_sample_files()
|
|
||||||
time.sleep(1)
|
|
||||||
self.assertEqual( len(self.sample_files), received[0] )
|
|
||||||
self.delete_sample_files()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
shutil.rmtree(self.organize_path)
|
|
||||||
|
|
||||||
def create_sample_files(self):
|
|
||||||
for f in self.sample_files: create_file(f)
|
|
||||||
|
|
||||||
def delete_sample_files(self):
|
|
||||||
for f in self.sample_files: os.remove(f)
|
|
||||||
|
|
||||||
def create_org(self):
|
|
||||||
return OrganizeListener( signal=self.sig )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import unittest
|
|
||||||
from media.monitor.manager import Manager
|
|
||||||
|
|
||||||
def add_paths(m,paths):
|
|
||||||
for path in paths:
|
|
||||||
m.add_watch_directory(path)
|
|
||||||
|
|
||||||
class TestManager(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.opath = "/home/rudi/Airtime/python_apps/media-monitor2/tests/"
|
|
||||||
self.ppath = "/home/rudi/Airtime/python_apps/media-monitor2/media/"
|
|
||||||
self.paths = [self.opath, self.ppath]
|
|
||||||
|
|
||||||
def test_init(self):
|
|
||||||
man = Manager()
|
|
||||||
self.assertTrue( len(man.watched_directories) == 0 )
|
|
||||||
self.assertTrue( man.watch_channel is not None )
|
|
||||||
self.assertTrue( man.organize_channel is not None )
|
|
||||||
|
|
||||||
def test_organize_path(self):
|
|
||||||
man = Manager()
|
|
||||||
man.set_organize_path( self.opath )
|
|
||||||
self.assertEqual( man.get_organize_path(), self.opath )
|
|
||||||
man.set_organize_path( self.ppath )
|
|
||||||
self.assertEqual( man.get_organize_path(), self.ppath )
|
|
||||||
|
|
||||||
def test_add_watch_directory(self):
|
|
||||||
man = Manager()
|
|
||||||
add_paths(man, self.paths)
|
|
||||||
for path in self.paths:
|
|
||||||
self.assertTrue( man.has_watch(path) )
|
|
||||||
|
|
||||||
def test_remove_watch_directory(self):
|
|
||||||
man = Manager()
|
|
||||||
add_paths(man, self.paths)
|
|
||||||
for path in self.paths:
|
|
||||||
self.assertTrue( man.has_watch(path) )
|
|
||||||
man.remove_watch_directory( path )
|
|
||||||
self.assertTrue( not man.has_watch(path) )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,44 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
import sys
|
|
||||||
import media.monitor.metadata as mmm
|
|
||||||
|
|
||||||
class TestMetadata(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.music_folder = u'/home/rudi/music'
|
|
||||||
|
|
||||||
def test_got_music_folder(self):
|
|
||||||
t = os.path.exists(self.music_folder)
|
|
||||||
self.assertTrue(t)
|
|
||||||
if not t:
|
|
||||||
print("'%s' must exist for this test to run." % self.music_folder )
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def test_metadata(self):
|
|
||||||
full_paths = (os.path.join(self.music_folder,filename) for filename in os.listdir(self.music_folder))
|
|
||||||
i = 0
|
|
||||||
for full_path in full_paths:
|
|
||||||
if os.path.isfile(full_path):
|
|
||||||
md_full = mmm.Metadata(full_path)
|
|
||||||
md = md_full.extract()
|
|
||||||
if i < 3:
|
|
||||||
i += 1
|
|
||||||
print("Sample metadata: '%s'" % md)
|
|
||||||
self.assertTrue( len( md.keys() ) > 0 )
|
|
||||||
utf8 = md_full.utf8()
|
|
||||||
for k,v in md.iteritems():
|
|
||||||
if hasattr(utf8[k], 'decode'):
|
|
||||||
self.assertEqual( utf8[k].decode('utf-8'), md[k] )
|
|
||||||
else: print("Skipping '%s' because it's a directory" % full_path)
|
|
||||||
|
|
||||||
def test_airtime_mutagen_dict(self):
|
|
||||||
for muta,airtime in mmm.mutagen2airtime.iteritems():
|
|
||||||
self.assertEqual( mmm.airtime2mutagen[airtime], muta )
|
|
||||||
|
|
||||||
def test_format_length(self):
|
|
||||||
# TODO : add some real tests for this function
|
|
||||||
x1 = 123456
|
|
||||||
print("Formatting '%s' to '%s'" % (x1, mmm.format_length(x1)))
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,44 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import media.metadata.process as md
|
|
||||||
|
|
||||||
class TestMetadataDef(unittest.TestCase):
|
|
||||||
def test_simple(self):
|
|
||||||
|
|
||||||
with md.metadata('MDATA_TESTING') as t:
|
|
||||||
t.optional(True)
|
|
||||||
t.depends('ONE','TWO')
|
|
||||||
t.default('unknown')
|
|
||||||
t.translate(lambda kw: kw['ONE'] + kw['TWO'])
|
|
||||||
|
|
||||||
h = { 'ONE' : "testing", 'TWO' : "123" }
|
|
||||||
result = md.global_reader.read('test_path',h)
|
|
||||||
self.assertTrue( 'MDATA_TESTING' in result )
|
|
||||||
self.assertEqual( result['MDATA_TESTING'], 'testing123' )
|
|
||||||
h1 = { 'ONE' : 'big testing', 'two' : 'nothing' }
|
|
||||||
result1 = md.global_reader.read('bs path', h1)
|
|
||||||
self.assertEqual( result1['MDATA_TESTING'], 'unknown' )
|
|
||||||
|
|
||||||
def test_topo(self):
|
|
||||||
with md.metadata('MDATA_TESTING') as t:
|
|
||||||
t.depends('shen','sheni')
|
|
||||||
t.default('megitzda')
|
|
||||||
t.translate(lambda kw: kw['shen'] + kw['sheni'])
|
|
||||||
|
|
||||||
with md.metadata('shen') as t:
|
|
||||||
t.default('vaxo')
|
|
||||||
|
|
||||||
with md.metadata('sheni') as t:
|
|
||||||
t.default('gio')
|
|
||||||
|
|
||||||
with md.metadata('vaxo') as t:
|
|
||||||
t.depends('shevetsi')
|
|
||||||
|
|
||||||
v = md.global_reader.read('bs mang', {})
|
|
||||||
self.assertEqual(v['MDATA_TESTING'], 'vaxogio')
|
|
||||||
self.assertTrue( 'vaxo' not in v )
|
|
||||||
|
|
||||||
md.global_reader.clear()
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,64 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import json
|
|
||||||
|
|
||||||
from media.monitor.airtime import AirtimeNotifier, AirtimeMessageReceiver
|
|
||||||
from mock import patch, Mock
|
|
||||||
from media.monitor.config import MMConfig
|
|
||||||
|
|
||||||
from media.monitor.manager import Manager
|
|
||||||
|
|
||||||
def filter_ev(d): return { i : j for i,j in d.iteritems() if i != 'event_type' }
|
|
||||||
|
|
||||||
class TestReceiver(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# TODO : properly mock this later
|
|
||||||
cfg = {}
|
|
||||||
self.amr = AirtimeMessageReceiver(cfg, Manager())
|
|
||||||
|
|
||||||
def test_supported(self):
|
|
||||||
# Every supported message should fire something
|
|
||||||
for event_type in self.amr.dispatch_table.keys():
|
|
||||||
msg = { 'event_type' : event_type, 'extra_param' : 123 }
|
|
||||||
filtered = filter_ev(msg)
|
|
||||||
# There should be a better way to test the following without
|
|
||||||
# patching private methods
|
|
||||||
with patch.object(self.amr, '_execute_message') as mock_method:
|
|
||||||
mock_method.side_effect = None
|
|
||||||
ret = self.amr.message(msg)
|
|
||||||
self.assertTrue(ret)
|
|
||||||
mock_method.assert_called_with(event_type, filtered)
|
|
||||||
|
|
||||||
def test_no_mod_message(self):
|
|
||||||
ev = { 'event_type' : 'new_watch', 'directory' : 'something here' }
|
|
||||||
filtered = filter_ev(ev)
|
|
||||||
with patch.object(self.amr, '_execute_message') as mock_method:
|
|
||||||
mock_method.return_value = "tested"
|
|
||||||
ret = self.amr.message(ev)
|
|
||||||
self.assertTrue( ret ) # message passing worked
|
|
||||||
mock_method.assert_called_with(ev['event_type'], filtered)
|
|
||||||
# test that our copy of the message does not get modified
|
|
||||||
self.assertTrue( 'event_type' in ev )
|
|
||||||
|
|
||||||
class TestAirtimeNotifier(unittest.TestCase):
|
|
||||||
def test_handle_message(self):
|
|
||||||
#from configobj import ConfigObj
|
|
||||||
test_cfg = MMConfig('./test_config.cfg')
|
|
||||||
ran = [False]
|
|
||||||
class MockReceiver(object):
|
|
||||||
def message(me,m):
|
|
||||||
self.assertTrue( 'event_type' in m )
|
|
||||||
self.assertEqual( m['path'], '/bs/path' )
|
|
||||||
ran[0] = True
|
|
||||||
airtime = AirtimeNotifier(cfg=test_cfg, message_receiver=MockReceiver())
|
|
||||||
m1 = Mock()
|
|
||||||
m1.ack = "ack'd message"
|
|
||||||
m2 = Mock()
|
|
||||||
m2.body = json.dumps({ 'event_type' : 'file_delete', 'path' : '/bs/path' })
|
|
||||||
airtime.handle_message(body=m1,message=m2)
|
|
||||||
self.assertTrue( ran[0] )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,36 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
from media.monitor import owners
|
|
||||||
|
|
||||||
class TestMMP(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.f = "test.mp3"
|
|
||||||
|
|
||||||
def test_has_owner(self):
|
|
||||||
owners.reset_owners()
|
|
||||||
o = 12345
|
|
||||||
self.assertTrue( owners.add_file_owner(self.f,o) )
|
|
||||||
self.assertTrue( owners.has_owner(self.f) )
|
|
||||||
|
|
||||||
def test_add_file_owner(self):
|
|
||||||
owners.reset_owners()
|
|
||||||
self.assertFalse( owners.add_file_owner('testing', -1) )
|
|
||||||
self.assertTrue( owners.add_file_owner(self.f, 123) )
|
|
||||||
self.assertTrue( owners.add_file_owner(self.f, 123) )
|
|
||||||
self.assertTrue( owners.add_file_owner(self.f, 456) )
|
|
||||||
|
|
||||||
def test_remove_file_owner(self):
|
|
||||||
owners.reset_owners()
|
|
||||||
self.assertTrue( owners.add_file_owner(self.f, 123) )
|
|
||||||
self.assertTrue( owners.remove_file_owner(self.f) )
|
|
||||||
self.assertFalse( owners.remove_file_owner(self.f) )
|
|
||||||
|
|
||||||
def test_get_owner(self):
|
|
||||||
owners.reset_owners()
|
|
||||||
self.assertTrue( owners.add_file_owner(self.f, 123) )
|
|
||||||
self.assertEqual( owners.get_owner(self.f), 123, "file is owned" )
|
|
||||||
self.assertEqual( owners.get_owner("random_stuff.txt"), -1,
|
|
||||||
"file is not owned" )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import media.monitor.pure as mmp
|
|
||||||
|
|
||||||
class TestMMP(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.md1 = {'MDATA_KEY_MD5': '71185323c2ab0179460546a9d0690107',
|
|
||||||
'MDATA_KEY_FTYPE': 'audioclip',
|
|
||||||
'MDATA_KEY_MIME': 'audio/vorbis',
|
|
||||||
'MDATA_KEY_DURATION': '0:0:25.000687',
|
|
||||||
'MDATA_KEY_SAMPLERATE': 48000,
|
|
||||||
'MDATA_KEY_BITRATE': 64000,
|
|
||||||
'MDATA_KEY_REPLAYGAIN': 0,
|
|
||||||
'MDATA_KEY_TITLE': u'ACDC_-_Back_In_Black-sample-64kbps'}
|
|
||||||
|
|
||||||
def test_apply_rules(self):
|
|
||||||
sample_dict = {
|
|
||||||
'key' : 'val',
|
|
||||||
'test' : 'IT',
|
|
||||||
}
|
|
||||||
rules = {
|
|
||||||
'key' : lambda x : x.upper(),
|
|
||||||
'test' : lambda y : y.lower()
|
|
||||||
}
|
|
||||||
sample_dict = mmp.apply_rules_dict(sample_dict, rules)
|
|
||||||
self.assertEqual(sample_dict['key'], 'VAL')
|
|
||||||
self.assertEqual(sample_dict['test'], 'it')
|
|
||||||
|
|
||||||
def test_default_to(self):
|
|
||||||
sd = { }
|
|
||||||
def_keys = ['one','two','three']
|
|
||||||
sd = mmp.default_to(dictionary=sd, keys=def_keys, default='DEF')
|
|
||||||
for k in def_keys: self.assertEqual( sd[k], 'DEF' )
|
|
||||||
|
|
||||||
def test_file_md5(self):
|
|
||||||
p = os.path.realpath(__file__)
|
|
||||||
m1 = mmp.file_md5(p)
|
|
||||||
m2 = mmp.file_md5(p,10)
|
|
||||||
self.assertTrue( m1 != m2 )
|
|
||||||
self.assertRaises( ValueError, lambda : mmp.file_md5('/file/path') )
|
|
||||||
self.assertTrue( m1 == mmp.file_md5(p) )
|
|
||||||
|
|
||||||
def test_sub_path(self):
|
|
||||||
f1 = "/home/testing/123.mp3"
|
|
||||||
d1 = "/home/testing"
|
|
||||||
d2 = "/home/testing/"
|
|
||||||
self.assertTrue( mmp.sub_path(d1, f1) )
|
|
||||||
self.assertTrue( mmp.sub_path(d2, f1) )
|
|
||||||
|
|
||||||
def test_parse_int(self):
|
|
||||||
self.assertEqual( mmp.parse_int("123"), "123" )
|
|
||||||
self.assertEqual( mmp.parse_int("123asf"), "123" )
|
|
||||||
self.assertEqual( mmp.parse_int("asdf"), None )
|
|
||||||
|
|
||||||
def test_truncate_to_length(self):
|
|
||||||
s1 = "testing with non string literal"
|
|
||||||
s2 = u"testing with unicode literal"
|
|
||||||
self.assertEqual( len(mmp.truncate_to_length(s1, 5)), 5)
|
|
||||||
self.assertEqual( len(mmp.truncate_to_length(s2, 8)), 8)
|
|
||||||
|
|
||||||
|
|
||||||
def test_owner_id(self):
|
|
||||||
start_path = "testing.mp3"
|
|
||||||
id_path = "testing.mp3.identifier"
|
|
||||||
o_id = 123
|
|
||||||
f = open(id_path, 'w')
|
|
||||||
f.write("123")
|
|
||||||
f.close()
|
|
||||||
possible_id = mmp.owner_id(start_path)
|
|
||||||
self.assertFalse( os.path.exists(id_path) )
|
|
||||||
self.assertEqual( possible_id, o_id )
|
|
||||||
self.assertEqual( -1, mmp.owner_id("something.random") )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,48 +0,0 @@
|
||||||
import unittest
|
|
||||||
from mock import MagicMock
|
|
||||||
|
|
||||||
from media.monitor.request import RequestSync
|
|
||||||
|
|
||||||
class TestRequestSync(unittest.TestCase):
|
|
||||||
|
|
||||||
def apc_mock(self):
|
|
||||||
fake_apc = MagicMock()
|
|
||||||
fake_apc.send_media_monitor_requests = MagicMock()
|
|
||||||
return fake_apc
|
|
||||||
|
|
||||||
def watcher_mock(self):
|
|
||||||
fake_watcher = MagicMock()
|
|
||||||
fake_watcher.flag_done = MagicMock()
|
|
||||||
return fake_watcher
|
|
||||||
|
|
||||||
def request_mock(self):
|
|
||||||
fake_request = MagicMock()
|
|
||||||
fake_request.safe_pack = MagicMock(return_value=[])
|
|
||||||
return fake_request
|
|
||||||
|
|
||||||
def test_send_media_monitor(self):
|
|
||||||
fake_apc = self.apc_mock()
|
|
||||||
fake_requests = [ self.request_mock() for x in range(1,5) ]
|
|
||||||
fake_watcher = self.watcher_mock()
|
|
||||||
rs = RequestSync(fake_watcher, fake_requests, fake_apc)
|
|
||||||
rs.run_request()
|
|
||||||
self.assertEquals(fake_apc.send_media_monitor_requests.call_count, 1)
|
|
||||||
|
|
||||||
def test_flag_done(self):
|
|
||||||
fake_apc = self.apc_mock()
|
|
||||||
fake_requests = [ self.request_mock() for x in range(1,5) ]
|
|
||||||
fake_watcher = self.watcher_mock()
|
|
||||||
rs = RequestSync(fake_watcher, fake_requests, fake_apc)
|
|
||||||
rs.run_request()
|
|
||||||
self.assertEquals(fake_watcher.flag_done.call_count, 1)
|
|
||||||
|
|
||||||
def test_safe_pack(self):
|
|
||||||
fake_apc = self.apc_mock()
|
|
||||||
fake_requests = [ self.request_mock() for x in range(1,5) ]
|
|
||||||
fake_watcher = self.watcher_mock()
|
|
||||||
rs = RequestSync(fake_watcher, fake_requests, fake_apc)
|
|
||||||
rs.run_request()
|
|
||||||
for req in fake_requests:
|
|
||||||
self.assertEquals(req.safe_pack.call_count, 1)
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,35 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
from media.monitor.syncdb import AirtimeDB
|
|
||||||
from media.monitor.log import get_logger
|
|
||||||
from media.monitor.pure import partition
|
|
||||||
import api_clients.api_client as ac
|
|
||||||
import prepare_tests
|
|
||||||
|
|
||||||
class TestAirtimeDB(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.ac = ac.AirtimeApiClient(logger=get_logger(),
|
|
||||||
config_path=prepare_tests.real_config)
|
|
||||||
|
|
||||||
def test_syncdb_init(self):
|
|
||||||
sdb = AirtimeDB(self.ac)
|
|
||||||
self.assertTrue( len(sdb.list_storable_paths()) > 0 )
|
|
||||||
|
|
||||||
def test_list(self):
|
|
||||||
self.sdb = AirtimeDB(self.ac)
|
|
||||||
for watch_dir in self.sdb.list_storable_paths():
|
|
||||||
self.assertTrue( os.path.exists(watch_dir) )
|
|
||||||
|
|
||||||
def test_directory_get_files(self):
|
|
||||||
sdb = AirtimeDB(self.ac)
|
|
||||||
print(sdb.list_storable_paths())
|
|
||||||
for wdir in sdb.list_storable_paths():
|
|
||||||
files = sdb.directory_get_files(wdir)
|
|
||||||
print( "total files: %d" % len(files) )
|
|
||||||
self.assertTrue( len(files) >= 0 )
|
|
||||||
self.assertTrue( isinstance(files, set) )
|
|
||||||
exist, deleted = partition(os.path.exists, files)
|
|
||||||
print("(exist, deleted) = (%d, %d)" % ( len(exist), len(deleted) ) )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,64 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import time
|
|
||||||
from media.saas.thread import InstanceThread, InstanceInheritingThread
|
|
||||||
|
|
||||||
# ugly but necessary for 2.7
|
|
||||||
signal = False
|
|
||||||
signal2 = False
|
|
||||||
|
|
||||||
class TestInstanceThread(unittest.TestCase):
|
|
||||||
def test_user_inject(self):
|
|
||||||
global signal
|
|
||||||
signal = False
|
|
||||||
u = "rudi"
|
|
||||||
class T(InstanceThread):
|
|
||||||
def run(me):
|
|
||||||
global signal
|
|
||||||
super(T, me).run()
|
|
||||||
signal = True
|
|
||||||
self.assertEquals(u, me.user())
|
|
||||||
t = T(u, name="test_user_inject")
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
time.sleep(0.2)
|
|
||||||
self.assertTrue(signal)
|
|
||||||
|
|
||||||
def test_inheriting_thread(utest):
|
|
||||||
global signal2
|
|
||||||
u = "testing..."
|
|
||||||
|
|
||||||
class TT(InstanceInheritingThread):
|
|
||||||
def run(self):
|
|
||||||
global signal2
|
|
||||||
utest.assertEquals(self.user(), u)
|
|
||||||
signal2 = True
|
|
||||||
|
|
||||||
class T(InstanceThread):
|
|
||||||
def run(self):
|
|
||||||
super(T, self).run()
|
|
||||||
child_thread = TT(name="child thread")
|
|
||||||
child_thread.daemon = True
|
|
||||||
child_thread.start()
|
|
||||||
|
|
||||||
parent_thread = T(u, name="Parent instance thread")
|
|
||||||
parent_thread.daemon = True
|
|
||||||
parent_thread.start()
|
|
||||||
|
|
||||||
time.sleep(0.2)
|
|
||||||
utest.assertTrue(signal2)
|
|
||||||
|
|
||||||
def test_different_user(utest):
|
|
||||||
u1, u2 = "ru", "di"
|
|
||||||
class T(InstanceThread):
|
|
||||||
def run(self):
|
|
||||||
super(T, self).run()
|
|
||||||
|
|
||||||
for u in [u1, u2]:
|
|
||||||
t = T(u)
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
utest.assertEquals(t.user(), u)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
|
@ -1,54 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
import time
|
|
||||||
import media.monitor.pure as mmp
|
|
||||||
from media.monitor.toucher import Toucher, ToucherThread
|
|
||||||
|
|
||||||
class BaseTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.p = "api_client.cfg"
|
|
||||||
|
|
||||||
class TestToucher(BaseTest):
|
|
||||||
def test_toucher(self):
|
|
||||||
t1 = mmp.last_modified(self.p)
|
|
||||||
t = Toucher(self.p)
|
|
||||||
t()
|
|
||||||
t2 = mmp.last_modified(self.p)
|
|
||||||
print("(t1,t2) = (%d, %d) diff => %d" % (t1, t2, t2 - t1))
|
|
||||||
self.assertTrue( t2 > t1 )
|
|
||||||
|
|
||||||
class TestToucherThread(BaseTest):
|
|
||||||
def test_thread(self):
|
|
||||||
t1 = mmp.last_modified(self.p)
|
|
||||||
ToucherThread(self.p, interval=1)
|
|
||||||
time.sleep(2)
|
|
||||||
t2 = mmp.last_modified(self.p)
|
|
||||||
print("(t1,t2) = (%d, %d) diff => %d" % (t1, t2, t2 - t1))
|
|
||||||
self.assertTrue( t2 > t1 )
|
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
from setuptools import setup
|
|
||||||
from subprocess import call
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
script_path = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
print script_path
|
|
||||||
os.chdir(script_path)
|
|
||||||
|
|
||||||
# Allows us to avoid installing the upstart init script when deploying on Airtime Pro:
|
|
||||||
if '--no-init-script' in sys.argv:
|
|
||||||
data_files = []
|
|
||||||
sys.argv.remove('--no-init-script') # super hax
|
|
||||||
else:
|
|
||||||
media_monitor_files = []
|
|
||||||
mm2_files = []
|
|
||||||
for root, dirnames, filenames in os.walk('media-monitor'):
|
|
||||||
for filename in filenames:
|
|
||||||
media_monitor_files.append(os.path.join(root, filename))
|
|
||||||
for root, dirnames, filenames in os.walk('media-monitor2'):
|
|
||||||
for filename in filenames:
|
|
||||||
mm2_files.append(os.path.join(root, filename))
|
|
||||||
|
|
||||||
data_files = [
|
|
||||||
('/etc/init', ['install/upstart/airtime-media-monitor.conf.template']),
|
|
||||||
('/etc/init.d', ['install/sysvinit/airtime-media-monitor']),
|
|
||||||
('/etc/airtime', ['install/media_monitor_logging.cfg']),
|
|
||||||
('/var/log/airtime/media-monitor', []),
|
|
||||||
('/var/tmp/airtime/media-monitor', []),
|
|
||||||
]
|
|
||||||
print data_files
|
|
||||||
|
|
||||||
setup(name='airtime-media-monitor',
|
|
||||||
version='1.0',
|
|
||||||
description='Airtime Media Monitor',
|
|
||||||
url='http://github.com/sourcefabric/Airtime',
|
|
||||||
author='sourcefabric',
|
|
||||||
license='AGPLv3',
|
|
||||||
packages=['media_monitor', 'mm2', 'mm2.configs',
|
|
||||||
'mm2.media', 'mm2.media.monitor',
|
|
||||||
'mm2.media.metadata', 'mm2.media.saas'
|
|
||||||
],
|
|
||||||
package_data={'': ['*.cfg']},
|
|
||||||
scripts=['bin/airtime-media-monitor'],
|
|
||||||
install_requires=[
|
|
||||||
'amqplib',
|
|
||||||
'anyjson',
|
|
||||||
'argparse',
|
|
||||||
'configobj',
|
|
||||||
'docopt',
|
|
||||||
'kombu',
|
|
||||||
'mutagen',
|
|
||||||
'poster',
|
|
||||||
'PyDispatcher',
|
|
||||||
'pyinotify',
|
|
||||||
'pytz',
|
|
||||||
'wsgiref'
|
|
||||||
],
|
|
||||||
zip_safe=False,
|
|
||||||
data_files=data_files)
|
|
||||||
|
|
||||||
# Reload the initctl config so that the media-monitor service works
|
|
||||||
if data_files:
|
|
||||||
print "Reloading initctl configuration"
|
|
||||||
#call(['initctl', 'reload-configuration'])
|
|
||||||
print "Run \"sudo service airtime-media-monitor start\""
|
|
|
@ -66,8 +66,6 @@ for i in ${FILES[*]}; do
|
||||||
echo $i
|
echo $i
|
||||||
done
|
done
|
||||||
echo "pip airtime-playout"
|
echo "pip airtime-playout"
|
||||||
# We're no longer using media-monitor
|
|
||||||
# echo "pip airtime-media-monitor"
|
|
||||||
|
|
||||||
echo -e "\nIf your web root is not listed, you will need to manually remove it."
|
echo -e "\nIf your web root is not listed, you will need to manually remove it."
|
||||||
|
|
||||||
|
@ -105,6 +103,6 @@ if [[ "$IN" = "y" || "$IN" = "Y" ]]; then
|
||||||
dropAirtimeDatabase
|
dropAirtimeDatabase
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pip uninstall -y airtime-playout airtime-media-monitor
|
pip uninstall -y airtime-playout airtime-media-monitor airtime-analyzer
|
||||||
service apache2 restart
|
service apache2 restart
|
||||||
echo "...Done"
|
echo "...Done"
|
||||||
|
|
Loading…
Reference in New Issue