#!/usr/bin/python # This program lets you create a Bibble output queue which outputs to a draft # Blogger blog post. It does so using Blogger's e-mail posting feature. # # The basic idea: We read lines printed to bibbledebug.txt until we see one # that indicates a complete batch. We then e-mail all images in the output # directory to Blogger (with some checks to make sure there are no stale # images laying around). # # # COPYRIGHT AND LICENSE # # Copyright (c) 2010 Reid Priedhorsky, reid@reidster.net # All rights reserved. # # Distributed under the 3-clause BSD license. Details at end of file. # # If you find this software useful, a note to the above e-mail address letting # me know would be very much appreciated. Feedback and patches are also # extremely welcome. # # While it is legally permissible to include this program in commercial # software without acknowledgement and/or payment, I consider it unethical to # do so. # # # INSTALLATION AND SETUP # # I've only tested this on Debian Linux but I'm sure it works on other # UNIX-like systems as well. Let me know if you have a success story. # # 1. Install mutt and configure it so it can send e-mail. # 2. Save this script somewhere in your $PATH and make it executable # 3. Configure Blogger: # a. Go to your blog's control panel page. # b. Open the Settings -> Email & Mobile tab # c. Under Email Posting Address, choose "Save emails as draft posts" and # make note of the posting address. # 4. Configure Bibble: # a. Create a new batch output queue. # b. Under Destination, choose Fixed and enter "/tmp/blogger" (or wherever) # c. Under Post Processing, check Open in Application # d. Click "Select..." # e. For Application, use "/bin/sh" # f. For Application Arguments, enter: # -c "bibbleblog /tmp/blogger /path/to/bibbledebug.txt postingaddress@blogger.com %1" # Substitute appropriate values. Note that the "%1" is a literal. # g. Configure the other output parameters as you like. # # # HOW TO USE # # 1. Send image versions to the appropriate output queue. # 2. Wait a little bit. # 3. Go to Blogger's Edit Posts view. # 4. A draft post titled "bibbleblog draft" should be present. # # If there are any problems, check the log file (/tmp/bibbleblog.log) for # clues. There is no output on stdout or stderr unless something goes wrong at # a fairly low level (we capture exceptions and put them into the log file). # If there's no output in the log file, try running the first instance of the # program manually from the console and then initiating the batch job. # # # BUGS: # # - The output batch job for this program must be the only one running in # Bibble, and it must be only running once. No other output jobs can be # started until this program is 100% complete, nor can this job be started # if another is running or it will get confused. There is no check for this # condition (though we do check if we found the right number of images). # # - Only JPEG output (*.jpg) is currently supported. # # - Untested when bibbledebug.txt uses a multibyte character encoding. # # - Will enter an infinite loop if bibbledebug.txt is empty when the program # is started. # # - Blogger has limits on the number of images you can upload at once (5?), # but this program does not know about or enforce these. import glob import logging as log import os import os.path import re import signal import sys import time import traceback ## constants USAGE = "Usage: bibbleblog QUEUE_DIR BIBBLEDEBUG_PATH TARGET_EMAIL IMAGENAME" LOG_FILENAME = "/tmp/bibbleblog.log" LOCK_NAME = "bibbleblog.lock" IMAGE_SUFFIX = "jpg" RE_BATCH_DONE = re.compile(r"Batch.*complete: (\d+) items") ## globals queue_dir = None bibbledebug = None target_email = None lock_fullpath = None sigterm_received = None ## code def main(): try: # logging setup log.basicConfig(filename="/tmp/bibbleblog.log", level=log.DEBUG, format="%(asctime)s %(process)s %(levelname)s %(message)s") log.info("starting") command_line_parse() if not (lock_acquire()): log.info("another instance appears to be running, exiting") sys.exit(0) signal.signal(signal.SIGTERM, handler_sigterm) log.info("waiting for completion notice") fp = open(bibbledebug) fp.seek(-1, os.SEEK_END) # Back up a couple of lines as otherwise we can miss the batch-complete # notice if there's only one image in the batch. seek_lines(fp, -1) done = False while (not done): time.sleep(1) for line in (fp.readlines()): log.debug("bibbledebug: %s" % line[:-1]) # batch complete line? m = RE_BATCH_DONE.search(line) if (m is not None): image_ct = int(m.group(1)) done = True if (sigterm_received): log.info("SIGTERM received, shutting down") image_ct = 0 done = True fp.close() if (image_ct > 0): log.info("preparing to upload %d images" % (image_ct)) images = sorted(glob.glob("%s/*.%s" % (queue_dir, IMAGE_SUFFIX))) if (len(images) != image_ct): fatal("expecting %d images but found %d" % (image_ct, len(images))) mutt_cmd = "mutt -s 'bibbleblog draft' -a %s -- %s < /dev/null" % (" ".join(images), target_email) do_cmd(mutt_cmd) log.info("e-mailed: %s" % (mutt_cmd)) for image in images: os.unlink(image) log.info("deleted images") lock_release() log.info("done") except Exception, x: log.error(traceback.format_exc()) fatal("exception caught, aborting") def command_line_parse(): if (len(sys.argv) != 5): fatal(USAGE) global queue_dir queue_dir = sys.argv[1] if (not os.path.isdir(queue_dir)): fatal("%s does not appear to be a directory" % queue_dir) global lock_fullpath lock_fullpath = "%s/%s" % (queue_dir, LOCK_NAME) global bibbledebug bibbledebug = sys.argv[2] if (not os.path.isfile(bibbledebug)): fatal("%s does not appear to be a file" % bibbledebug) global target_email target_email = sys.argv[3] log.debug("queue directory: %s" % queue_dir) log.debug("bibbledebug.txt: %s" % bibbledebug) log.debug("target e-mail: %s" % target_email) log.debug("output filename (ignored): %s" % sys.argv[4]) def do_cmd(cmd): if os.system(cmd): fatal("command failed: %s" % cmd) def fatal(message): log.error(message) log.error("aborting") sys.exit(1) def handler_sigterm(signum, frame): global sigterm_received sigterm_received = True def lock_acquire(): try: os.mkdir(lock_fullpath) log.info("acquired lock") return True except OSError, x: log.info("failed to acquire lock (%s)" % x.strerror) return False def lock_release(): os.rmdir(lock_fullpath) log.info("released lock") def seek_lines(fp, ct): # FIXME: this is very brittle. assert(ct < 0) ct = -ct + 1 seen_ct = 0 while True: if (fp.read(1) == '\n'): seen_ct += 1 if (ct == seen_ct): break fp.seek(-2, os.SEEK_CUR) #log.debug('seeked to %d' % (fp.tell())) if (__name__ == "__main__"): main() # LICENSE # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * The names of contributors may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL ANY COPYRIGHT HOLDERS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.