a violet pig being the logo of this site

upload_imageΒΆ

Download this file

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2008 Sebastian Wiesner <lunaryorn@googlemail.com>

# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# http://sam.zoy.org/wtfpl/COPYING for more details.


"""
    upload_image
    ============

    Cmdline script, which uploads to different image hosters.

    :author: Sebastian Wiesner
    :contact: lunaryorn@googlemail.com
    :copyright: 2008 by Sebastian Wiesner
    :license: WTFPL
"""

from __future__ import print_function

import os
import sys
import imghdr
import urllib2
import webbrowser
import subprocess
from cStringIO import StringIO

import argparse
import ClientForm
import lxml.html
from lxml.cssselect import CSSSelector


def detect_mimetype(img):
    """Detects mime type of `img`.  Returns None, if mime type couldn't be
    determined.

    :type img: str or file-like object supporting ``read``, ``tell`` and
               ``seek``
    """
    format = imghdr.what(img)
    return ('image/' + format if format is not None else None)


class Service(object):
    """Abstract class for services.  Each deriving class must re-define the
    class variables.

    :CVariables:
    - `url`: Website of the service
    - `name`:  A descriptive name of the upload service
    """

    def _fill_missing_arguments(self, image, mime_type, filename):
        """Fills missing arguments to upload_file.  If `mimetype` is
        ``None``, the mime type of `image` is guessed with
        `detect_mimetype`.

        If `filename` is ``None``, this method attempts to get the filename
        from the `name` attribute of the stream denoted by `image`.  If this
        fails, it sets the name to \"foo\".

        All three arguments are returned properly initialized.  Note, that
        the caller is responsible for proper closing of the returned
        file-like object.

        :returns: ``(image, mime_type, filename)``
        :returntype: ``(file-like object, str, str)``
        """
        if mime_type is None:
            mime_type = detect_mimetype(image)
        if mime_type is None:
            raise IOError('Couldn\'t detect mimetype of {0}'.format(image))

        stream = (open(image, 'rb') if isinstance(image, basestring)
                  else image)

        if filename is None:
            filename = getattr(stream, 'name', None)
        if filename is None:
            raise ValueError('No filename given')

        return stream, mime_type, os.path.basename(filename)

    def upload_image(self, image, mime_type=None, filename=None):
        """Uploads `image` with `mime_type` as `filename`.

        Returns ``(image_link, bbcode, links)``, where ``image_link`` is
        direct link to the image, ``bbcode`` a bbcode line useful for
        webforums and ``links`` the complete list of all links."""
        raise NotImplementedError()

    def __repr__(self):
        if hasattr(self, 'name'):
            return '<Service "{0.name}" at {1}>'.format(self, id(self))
        else:
            return object.__repr__(self)


class WebFormService(Service):
    """Service, which parses web forms, fills them and returns a list of
    forms extracted from the web site.

    Subclasses of this class should have an attribute ``webform_url``,
    denoting the webform to parse, and must reimplement ``_get_links``.
    """

    def upload_image(self, image, mime_type=None, filename=None):
        """Uploads `image` with `mime_type` as `filename`. Missing
        values are autodetected.
        """
        html = self._get_webform_html()
        forms = ClientForm.ParseResponse(html, backwards_compat=False)
        upload_form = self._find_upload_webform(forms)
        stream, mime_type, filename = self._fill_missing_arguments(
            image, mime_type, filename)
        try:
            upload_form.add_file(stream, mime_type, filename)
            self._fill_additional_form_fields(upload_form)
            request = upload_form.click()
            response = urllib2.urlopen(request)
            return self._get_links(response)
        finally:
            stream.close()

    def _fill_additional_form_fields(self, form):
        """Called to fill additional fields in `form`, if necessary.  The
        default implementation does nothing.

        The return value of this method is ignored.
        """
        pass


    def _get_links(self, response):
        """Extracts all links from `response`, which is a urllib2 response
        object as returned by ``urllib2.urlopen``.

        It must return ``(image_link, bbcode, links)``, where ``image_link``
        is the direct link to the image, ``bbcode`` is a bbcode line usable
        for webforums and ``links`` is the complete list of links."""
        raise NotImplementedError()

    def _get_webform_html(self):
        """Returns html code of the upload webform.  The default
        implementation uses ``urllib2.urlopen`` with ``self.webform_url`` to
        retrieve the html code.

        Subclasses can reimplement this, to customize loading."""
        return urllib2.urlopen(self.webform_url)

    def _find_upload_webform(self, forms):
        """Finds the form which handles the upload.  The default
        implementation blindly iterates over all forms and returns the one,
        which has a file upload field.

        Raises `ClientForm.ControlNotFoundError`, if no upload webform
        was found."""
        for form in forms:
            try:
                form.find_control(type='file')
                return form
            except ClientForm.ControlNotFoundError, err:
                # throw away forms, that don't have an upload control
                pass
        else:
            raise err


class ImageBananaService(WebFormService):
    """Uploads images to imagabanana.de"""

    url = 'http://www.imagebanana.de/'
    name = 'ImageBanana'
    webform_url = url
    find_url_elements = CSSSelector('input .input_text')

    def _get_links(self, response):
        tree = lxml.html.parse(response)
        urls = [el.get('value') for el in self.find_url_elements(tree)]
        return urls[1], urls[3], urls


class UbuntuPicsService(WebFormService):
    """Uploads images to pics.ubuntu-projekte.de"""

    url = 'http://www.ubuntu-pics.de/'
    name = 'UbuntuPics'
    webform_url = 'http://www.ubuntu-pics.de/easy.html'

    def _get_links(self, response):
        tree = lxml.html.parse(response)
        content = tree.getroot().get_element_by_id('content')
        direct = content.get_element_by_id('direct').get('value')
        bbcode = content.get_element_by_id('bbcode').get('value')
        urls = content.xpath('input/attribute::value')
        return direct, bbcode, urls


def copy_to_clipboard(text):
    """Copy `text` to clipboard."""
    p = subprocess.Popen(["xsel", "-i"], stdin=subprocess.PIPE)
    return p.communicate(text)


def get_all_services():
    objects = globals().itervalues()
    classes = (obj for obj in objects if isinstance(obj, type))
    services = (cls for cls in classes if issubclass(cls, Service))
    return dict(((s.name, s) for s in services if hasattr(s, 'name')))


SERVICES = get_all_services()


class ListServices(argparse.Action):
    def __init__(self, *args, **kwargs):
        if not 'nargs' in kwargs:
            kwargs['nargs'] = 0
        super(ListServices, self).__init__(*args, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        table = []
        widths = [0, 0]
        for service in SERVICES.itervalues():
            row = [service.name, service.url]
            widths = map(max, zip(widths, map(len, row)))
            table.append(row)
        line_tmpl = '{0[0]:<{1[0]}} - {0[1]:<{1[1]}}'
        for row in table:
            print(line_tmpl.format(row, widths))
        parser.exit()


def _parse_args():
    parser = argparse.ArgumentParser(epilog="""
(C) 2008  Sebastian Wiesner, licensed under the terms of WTFPL 2.""",
                          description="""
Uploads images. If you don't specify a file name, the image is read from
standard input. An image read from standard input will be called \"stdin\"
unless you specify an explicit filename using the --filename option.""")
    parser.add_argument('--list-services', action=ListServices,
                        help='List all upload services and exit.')
    parser.add_argument('image', nargs='?', help='Image to upload. '
                        '"-" means standard input, which is the default '
                        'if unspecified.',)
    upload_args = parser.add_argument_group('Upload settings',
                                            'How to upload a file?')
    upload_args.add_argument('-s', '--service', choices=SERVICES,
                             help='Load image up to SERVICE. Defaults to '
                             '"UbuntuPics". The default can be changed '
                             'through the UPLOAD_IMAGE_DEFAULT_SERVICE '
                             'environment variable.')
    upload_args.add_argument('-f', '--filename', metavar='NAME',
                             help='Transmits NAME as filename to the '
                             'server. Defaults to local filename, or '
                             '"stdin", if image data comes from standard '
                             'input.')
    upload_args.add_argument('-m', '--mime-type', metavar='TYPE',
                             help='Force mimetype to be TYPE. If '
                             'unspecified, mime type is auto-detected from '
                             'image format.')
    result_args = parser.add_argument_group('Result handling',
                                            'What to do with the result of '
                                            'the upload?')
    result_args.add_argument('-c', '--copy',
                             choices=['no', 'bbcode', 'image'],
                             help='What to copy into clipboard?')
    result_args.add_argument('-b', '--browser', action='store_true',
                             help='If set, image url is opened in '
                             'webbrowser.')
    misc_args = parser.add_argument_group('Misc settings')
    misc_args.add_argument('-d', '--delete', action='store_true',
                           help='Delete the file after uploading (use with '
                           'care!)', dest='delete')
    def_service = os.environ.get('UPLOAD_IMAGE_DEFAULT_SERVICE',
                                 UbuntuPicsService.name)
    parser.set_defaults(copy='bbcode', service=def_service, image='-')
    return parser.parse_args()


def main():
    try:
        # setup localized environment
        import locale
        locale.setlocale(locale.LC_ALL, '')
        args = _parse_args()
        print('Uploading to {0.service}...'.format(args), end='\n'*2)
        # finalize command line arguments
        if args.filename is None:
            args.filename = (args.image if args.image != '-' else 'stdin')
        if args.image == '-':
            args.image = StringIO(sys.stdin.read())
        try:
            # upload image and print return values
            service = SERVICES[args.service]()
            image, bbcode, urls = service.upload_image(
                args.image, args.mime_type, args.filename)
            urls.sort()
            urls.reverse()
            print('bbcode:', bbcode)
            print('image:', image, end='\n'*2)
            for url in urls:
                print(url)
        except ClientForm.ControlNotFoundError:
            sys.exit('Website at {0.url} doesn\'t support file '
                     'uploads'.format(service))
        if args.copy != 'no':
            try:
                copy_to_clipboard(locals()[args.copy])
            except OSError as err:
                sys.exit(str(err))
        if args.browser:
            webbrowser.open_new_tab(image)
        if args.delete:
            os.remove(image)
    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    main()

Previous topic

pydo.el

Next topic

About ...

This Page