#!/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.