diff --git a/modules/quvi/__init__.py b/modules/quvi/__init__.py
new file mode 100644
index 00000000..f4925f8c
--- /dev/null
+++ b/modules/quvi/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Vincent A
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from .backend import QuviBackend, QuviVideo
+
+
+__all__ = ['QuviBackend', 'QuviVideo']
diff --git a/modules/quvi/backend.py b/modules/quvi/backend.py
new file mode 100644
index 00000000..7891296c
--- /dev/null
+++ b/modules/quvi/backend.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Vincent A
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+# sample usage: youtube.XXXXXX@quvi
+# or also: https://www.youtube.com/watch?v=XXXXXX@quvi
+# shortened URLs are also supported
+
+# this backend requires the quvi 0.4 C library to be installed
+
+
+import datetime
+from weboob.capabilities.base import UserError, StringField
+from weboob.capabilities.video import ICapVideo, BaseVideo
+from weboob.tools.backend import BaseBackend
+from weboob.tools.capabilities.thumbnail import Thumbnail
+from weboob.tools.misc import to_unicode
+
+from quvi import LibQuvi, QuviError
+
+
+__all__ = ['QuviBackend', 'QuviVideo']
+
+
+class QuviBackend(BaseBackend, ICapVideo):
+ NAME = 'quvi'
+ DESCRIPTION = u'Multi-website video helper with quvi. Handles Youtube, BBC, and a lot more'
+ MAINTAINER = u'Vincent A'
+ EMAIL = 'dev@indigo.re'
+ LICENSE = 'AGPLv3+'
+ VERSION = '0.h'
+
+ BROWSER = None
+
+ def get_video(self, _id):
+ video = QuviVideo(_id)
+
+ parser = LibQuvi()
+ if not parser.load():
+ raise UserError('Make sure libquvi 0.4 is installed')
+
+ try:
+ info = parser.get_info(video.page_url)
+ except QuviError as qerror:
+ raise UserError(qerror.message)
+
+ video.url = to_unicode(info.get('url'))
+ if not video.url:
+ raise NotImplementedError()
+
+ video.ext = to_unicode(info.get('suffix'))
+ video.title = to_unicode(info.get('title'))
+ video.page = to_unicode(info.get('page'))
+ duration = int(info.get('duration', 0))
+ if duration:
+ video.duration = datetime.timedelta(milliseconds=duration)
+ if info.get('thumbnail'):
+ video.thumbnail = Thumbnail(info.get('thumbnail'))
+ return video
+
+
+class QuviVideo(BaseVideo):
+ BACKENDS = {
+ 'youtube': 'https://www.youtube.com/watch?v=%s',
+ 'vimeo': 'http://vimeo.com/%s',
+ 'dailymotion': 'http://www.dailymotion.com/video/%s',
+ 'metacafe': 'http://www.metacafe.com/watch/%s/',
+ 'arte': 'http://videos.arte.tv/fr/videos/plop--%s.html',
+ 'videa': 'http://videa.hu/videok/%s/',
+ 'wimp': 'http://www.wimp.com/%s/',
+ 'funnyordie': 'http://www.funnyordie.com/videos/%s/',
+ 'tapuz': 'http://flix.tapuz.co.il/v/watch-%s-.html',
+ 'liveleak': 'http://www.liveleak.com/view?i=%s',
+ # nsfw
+ 'xhamster': 'https://xhamster.com/movies/%s/plop.html',
+ 'xvideos': 'http://www.xvideos.com/video%s/',
+ 'redtube': 'http://www.redtube.com/%s',
+ 'xnxx': 'http://video.xnxx.com/video%s/',
+ # more websites are supported, but . isn't always enough
+ # however, URLs are supported, like this:
+ # https://www.youtube.com/watch?v=BaW_jenozKc@quvi
+ }
+
+ page = StringField('Page URL of the video')
+
+ @classmethod
+ def id2url(cls, _id):
+ if _id.startswith('http'):
+ return _id
+
+ sub_backend, sub_id = _id.split('.', 1)
+ try:
+ return cls.BACKENDS[sub_backend] % sub_id
+ except KeyError:
+ raise NotImplementedError()
+
+ @property
+ def page_url(self):
+ if self.page:
+ return self.page
+ else:
+ return self.id2url(self.id)
diff --git a/modules/quvi/favicon.png b/modules/quvi/favicon.png
new file mode 100644
index 00000000..a84132a8
Binary files /dev/null and b/modules/quvi/favicon.png differ
diff --git a/modules/quvi/quvi.py b/modules/quvi/quvi.py
new file mode 100644
index 00000000..f7fbb4a0
--- /dev/null
+++ b/modules/quvi/quvi.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Vincent A
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from ctypes import cdll, c_char_p, c_double, c_void_p, byref
+from ctypes.util import find_library
+
+
+__all__ = ['LibQuvi', 'QuviError']
+
+
+class QuviError(Exception):
+ pass
+
+
+class LibQuvi04(object):
+ QUVI_VERSION = 0
+
+ QUVIOPT_FORMAT = 0
+ QUVIOPT_CATEGORY = 4
+ QUVI_OK = 0
+ QUVI_LAST = 5
+
+ QUVIPROP_PAGEURL = 0x100002
+ QUVIPROP_PAGETITLE = 0x100003
+ QUVIPROP_MEDIAID = 0x100004
+ QUVIPROP_MEDIAURL = 0x100005
+ QUVIPROP_FILESUFFIX = 0x100008
+ #~ QUVIPROP_FORMAT = 0x10000A
+ QUVIPROP_MEDIATHUMBNAILURL = 0x10000C
+ QUVIPROP_MEDIACONTENTLENGTH = 0x300006
+ QUVIPROP_MEDIADURATION = 0x30000D
+
+ QUVIPROTO_HTTP = 1
+ QUVIPROTO_RTMP = 8
+
+ def __init__(self, lib=None):
+ self.lib = lib
+ self.qh = c_void_p()
+ self.qmh = c_void_p()
+
+ def load(self):
+ path = find_library('quvi')
+ if not path:
+ return False
+
+ self.lib = cdll.LoadLibrary(path)
+ if self.lib is None:
+ return False
+
+ version_str = c_char_p(self.lib.quvi_version(self.QUVI_VERSION)).value
+ if version_str.startswith('v0.4'):
+ return True
+ else:
+ return False
+
+ def _cleanup(self):
+ if self.qmh:
+ self.lib.quvi_parse_close(byref(self.qmh))
+ self.qmh = c_void_p()
+ if self.qh:
+ self.lib.quvi_close(byref(self.qh))
+ self.qh = c_void_p()
+
+ def get_info(self, url):
+ try:
+ return self._get_info(url)
+ finally:
+ self._cleanup()
+
+ def _get_info(self, url):
+ status = self.lib.quvi_init(byref(self.qh))
+ self._assert_ok(status)
+
+ status = self.lib.quvi_setopt(self.qh, self.QUVIOPT_FORMAT, 'best')
+ self._assert_ok(status)
+
+ status = self.lib.quvi_parse(self.qh, c_char_p(url), byref(self.qmh))
+ self._assert_ok(status)
+
+ info = {}
+ info['url'] = self._get_str(self.QUVIPROP_MEDIAURL)
+ info['title'] = self._get_str(self.QUVIPROP_PAGETITLE)
+ info['suffix'] = self._get_str(self.QUVIPROP_FILESUFFIX)
+ info['page'] = self._get_str(self.QUVIPROP_PAGEURL) # uncut!
+ info['media_id'] = self._get_str(self.QUVIPROP_MEDIAID)
+ info['thumbnail'] = self._get_str(self.QUVIPROP_MEDIATHUMBNAILURL)
+ info['duration'] = self._get_double(self.QUVIPROP_MEDIADURATION)
+ info['size'] = self._get_double(self.QUVIPROP_MEDIACONTENTLENGTH)
+
+ return info
+
+ def _assert_ok(self, status):
+ if status != self.QUVI_OK:
+ c_msg = c_char_p(self.lib.quvi_strerror(self.qh, status))
+ raise QuviError(c_msg.value)
+
+ def _get_str(self, prop):
+ c_value = c_char_p()
+ status = self.lib.quvi_getprop(self.qmh, prop, byref(c_value))
+ self._assert_ok(status)
+
+ return c_value.value
+
+ def _get_double(self, prop):
+ c_value = c_double()
+ status = self.lib.quvi_getprop(self.qmh, prop, byref(c_value))
+ self._assert_ok(status)
+
+ return c_value.value
+
+
+LibQuvi = LibQuvi04
diff --git a/modules/quvi/test.py b/modules/quvi/test.py
new file mode 100644
index 00000000..ee2c0322
--- /dev/null
+++ b/modules/quvi/test.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Vincent A
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from weboob.tools.test import BackendTest
+
+
+class QuviTest(BackendTest):
+ BACKEND = 'quvi'
+
+ def test_get_id(self):
+ v = self.backend.get_video('youtube.BaW_jenozKc')
+ assert len(v.url)
+ assert len(v.title)
+ assert (v.page_url == 'https://www.youtube.com/watch?v=BaW_jenozKc')
+
+ def test_get_url(self):
+ v = self.backend.get_video('https://www.youtube.com/watch?v=BaW_jenozKc')
+ assert len(v.url)
+ assert len(v.title)
+ # did we retrieve more?
+ assert len(v.ext)
+ assert v.duration
+ assert v.thumbnail
+ assert v.page_url == 'https://www.youtube.com/watch?v=BaW_jenozKc'
+
+ def test_get_shortened(self):
+ v = self.backend.get_video('http://youtu.be/BaW_jenozKc')
+ assert len(v.url)
+ assert len(v.title)
+ assert v.page_url.startswith('http://www.youtube.com/watch?v=BaW_jenozKc')
+