about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.163
-rw-r--r--doc/ranger.pod66
-rw-r--r--ranger/config/commands.py86
-rw-r--r--ranger/config/rc.conf10
-rw-r--r--ranger/container/fsobject.py2
-rw-r--r--ranger/container/settings.py2
-rw-r--r--ranger/core/actions.py8
-rw-r--r--ranger/core/fm.py8
-rw-r--r--ranger/core/metadata.py143
-rw-r--r--ranger/ext/openstruct.py15
-rw-r--r--ranger/ext/papermanager.py171
-rw-r--r--ranger/gui/widgets/browsercolumn.py26
12 files changed, 272 insertions, 328 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1
index 49339bf2..b27aa9f3 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "RANGER 1"
-.TH RANGER 1 "ranger-1.6.1" "01/01/2015" "ranger manual"
+.TH RANGER 1 "ranger-1.6.1" "01/17/2015" "ranger manual"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -686,14 +686,14 @@ limit.
 .IP "max_history_size [integer, none]" 4
 .IX Item "max_history_size [integer, none]"
 How many directory changes should be kept in history?
+.IP "metadata_deep_search [bool]" 4
+.IX Item "metadata_deep_search [bool]"
+When the metadata manager module looks for metadata, should it only look for a
+\&\*(L".metadata.json\*(R" file in the current directory, or do a deep search and check
+all directories above the current one as well?
 .IP "mouse_enabled [bool] <zm>" 4
 .IX Item "mouse_enabled [bool] <zm>"
 Enable mouse input?
-.IP "papermanager_deep_search [bool]" 4
-.IX Item "papermanager_deep_search [bool]"
-When the paper manager module looks for metadata, should it only look for a
-\&\*(L".paperinfo\*(R" file in the current directory, or do a deep search and check all
-directories above the current one as well?
 .IP "padding_right [bool]" 4
 .IX Item "padding_right [bool]"
 When collapse_preview is on and there is no preview, should there remain a
@@ -822,14 +822,11 @@ ranger.  For your convenience, this is a list of the \*(L"public\*(R" commands i
 \& map key command
 \& mark pattern
 \& mark_tag [tags]
+\& meta key value
 \& mkdir dirname
 \& open_with [application] [flags] [mode]
-\& paper
-\& paper_authors [authors]
-\& paper_title [title]
-\& paper_url [url]
-\& paper_year [year]
 \& pmap key command
+\& prompt_metadata [key1 [key2 [...]]]
 \& punmap keys...
 \& quit
 \& quit!
@@ -930,8 +927,8 @@ Set the global default linemode to \*(L"permissions\*(R":
 Set the default linemode to \*(L"permissions\*(R" for all files tagged with \*(L"p\*(R" or \*(L"P\*(R":
  :default_linemode tag=pP permissions
 .Sp
-Set the default linemode for all files in ~/books/ to \*(L"papertitle\*(R":
- :default_linemode path=/home/.*?/books/.* papertitle
+Set the default linemode for all files in ~/books/ to \*(L"metatitle\*(R":
+ :default_linemode path=/home/.*?/books/.* metatitle
 .IP "delete" 2
 .IX Item "delete"
 Destroy all files in the selection with a roundhouse kick.  ranger will ask for
@@ -981,11 +978,12 @@ Looks for a string in all marked files or directories.
 .IX Item "linemode linemodename"
 Sets the linemode of all files in the current directory.  The linemode may be:
 .Sp
-.Vb 4
+.Vb 5
 \& "filename": display each line as "<basename>...<size>"
 \& "permissions": display each line as "<permissions> <owner> <group> <basename>"
-\& "papertitle": display metadata from .paperinfo files if available, fall back
-\&     to the "filename" linemode if no metadata was found.  See :paper commands.
+\& "metatitle": display metadata from .metadata.json files if
+\&     available, fall back to the "filename" linemode if no
+\&     metadata was found.  See :meta command.
 .Ve
 .IP "load_copy_buffer" 2
 .IX Item "load_copy_buffer"
@@ -1010,6 +1008,17 @@ This command is based on the \fIscout\fR command and supports all of its options
 .IX Item "mark_tag [tags]"
 Mark all tags that are tagged with either of the given tags.  When leaving out
 the tag argument, all tagged files are marked.
+.IP "meta \fIkey\fR \fIvalue\fR" 2
+.IX Item "meta key value"
+Set the metadata of the currently highlighted file.  Example:
+.Sp
+.Vb 2
+\& :meta title The Hitchhiker\*(Aqs Guide to the Galaxy
+\& :meta year 1979
+.Ve
+.Sp
+This metadata can be displayed by, for example, using the \*(L"metatitle\*(R" line mode
+by typing Mt.
 .IP "mkdir \fIdirname\fR" 2
 .IX Item "mkdir dirname"
 Creates a directory with the name \fIdirname\fR.
@@ -1023,27 +1032,13 @@ of applications is generated by the external file opener \*(L"rifle\*(R" and can
 displayed when pressing \*(L"r\*(R" in ranger.
 .Sp
 Note that if you specify an application, the mode is ignored.
-.IP "paper" 2
-.IX Item "paper"
-This command opens a series of commands on the console that will ask the user
-to input metadata about the current file.  This is used by the paper manager
-module of ranger and can be later displayed in ranger, for example by setting
-the option \*(L"linemode\*(R" to \*(L"papertitle\*(R".
-.IP "paper_authors \fIauthors\fR" 2
-.IX Item "paper_authors authors"
-Tells the paper manager to set/update the authors of the current file
-.IP "paper_title \fItitle\fR" 2
-.IX Item "paper_title title"
-Tells the paper manager to set/update the title of the current file
-.IP "paper_url \fIurl\fR" 2
-.IX Item "paper_url url"
-Tells the paper manager to set/update the url of the current file
-.IP "paper_year \fIyear\fR" 2
-.IX Item "paper_year year"
-Tells the paper manager to set/update the year of the current file
 .IP "pmap \fIkey\fR \fIcommand\fR" 2
 .IX Item "pmap key command"
 Binds keys for the pager. Works like the \f(CW\*(C`map\*(C'\fR command.
+.IP "prompt_metadata [\fIkeys ...\fR]" 2
+.IX Item "prompt_metadata [keys ...]"
+Prompt the user to input metadata with the \f(CW\*(C`meta\*(C'\fR command for multiple keys in
+a row.
 .IP "punmap [\fIkeys ...\fR]" 2
 .IX Item "punmap [keys ...]"
 Removes key mappings of the pager. Works like the \f(CW\*(C`unmap\*(C'\fR command.
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 279e44fd..67297d18 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -676,15 +676,15 @@ limit.
 
 How many directory changes should be kept in history?
 
-=item mouse_enabled [bool] <zm>
+=item metadata_deep_search [bool]
 
-Enable mouse input?
+When the metadata manager module looks for metadata, should it only look for a
+".metadata.json" file in the current directory, or do a deep search and check
+all directories above the current one as well?
 
-=item papermanager_deep_search [bool]
+=item mouse_enabled [bool] <zm>
 
-When the paper manager module looks for metadata, should it only look for a
-".paperinfo" file in the current directory, or do a deep search and check all
-directories above the current one as well?
+Enable mouse input?
 
 =item padding_right [bool]
 
@@ -839,14 +839,11 @@ ranger.  For your convenience, this is a list of the "public" commands including
  map key command
  mark pattern
  mark_tag [tags]
+ meta key value
  mkdir dirname
  open_with [application] [flags] [mode]
- paper
- paper_authors [authors]
- paper_title [title]
- paper_url [url]
- paper_year [year]
  pmap key command
+ prompt_metadata [key1 [key2 [...]]]
  punmap keys...
  quit
  quit!
@@ -961,8 +958,8 @@ Set the global default linemode to "permissions":
 Set the default linemode to "permissions" for all files tagged with "p" or "P":
  :default_linemode tag=pP permissions
 
-Set the default linemode for all files in ~/books/ to "papertitle":
- :default_linemode path=/home/.*?/books/.* papertitle
+Set the default linemode for all files in ~/books/ to "metatitle":
+ :default_linemode path=/home/.*?/books/.* metatitle
 
 =item delete
 
@@ -1023,8 +1020,9 @@ Sets the linemode of all files in the current directory.  The linemode may be:
 
  "filename": display each line as "<basename>...<size>"
  "permissions": display each line as "<permissions> <owner> <group> <basename>"
- "papertitle": display metadata from .paperinfo files if available, fall back
-     to the "filename" linemode if no metadata was found.  See :paper commands.
+ "metatitle": display metadata from .metadata.json files if
+     available, fall back to the "filename" linemode if no
+     metadata was found.  See :meta command.
 
 =item load_copy_buffer
 
@@ -1053,6 +1051,16 @@ This command is based on the I<scout> command and supports all of its options.
 Mark all tags that are tagged with either of the given tags.  When leaving out
 the tag argument, all tagged files are marked.
 
+=item meta I<key> I<value>
+
+Set the metadata of the currently highlighted file.  Example:
+
+ :meta title The Hitchhiker's Guide to the Galaxy
+ :meta year 1979
+
+This metadata can be displayed by, for example, using the "metatitle" line mode
+by typing Mt.
+
 =item mkdir I<dirname>
 
 Creates a directory with the name I<dirname>.
@@ -1068,33 +1076,15 @@ displayed when pressing "r" in ranger.
 
 Note that if you specify an application, the mode is ignored.
 
-=item paper
-
-This command opens a series of commands on the console that will ask the user
-to input metadata about the current file.  This is used by the paper manager
-module of ranger and can be later displayed in ranger, for example by setting
-the option "linemode" to "papertitle".
-
-=item paper_authors I<authors>
-
-Tells the paper manager to set/update the authors of the current file
-
-=item paper_title I<title>
-
-Tells the paper manager to set/update the title of the current file
-
-=item paper_url I<url>
-
-Tells the paper manager to set/update the url of the current file
-
-=item paper_year I<year>
-
-Tells the paper manager to set/update the year of the current file
-
 =item pmap I<key> I<command>
 
 Binds keys for the pager. Works like the C<map> command.
 
+=item prompt_metadata [I<keys ...>]
+
+Prompt the user to input metadata with the C<meta> command for multiple keys in
+a row.
+
 =item punmap [I<keys ...>]
 
 Removes key mappings of the pager. Works like the C<unmap> command.
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index a8813de2..ba7d51d3 100644
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -1348,87 +1348,57 @@ class flat(Command):
         self.fm.thisdir.load_content()
 
 
-# Papermanager commands
+# Metadata commands
 # --------------------------------
-class paper(Command):
+
+class prompt_metadata(Command):
     """
-    :paper
+    :prompt_metadata <key1> [<key2> [<key3> ...]]
 
-    This command opens a series of commands on the console that will ask the
-    user to input metadata about the current file.  This is used by the paper
-    manager module of ranger and can be later displayed in ranger, for example
-    by setting the option "linemode" to "papertitle".
+    Prompt the user to input metadata for multiple keys in a row.
     """
-    _paper_console_chain = None
-    def execute(self):
-        # TODO: This sets a pseudo-global variable containing a stack of
-        # commands that should be opened in the console next.  It's a
-        # work-around for ranger's lack of inherent console command chaining
-        # and will hopefully be implemented properly in the future.
-        paper._paper_console_chain = ["url", "year", "authors", "title"]
 
+    _command_name = "meta"
+    _console_chain = None
+    def execute(self):
+        prompt_metadata._console_chain = self.args[1:]
         self._process_command_stack()
 
     def _process_command_stack(self):
-        if paper._paper_console_chain:
-            key = paper._paper_console_chain.pop()
-            self._paper_fill_console(key)
+        if prompt_metadata._console_chain:
+            key = prompt_metadata._console_chain.pop()
+            self._fill_console(key)
         else:
             for col in self.fm.ui.browser.columns:
                 col.need_redraw = True
 
-    def _paper_fill_console(self, key):
-        paperinfo = self.fm.papermanager.get_paper_info(self.fm.thisfile.path)
-        if key in paperinfo and paperinfo[key]:
-            existing_value = paperinfo[key]
+    def _fill_console(self, key):
+        metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
+        if key in metadata and metadata[key]:
+            existing_value = metadata[key]
         else:
             existing_value = ""
-        text = "paper_%s %s" % (key, existing_value)
+        text = "%s %s %s" % (self._command_name, key, existing_value)
         self.fm.open_console(text, position=len(text))
 
 
-class paper_title(paper):
+class meta(prompt_metadata):
     """
-    :paper_title <title>
+    :meta <key> [<value>]
 
-    Tells the paper manager to set/update the title of the current file
+    Change metadata of a file.  Deletes the key if value is empty.
     """
-    _key = "title"
 
     def execute(self):
+        key = self.arg(1)
+        value = self.rest(1)
         update_dict = dict()
-        update_dict[self._key] = self.rest(1)
-        self.fm.papermanager.set_paper_info(self.fm.thisfile.path, update_dict)
+        update_dict[key] = self.rest(2)
+        self.fm.metadata.set_metadata(self.fm.thisfile.path, update_dict)
         self._process_command_stack()
 
     def tab(self):
-        paperinfo = self.fm.papermanager.get_paper_info(self.fm.thisfile.path)
-        if paperinfo[self._key]:
-            return self.arg(0) + " " + paperinfo[self._key]
-
-
-class paper_authors(paper_title):
-    """
-    :paper_authors <authors>
-
-    Tells the paper manager to set/update the authors of the current file
-    """
-    _key = "authors"
-
-
-class paper_url(paper_title):
-    """
-    :paper_url <authors>
-
-    Tells the paper manager to set/update the url of the current file
-    """
-    _key = "url"
-
-
-class paper_year(paper_title):
-    """
-    :paper_year <authors>
-
-    Tells the paper manager to set/update the year of the current file
-    """
-    _key = "year"
+        key = self.arg(1)
+        metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
+        if key in metadata and metadata[key]:
+            return self.arg(0) + " " + metadata[key]
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 4c8fd799..687d591e 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -174,10 +174,10 @@ set show_selection_in_titlebar true
 # increases CPU load.
 set idle_delay 2000
 
-# When the paper manager module looks for metadata, should it only look for a
-# ".paperinfo" file in the current directory, or do a deep search and check all
-# directories above the current one as well?
-set papermanager_deep_search false
+# When the metadata manager module looks for metadata, should it only look for
+# a ".metadata.json" file in the current directory, or do a deep search and
+# check all directories above the current one as well?
+set metadata_deep_search false
 
 # ===================================================================
 # == Local Options
@@ -240,7 +240,7 @@ map cd console cd
 # Change the line mode
 map Mf linemode filename
 map Mp linemode permissions
-map Mt linemode papertitle
+map Mt linemode metatitle
 
 # Tagging / Marking
 map t       tag_toggle
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 2aba1bfb..08161c3c 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -12,7 +12,7 @@ DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits',
 
 BAD_INFO = '?'
 
-POSSIBLE_LINEMODES = ("filename", "papertitle", "permissions")
+POSSIBLE_LINEMODES = ("filename", "metatitle", "permissions")
 DEFAULT_LINEMODE = "filename"
 
 import re
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index 106ef5fb..55585029 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -28,10 +28,10 @@ ALLOWED_SETTINGS = {
     'idle_delay': int,
     'max_console_history_size': (int, type(None)),
     'max_history_size': (int, type(None)),
+    'metadata_deep_search': bool,
     'mouse_enabled': bool,
     'open_all_images': bool,
     'padding_right': bool,
-    'papermanager_deep_search': bool,
     'preview_directories': bool,
     'preview_files': bool,
     'preview_images': bool,
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 82ceeba3..62092617 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -52,8 +52,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
         self.garbage_collect(-1)
         self.enter_dir(old_path)
         self.change_mode('normal')
-        if self.papermanager:
-            self.papermanager.reset()
+        if self.metadata:
+            self.metadata.reset()
 
     def change_mode(self, mode):
         if mode == self.mode:
@@ -158,8 +158,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
         """
         Change what is displayed as a filename.
 
-        - "mode" may be: "filename", "permissions", "papertitle", the mode "normal"
-          is mapped to "filename".
+        - "mode" may be: "filename", "permissions", "metatitle", the mode
+          "normal" is mapped to "filename".
         - "directory" specifies the directory. None means the current directory
         - "depth" specifies the recursion depth
         """
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 816bf5f1..87c14f7a 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -20,7 +20,7 @@ from ranger.gui.ui import UI
 from ranger.container.bookmarks import Bookmarks
 from ranger.core.runner import Runner
 from ranger.ext.img_display import ImageDisplayer
-from ranger.ext.papermanager import PaperManager
+from ranger.core.metadata import MetadataManager
 from ranger.ext.rifle import Rifle
 from ranger.container.directory import Directory
 from ranger.ext.signals import SignalDispatcher
@@ -61,7 +61,7 @@ class FM(Actions, SignalDispatcher):
         self.loader = Loader()
         self.copy_buffer = set()
         self.do_cut = False
-        self.papermanager = PaperManager()
+        self.metadata = MetadataManager()
 
         try:
             self.username = pwd.getpwuid(os.geteuid()).pw_name
@@ -164,8 +164,8 @@ class FM(Actions, SignalDispatcher):
             self.notify(text, bad=True)
         self.run = Runner(ui=self.ui, logfunc=mylogfunc, fm=self)
 
-        self.settings.signal_bind('setopt.papermanager_deep_search',
-                lambda signal: setattr(signal.fm.papermanager, 'deep_search',
+        self.settings.signal_bind('setopt.metadata_deep_search',
+                lambda signal: setattr(signal.fm.metadata, 'deep_search',
                     signal.value))
 
     def destroy(self):
diff --git a/ranger/core/metadata.py b/ranger/core/metadata.py
new file mode 100644
index 00000000..99872039
--- /dev/null
+++ b/ranger/core/metadata.py
@@ -0,0 +1,143 @@
+# Copyright (C) 2014  Roman Zimbelmann <hut@hut.pm>
+# This software is distributed under the terms of the GNU GPL version 3.
+
+"""
+A Metadata Manager that reads information about files from a json database.
+
+The database is contained in a local .metadata.json file.
+"""
+
+METADATA_FILE_NAME = ".metadata.json"
+DEEP_SEARCH_DEFAULT = False
+
+import copy
+from os.path import join, dirname, exists, basename
+from ranger.ext.openstruct import DefaultOpenStruct as ostruct
+
+class MetadataManager(object):
+    def __init__(self):
+        # metadata_cache maps filenames to dicts containing their metadata
+        self.metadata_cache = dict()
+        # metafile_cache maps .metadata.json filenames to their entries
+        self.metafile_cache = dict()
+        self.deep_search = DEEP_SEARCH_DEFAULT
+
+    def reset(self):
+        self.metadata_cache.clear()
+        self.metafile_cache.clear()
+
+    def get_metadata(self, filename):
+        try:
+            return ostruct(copy.deepcopy(self.metadata_cache[filename]))
+        except KeyError:
+            try:
+                return ostruct(copy.deepcopy(self._get_entry(filename)))
+            except KeyError:
+                return ostruct()
+
+    def set_metadata(self, filename, update_dict):
+        import json
+        result = None
+        found = False
+
+        if not self.deep_search:
+            metafile = next(self._get_metafile_names(filename))
+            return self._set_metadata_raw(filename, update_dict, metafile)
+
+        metafile = self._get_metafile_name(filename)
+        return self._set_metadata_raw(filename, update_dict, metafile)
+
+    def _set_metadata_raw(self, filename, update_dict, metafile):
+        import json
+        valid = (filename, basename(filename))
+
+        entries = self._get_metafile_content(metafile)
+        try:
+            entry = entries[filename]
+        except KeyError:
+            try:
+                entry = entries[basename(filename)]
+            except KeyError:
+                entry = entries[basename(filename)] = {}
+        entry.update(update_dict)
+
+        # Delete key if the value is empty
+        for key, value in update_dict.items():
+            if value == "":
+                del entry[key]
+
+        # Full update of the cache, to be on the safe side:
+        self.metadata_cache[filename] = entry
+        self.metafile_cache[metafile] = entries
+
+        with open(metafile, "w") as f:
+            json.dump(entries, f, check_circular=True, indent=2)
+
+    def _get_entry(self, filename):
+        if filename in self.metadata_cache:
+            return self.metadata_cache[filename]
+        else:
+            valid = (filename, basename(filename))
+
+            # Try to find an entry for this file in any of
+            # the applicable .metadata.json files
+            for metafile in self._get_metafile_names(filename):
+                entries = self._get_metafile_content(metafile)
+                # Check for a direct match:
+                if filename in entries:
+                    entry = entries[filename]
+                # Check for a match of the base name:
+                elif basename(filename) in entries:
+                    entry = entries[basename(filename)]
+                else:
+                    # No match found, try another entry
+                    continue
+
+                self.metadata_cache[filename] = entry
+                return entry
+
+            raise KeyError
+
+    def _get_metafile_content(self, metafile):
+        import json
+        if metafile in self.metafile_cache:
+            return self.metafile_cache[metafile]
+        else:
+            if exists(metafile):
+                with open(metafile, "r") as f:
+                    try:
+                        entries = json.load(f)
+                    except ValueError:
+                        raise ValueError("Failed decoding JSON file %s" %
+                                metafile)
+                self.metafile_cache[metafile] = entries
+                return entries
+            else:
+                return {}
+
+    def _get_metafile_names(self, path):
+        # Iterates through the paths of all .metadata.json files that could
+        # influence the metadata of the given file.
+        # When deep_search is deactivated, this only yields the .metadata.json
+        # file in the same directory as the given file.
+
+        base = dirname(path)
+        yield join(base, METADATA_FILE_NAME)
+        if self.deep_search:
+            dirs = base.split("/")[1:]
+            for i in reversed(range(len(dirs))):
+                yield join("/" + "/".join(dirs[0:i]), METADATA_FILE_NAME)
+
+    def _get_metafile_name(self, filename):
+        first = None
+        for metafile in self._get_metafile_names(filename):
+            if first is None:
+                first = metafile
+
+            entries = self._get_metafile_content(metafile)
+            if filename in entries or basename(filename) in entries:
+                return metafile
+
+        # _get_metafile_names should return >0 names, but just in case...:
+        assert first is not None, "failed finding location for .metadata.json"
+        return first
diff --git a/ranger/ext/openstruct.py b/ranger/ext/openstruct.py
index 5881fd9f..55dd0a40 100644
--- a/ranger/ext/openstruct.py
+++ b/ranger/ext/openstruct.py
@@ -1,6 +1,8 @@
 # Copyright (C) 2009-2013  Roman Zimbelmann <hut@hut.pm>
 # This software is distributed under the terms of the GNU GPL version 3.
 
+import collections
+
 # prepend __ to arguments because one might use "args"
 # or "keywords" as a keyword argument.
 
@@ -9,3 +11,16 @@ class OpenStruct(dict):
     def __init__(self, *__args, **__keywords):
         dict.__init__(self, *__args, **__keywords)
         self.__dict__ = self
+
+
+class DefaultOpenStruct(collections.defaultdict):
+    """The fusion of dict and struct, with default values"""
+    def __init__(self, *__args, **__keywords):
+        collections.defaultdict.__init__(self, None, *__args, **__keywords)
+        self.__dict__ = self
+
+    def __getattr__(self, name):
+        if name not in self.__dict__:
+            return None
+        else:
+            return self.__dict__[name]
diff --git a/ranger/ext/papermanager.py b/ranger/ext/papermanager.py
deleted file mode 100644
index 1f258af3..00000000
--- a/ranger/ext/papermanager.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# Copyright (C) 2014  Roman Zimbelmann <hut@hut.pm>
-# This software is distributed under the terms of the GNU GPL version 3.
-
-"""
-A Paper Manager that reads metadata information about papers from a file.
-
-The file is named .paperinfo and is formatted as comma-separated values.
-
-The columns are:
-1. Filename
-2. Date
-3. Title
-4. Authors
-5. URL
-"""
-
-PAPERINFO_FILE_NAME = ".paperinfo"
-DEEP_SEARCH_DEFAULT = False
-
-import csv
-from os.path import join, dirname, exists, basename
-
-from ranger.ext.openstruct import OpenStruct
-
-class PaperManager(object):
-    def __init__(self):
-        self.metadata_cache = dict()
-        self.metafile_cache = dict()
-        self.deep_search = DEEP_SEARCH_DEFAULT
-
-    def reset(self):
-        self.metadata_cache.clear()
-        self.metafile_cache.clear()
-
-    def get_paper_info(self, filename):
-        try:
-            return self.metadata_cache[filename]
-        except KeyError:
-            result = OpenStruct(filename=filename, title=None, year=None,
-                    authors=None, url=None)
-
-            valid = (filename, basename(filename))
-            for metafile in self._get_metafile_names(filename):
-                for entry in self._get_metafile_content(metafile):
-                    if entry[0] in valid:
-                        self._fill_ostruct_with_data(result, entry)
-                        self.metadata_cache[filename] = result
-                        return result
-
-            # Cache the value
-            self.metadata_cache[filename] = result
-            return result
-
-    def set_paper_info(self, filename, update_dict):
-        result = None
-        found = False
-        valid = (filename, basename(filename))
-        first_metafile = None
-
-        if not self.deep_search:
-            metafile = next(self._get_metafile_names(filename))
-            return self._set_paper_info_raw(filename, update_dict, metafile)
-
-        for i, metafile in enumerate(self._get_metafile_names(filename)):
-            if i == 0:
-                first_metafile = metafile
-
-            csvfile = None
-            try:
-                csvfile = open(metafile, "r")
-            except:
-                # .paperinfo file doesn't exist... look for another one.
-                pass
-            else:
-                reader = csv.reader(csvfile, skipinitialspace=True)
-                for row in reader:
-                    name, year, title, authors, url = row
-                    if name in valid:
-                        return self._set_paper_info_raw(filename, update_dict,
-                                metafile)
-                self.metadata_cache[filename] = result
-            finally:
-                if csvfile:
-                    csvfile.close()
-
-        # No .paperinfo file found, so let's create a new one in the same path
-        # as the given file.
-        if first_metafile:
-            return self._set_paper_info_raw(filename, update_dict, first_metafile)
-
-    def _set_paper_info_raw(self, filename, update_dict, metafile):
-        valid = (filename, basename(filename))
-        paperinfo = OpenStruct(filename=filename, title=None, year=None,
-                authors=None, url=None)
-
-        try:
-            with open(metafile, "r") as infile:
-                reader = csv.reader(infile, skipinitialspace=True)
-                rows = list(reader)
-        except IOError:
-            rows = []
-
-        with open(metafile, "w") as outfile:
-            writer = csv.writer(outfile)
-            found = False
-
-            # Iterate through all rows and write them back to the file.
-            for row in rows:
-                if not found and row[0] in valid:
-                    # When finding the row that corresponds to the given filename,
-                    # update the items with the information from update_dict.
-                    self._fill_row_with_ostruct(row, update_dict)
-                    self._fill_ostruct_with_data(paperinfo, row)
-                    self.metadata_cache[filename] = paperinfo
-                    found = True
-                writer.writerow(row)
-
-            # If the row was never found, create a new one.
-            if not found:
-                row = [basename(filename), None, None, None, None]
-                self._fill_row_with_ostruct(row, update_dict)
-                self._fill_ostruct_with_data(paperinfo, row)
-                self.metadata_cache[filename] = paperinfo
-                writer.writerow(row)
-
-    def _get_metafile_content(self, metafile):
-        if metafile in self.metafile_cache:
-            return self.metafile_cache[metafile]
-        else:
-            if exists(metafile):
-                reader = csv.reader(open(metafile, "r"), skipinitialspace=True)
-
-                entries = list(entry for entry in reader if len(entry) == 5)
-                self.metafile_cache[metafile] = entries
-                return entries
-            else:
-                return []
-
-    def _get_metafile_names(self, path):
-        # Iterates through the paths of all .paperinfo files that could
-        # influence the metadata of the given file.
-        # When deep_search is deactivated, this only yields the .paperinfo file
-        # in the same directory as the given file.
-
-        base = dirname(path)
-        yield join(base, PAPERINFO_FILE_NAME)
-        if self.deep_search:
-            dirs = base.split("/")[1:]
-            for i in reversed(range(len(dirs))):
-                yield join("/" + "/".join(dirs[0:i]), PAPERINFO_FILE_NAME)
-
-    def _fill_ostruct_with_data(self, ostruct, dataset):
-        # Copy data from a CSV row to a dict/ostruct
-
-        filename, year, title, authors, url = dataset
-        if year:    ostruct['year']    = year
-        if title:   ostruct['title']   = title
-        if authors: ostruct['authors'] = authors
-        if url:     ostruct['url']     = url
-
-    def _fill_row_with_ostruct(self, row, update_dict):
-        # Copy data from a dict/ostruct into a CSV row
-        for key, value in update_dict.items():
-            if key == "year":
-                row[1] = value
-            elif key == "title":
-                row[2] = value
-            elif key == "authors":
-                row[3] = value
-            elif key == "url":
-                row[4] = value
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 7fe4927b..00cac553 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -249,17 +249,18 @@ class BrowserColumn(Pager):
                 tagged_marker = " "
 
             # Extract linemode-related information from the drawn object
-            paperinfo = None
+            metadata = None
             use_linemode = drawn._linemode
-            if use_linemode == "papertitle":
-                paperinfo = self.fm.papermanager.get_paper_info(drawn.path)
-                if not paperinfo.title:
+            if use_linemode == "metatitle":
+                metadata = self.fm.metadata.get_metadata(drawn.path)
+                if not metadata.title:
                     use_linemode = "filename"
 
+            metakey = hash(repr(sorted(metadata.items()))) if metadata else 0
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                     drawn.path in copied, tagged_marker, drawn.infostring,
                     drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut,
-                    use_linemode)
+                    use_linemode, metakey)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
@@ -268,11 +269,12 @@ class BrowserColumn(Pager):
 
 
             # Deal with the line mode
-            if use_linemode == "papertitle":
-                if paperinfo.year:
-                    text = "%s - %s" % (paperinfo.year, paperinfo.title)
+            if use_linemode == "metatitle":
+                assert metadata.title, "Ensure that metadata.title is set!"
+                if metadata.year:
+                    text = "%s - %s" % (metadata.year, metadata.title)
                 else:
-                    text = paperinfo.title
+                    text = metadata.title
             if use_linemode == "filename":
                 text = drawn.drawn_basename
             elif use_linemode == "permissions":
@@ -311,9 +313,9 @@ class BrowserColumn(Pager):
             infostringlen = 0
             if use_linemode == "filename":
                 infostring = self._draw_infostring_display(drawn, space)
-            elif use_linemode == "papertitle":
-                if paperinfo and paperinfo.authors:
-                    authorstring = paperinfo.authors
+            elif use_linemode == "metatitle":
+                if metadata.authors:
+                    authorstring = metadata.authors
                     if ',' in authorstring:
                         authorstring = authorstring[0:authorstring.find(",")]
                     infostring.append([" " + authorstring + " ", ["infostring"]])