diff --git a/modules/canalplus/browser.py b/modules/canalplus/browser.py
index f59e117a..8dae0251 100644
--- a/modules/canalplus/browser.py
+++ b/modules/canalplus/browser.py
@@ -28,13 +28,13 @@ from weboob.tools.browser.decorators import id2url
from .pages import InitPage, VideoPage
from .video import CanalplusVideo
-from weboob.capabilities.collection import Collection, CollectionNotFound
+from weboob.capabilities.collection import CollectionNotFound
__all__ = ['CanalplusBrowser']
class XMLParser(object):
- def parse(self, data, encoding=None):
+ def parse(self, data, encoding=None):
if encoding is None:
parser = None
else:
@@ -60,11 +60,8 @@ class CanalplusBrowser(BaseBrowser):
}
def __init__(self, quality, *args, **kwargs):
- BaseBrowser.__init__(self, parser= self.PARSER, *args, **kwargs)
- if quality in self.FORMATS:
- self.quality = self.FORMATS[quality]
- else:
- self.quality = 'HD'
+ BaseBrowser.__init__(self, parser=self.PARSER, *args, **kwargs)
+ self.quality = self.FORMATS.get(quality, self.FORMATS['hd'])
def home(self):
self.location('http://service.canal-plus.com/video/rest/initPlayer/cplus/')
@@ -79,19 +76,33 @@ class CanalplusBrowser(BaseBrowser):
return self.page.get_video(video, self.quality)
def iter_resources(self, split_path):
- self.home()
- collections = self.page.collections
+ if not self.is_on_page(InitPage):
+ self.home()
+ channels = self.page.get_channels()
- def walk_res(path, collections):
- if len(path) == 0 or not isinstance(collections, (list, Collection)):
- return collections
- i = path[0]
- matches = [collection
- for collection in collections
- if collection.id == i or collection.title == i]
- if not len(matches):
- raise CollectionNotFound(path)
+ if len(split_path) == 0:
+ for channel in channels:
+ if len(channel.split_path) == 1:
+ yield channel
+ elif len(split_path) == 1:
+ for channel in channels:
+ if len(channel.split_path) == 2 and split_path[0] == channel.split_path[0]:
+ yield channel
+ elif len(split_path) == 2:
+ subchannels = self.iter_resources(split_path[0:1])
+ channel = None
+ for subchannel in subchannels:
+ # allow matching by title for backward compatibility (for now)
+ if split_path[0] == subchannel.split_path[0] and \
+ split_path[1] in (subchannel.split_path[1], subchannel.title):
+ channel = subchannel
+ if channel:
+ self.location("http://service.canal-plus.com/video/rest/getMEAs/cplus/%s" % channel.id)
+ assert self.is_on_page(VideoPage)
+ for video in self.page.iter_channel():
+ yield video
+ else:
+ raise CollectionNotFound(split_path)
- return walk_res(path[1:], matches[0])
-
- return walk_res(split_path, collections)
+ else:
+ raise CollectionNotFound(split_path)
diff --git a/modules/canalplus/pages/initpage.py b/modules/canalplus/pages/initpage.py
index 1df73421..a5709f60 100644
--- a/modules/canalplus/pages/initpage.py
+++ b/modules/canalplus/pages/initpage.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright(C) 2010-2011 Nicolas Duhamel
+# Copyright(C) 2010-2012 Nicolas Duhamel, Laurent Bachelier
#
# This file is part of weboob.
#
@@ -26,24 +26,19 @@ __all__ = ['InitPage']
class InitPage(BasePage):
- def on_loaded(self):
- self.collections = []
-
- def do(_id):
- self.browser.location("http://service.canal-plus.com/video/rest/getMEAs/cplus/%s" % _id)
- return self.browser.page.iter_channel()
-
- # Parse the list of channels
+ def get_channels(self):
+ """
+ Extract all possible channels (paths) from the page
+ """
+ channels = list()
for elem in self.document[2].getchildren():
- children = []
for e in elem.getchildren():
if e.tag == "NOM":
- _id = e.text.strip()
+ name = e.text.strip()
+ channels.append(Collection([name]))
elif e.tag == "SELECTIONS":
for select in e:
- sub = Collection(_id=select[0].text,
- title=select[1].text.strip(),
- fct=do)
- children.append(sub)
- coll = Collection(_id, children=children)
- self.collections.append(coll)
+ sub = Collection([name, select[0].text],
+ title=select[1].text.strip())
+ channels.append(sub)
+ return channels
diff --git a/modules/radiofrance/backend.py b/modules/radiofrance/backend.py
index a474ed22..672bfe8b 100644
--- a/modules/radiofrance/backend.py
+++ b/modules/radiofrance/backend.py
@@ -104,7 +104,7 @@ class RadioFranceBackend(BaseBackend, ICapRadio, ICapCollection, ICapVideo):
def iter_resources(self, objs, split_path):
if Radio in objs:
- if len(split_path) == 1 and split_path[0] == 'francebleu':
+ if split_path == [u'francebleu']:
for _id in sorted(self._RADIOS.iterkeys()):
if _id.startswith('fb'):
yield self.get_radio(_id)
@@ -112,13 +112,12 @@ class RadioFranceBackend(BaseBackend, ICapRadio, ICapCollection, ICapVideo):
for _id in sorted(self._RADIOS.iterkeys()):
if not _id.startswith('fb'):
yield self.get_radio(_id)
- yield Collection('francebleu', 'France Bleu',
- children=self.iter_resources(objs, ['francebleu']))
+ yield Collection(['francebleu'], 'France Bleu')
else:
raise CollectionNotFound(split_path)
def iter_radios_search(self, pattern):
- for radio in self._flatten_resources(self.iter_resources((Radio, ), [])):
+ for radio in self.iter_resources_flat((Radio, ), []):
if pattern.lower() in radio.title.lower() or pattern.lower() in radio.description.lower():
yield radio
diff --git a/modules/redmine/backend.py b/modules/redmine/backend.py
index 745513c1..7b3d6714 100644
--- a/modules/redmine/backend.py
+++ b/modules/redmine/backend.py
@@ -97,7 +97,7 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
def iter_resources(self, objs, split_path):
if Project in objs or Issue in objs:
if len(split_path) == 0:
- return [Collection(project.id, project.name, fct=self.iter_issues)
+ return [Collection([project.id], project.name)
for project in self.iter_projects()]
if len(split_path) == 1:
diff --git a/weboob/applications/boobtracker/boobtracker.py b/weboob/applications/boobtracker/boobtracker.py
index 427d4bb8..544b3d0c 100644
--- a/weboob/applications/boobtracker/boobtracker.py
+++ b/weboob/applications/boobtracker/boobtracker.py
@@ -136,7 +136,7 @@ class BoobTracker(ReplApplication):
query.category = self.options.category
query.status = self.options.status
- self.change_path('/%s/search' % query.project)
+ self.change_path([query.project, u'search'])
for backend, issue in self.do('iter_issues', query, backends=backends):
self.add_object(issue)
self.format(issue)
diff --git a/weboob/applications/flatboob/flatboob.py b/weboob/applications/flatboob/flatboob.py
index 47fad288..818cb5bd 100644
--- a/weboob/applications/flatboob/flatboob.py
+++ b/weboob/applications/flatboob/flatboob.py
@@ -147,7 +147,7 @@ class Flatboob(ReplApplication):
query.cost_max = self.ask_int('Enter max cost')
query.nb_rooms = self.ask_int('Enter number of rooms')
- self.change_path('/housings')
+ self.change_path([u'housings'])
for backend, housing in self.do('search_housings', query):
self.add_object(housing)
self.format(housing)
diff --git a/weboob/applications/radioob/radioob.py b/weboob/applications/radioob/radioob.py
index 16530471..9945ab18 100644
--- a/weboob/applications/radioob/radioob.py
+++ b/weboob/applications/radioob/radioob.py
@@ -135,7 +135,7 @@ class Radioob(ReplApplication):
If PATTERN is not given, this command will list all the radios.
"""
self.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'All radios')
- self.change_path('/search')
+ self.change_path([u'search'])
for backend, radio in self.do('iter_radios_search', pattern=pattern):
self.add_object(radio)
self.format(radio)
diff --git a/weboob/applications/videoob/videoob.py b/weboob/applications/videoob/videoob.py
index 67611c55..688028d8 100644
--- a/weboob/applications/videoob/videoob.py
+++ b/weboob/applications/videoob/videoob.py
@@ -223,7 +223,7 @@ class Videoob(ReplApplication):
return 1
self.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'Latest videos')
- self.change_path('/search')
+ self.change_path([u'search'])
for backend, video in self.do('search_videos', pattern=pattern, nsfw=self.nsfw,
max_results=self.options.count):
self.add_object(video)
diff --git a/weboob/applications/weboorrents/weboorrents.py b/weboob/applications/weboorrents/weboorrents.py
index 6008f530..0e47cd65 100644
--- a/weboob/applications/weboorrents/weboorrents.py
+++ b/weboob/applications/weboorrents/weboorrents.py
@@ -175,7 +175,7 @@ class Weboorrents(ReplApplication):
Search torrents.
"""
- self.change_path('/search')
+ self.change_path([u'search'])
if not pattern:
pattern = None
self.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'Latest torrents')
diff --git a/weboob/applications/wetboobs/wetboobs.py b/weboob/applications/wetboobs/wetboobs.py
index 70407654..48c34baf 100644
--- a/weboob/applications/wetboobs/wetboobs.py
+++ b/weboob/applications/wetboobs/wetboobs.py
@@ -96,7 +96,7 @@ class WetBoobs(ReplApplication):
Search cities.
"""
- self.change_path('/cities')
+ self.change_path(['cities'])
for backend, city in self.do('iter_city_search', pattern, caps=ICapWeather):
self.add_object(city)
self.format(city)
@@ -143,7 +143,7 @@ class WetBoobs(ReplApplication):
List all rivers. If PATTERN is specified, search on a pattern.
"""
- self.change_path('/gauges')
+ self.change_path([u'gauges'])
for backend, gauge in self.do('iter_gauges', pattern or None, caps=ICapWaterLevel):
self.add_object(gauge)
self.format(gauge)
diff --git a/weboob/capabilities/collection.py b/weboob/capabilities/collection.py
index 725c58a4..6bfb8e44 100644
--- a/weboob/capabilities/collection.py
+++ b/weboob/capabilities/collection.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
-from .base import IBaseCap
+from .base import IBaseCap, CapBaseObject
__all__ = ['ICapCollection', 'Collection', 'CollectionNotFound']
@@ -31,42 +31,18 @@ class CollectionNotFound(Exception):
Exception.__init__(self, msg)
-class Children(object):
+class Collection(CapBaseObject):
"""
- Dynamic property of a Collection.
- Returns a list, either by calling a function or because
- it already has the list.
- """
- def __get__(self, obj, type=None):
- if obj._children is None:
- if callable(obj._fct):
- obj._children = obj._fct(obj.id)
- return obj._children or []
+ A Collection is a "fake" object returned in results, which shows you can get
+ more results if you go into its path.
-
-class Collection(object):
+ It is a dumb object, it must not contain callbacks to a backend.
"""
- Collection of objects.
- Should provide a way to be filled, either by providing the children
- right away, or a function. The function will be called once with the id
- as an argument if there were no children provided, but only on demand.
- It can be found in a list of objects, it indicantes a "folder"
- you can hop into.
- id and title should be unicode.
- """
- children = Children()
- backend = None
-
- def __init__(self, _id=None, title=None, children=None, fct=None):
- self.id = _id
+ def __init__(self, split_path, backend=None, title=None):
+ self.split_path = split_path
self.title = title
- # It does not make sense to have both at init
- assert not (fct is not None and children is not None)
- self._children = children
- self._fct = fct
-
- def __iter__(self):
- return iter(self.children)
+ _id = split_path[-1] if len(split_path) else None
+ CapBaseObject.__init__(self, _id, backend)
def __unicode__(self):
if self.title and self.id:
@@ -78,19 +54,19 @@ class Collection(object):
class ICapCollection(IBaseCap):
- def _flatten_resources(self, resources, clean_only=False):
+ def iter_resources_flat(self, objs, split_path, clean_only=False):
"""
- Expand all collections in a list
- If clean_only is True, do not expand collections, only remove them.
+ Call iter_resources() to fetch all resources in the tree.
+ If clean_only is True, do not explore paths, only remove them.
+ split_path is used to set the starting path.
"""
- lst = list()
- for resource in resources:
- if isinstance(resource, (list, Collection)):
+ for resource in self.iter_resources(objs, split_path):
+ if isinstance(resource, Collection):
if not clean_only:
- lst.extend(self._flatten_resources(resource))
+ for res in self.iter_resources_flat(objs, resource.split_path):
+ yield res
else:
- lst.append(resource)
- return lst
+ yield resource
def iter_resources(self, objs, split_path):
"""
diff --git a/weboob/tools/application/repl.py b/weboob/tools/application/repl.py
index d9f5c647..e636fca6 100644
--- a/weboob/tools/application/repl.py
+++ b/weboob/tools/application/repl.py
@@ -30,7 +30,7 @@ from weboob.capabilities.base import FieldNotFound, CapBaseObject
from weboob.core import CallErrors
from weboob.tools.application.formatters.iformatter import MandatoryFieldsNotFound
from weboob.tools.misc import to_unicode
-from weboob.tools.path import Path
+from weboob.tools.path import WorkingPath
from weboob.tools.ordereddict import OrderedDict
from weboob.capabilities.collection import Collection, ICapCollection, CollectionNotFound
@@ -137,23 +137,22 @@ class ReplApplication(Cmd, ConsoleApplication):
self._interactive = False
self.objects = []
self.collections = []
- self.working_path = Path()
+ self.working_path = WorkingPath()
@property
def interactive(self):
return self._interactive
def _change_prompt(self):
- path = self.working_path.tostring()
- if len(path) > 0 and path != '/':
- self.prompt = '%s:%s> ' % (self.APPNAME, path)
+ if len(self.working_path.get()):
+ self.prompt = u'%s:%s> ' % (self.APPNAME, unicode(self.working_path))
else:
- self.prompt = '%s> ' % (self.APPNAME)
+ self.prompt = u'%s> ' % (self.APPNAME)
self.objects = []
self.collections = []
- def change_path(self, path):
- self.working_path.fromstring(path)
+ def change_path(self, split_path):
+ self.working_path.location(split_path)
self._change_prompt()
def add_object(self, obj):
@@ -885,16 +884,19 @@ class ReplApplication(Cmd, ConsoleApplication):
cd [PATH]
Follow a path.
- If empty, return home.
+ ".." is a special case and goes up one directory.
+ "" is a special case and goes home.
"""
if not len(line.strip()):
self.working_path.home()
+ elif line.strip() == '..':
+ self.working_path.up()
else:
- self.working_path.extend(line)
+ self.working_path.cd1(line)
objects, collections = self._fetch_objects(objs=self.COLLECTION_OBJECTS)
if len(objects) + len(collections) == 0:
- print >>sys.stderr, "Path: %s not found" % self.working_path.tostring()
+ print >>sys.stderr, u"Path: %s not found" % unicode(self.working_path)
self.working_path.restore()
return 1
diff --git a/weboob/tools/path.py b/weboob/tools/path.py
index a148360e..aa07ade3 100644
--- a/weboob/tools/path.py
+++ b/weboob/tools/path.py
@@ -16,64 +16,80 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
-import urllib
-import posixpath
-import copy
+from copy import copy
+from posixpath import sep, join
-class Path(object):
+
+class WorkingPath(object):
def __init__(self):
- self._working_path = []
- self._previous = self._working_path
+ self.split_path = []
+ self.previous = copy(self.split_path)
-
- def extend(self, user_input):
+ def cd1(self, user_input):
"""
- Add a new part to the current path
+ Append *one* level to the current path.
+ This means that separators (/) will get escaped.
"""
+ split_path = self.get()
+ split_path.append(user_input)
+ self.location(split_path)
- user_input = urllib.quote_plus(user_input)
- user_input = posixpath.normpath(user_input)
-
- escape = lambda s: s.replace('/', '%2F')
- current_path = map(escape, self._working_path)
-
- abspath = posixpath.normpath(posixpath.join('/' + '/'.join(current_path), user_input))
-
- abspath = abspath.split('/')[1:]
- while len(abspath) > 0 and abspath[0] == u'': del abspath[0]
-
- final_parse = map(urllib.unquote_plus, abspath)
-
- self._previous = self._working_path
-
- if len(final_parse) == 0:
- self._working_path = []
-
- self._working_path = final_parse
+ def location(self, split_path):
+ """
+ Go to a new path, and store the previous path.
+ """
+ self.previous = self.get()
+ self.split_path = split_path
def restore(self):
"""
Go to the previous path
"""
- self._working_path = self._previous
+ self.split_path, self.previous = self.previous, self.split_path
def home(self):
"""
Go to the root
"""
- self._previous = self._working_path
- self._working_path = []
+ self.location([])
+
+ def up(self):
+ """
+ Go up one directory
+ """
+ self.location(self.split_path[:-1])
def get(self):
- return copy.copy(self._working_path)
+ """
+ Get the current working path
+ """
+ return copy(self.split_path)
- def fromstring(self, path):
- if path[0] == '/':
- path = path[1:]
- escape = lambda s: s.replace('\/', '/')
- self._working_path = map(escape, path.split('/'))
+ def __unicode__(self):
+ return join(sep, *[s.replace(u'/', u'\/') for s in self.split_path])
- def tostring(self):
- escape = lambda s: s.replace('/', '\/')
- path = map(escape, self._working_path)
- return '/' + '/'.join(path)
+
+def test():
+ wp = WorkingPath()
+ assert wp.get() == []
+ assert unicode(wp) == u'/'
+ wp.cd1(u'lol')
+ assert wp.get() == [u'lol']
+ assert unicode(wp) == u'/lol'
+ wp.cd1(u'cat')
+ assert wp.get() == [u'lol', u'cat']
+ assert unicode(wp) == u'/lol/cat'
+ wp.restore()
+ assert unicode(wp) == u'/lol'
+ wp.home()
+ assert wp.get() == []
+ assert unicode(wp) == u'/'
+ wp.up()
+ assert wp.get() == []
+ assert unicode(wp) == u'/'
+ wp.location(['aa / aa', 'bbbb'])
+ assert unicode(wp) == u'/aa \/ aa/bbbb'
+ wp.up()
+ assert unicode(wp) == u'/aa \/ aa'
+ wp.cd1(u'héhé/hé')
+ assert unicode(wp) == u'/aa \/ aa/héhé\/hé'