#!/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()