diff options
author | hut <hut@lepus.uberspace.de> | 2015-01-17 03:43:56 +0100 |
---|---|---|
committer | hut <hut@lepus.uberspace.de> | 2015-01-17 03:43:56 +0100 |
commit | e0fc3b1966d39379b82fdb4dd1c068e81255141e (patch) | |
tree | 48b6be36dfbdc5a8a268bfe0324724ff0aedbed1 /ranger | |
parent | 442ca1f79e0dd6606154cd95c308fada660d23a6 (diff) | |
parent | feb6b88fbca743d824a7a452f87a0ac1ea69b148 (diff) | |
download | ranger-e0fc3b1966d39379b82fdb4dd1c068e81255141e.tar.gz |
Merge branch 'metadata'
Diffstat (limited to 'ranger')
-rw-r--r-- | ranger/config/commands.py | 86 | ||||
-rw-r--r-- | ranger/config/rc.conf | 10 | ||||
-rw-r--r-- | ranger/container/fsobject.py | 2 | ||||
-rw-r--r-- | ranger/container/settings.py | 2 | ||||
-rw-r--r-- | ranger/core/actions.py | 8 | ||||
-rw-r--r-- | ranger/core/fm.py | 8 | ||||
-rw-r--r-- | ranger/core/metadata.py | 143 | ||||
-rw-r--r-- | ranger/ext/openstruct.py | 15 | ||||
-rw-r--r-- | ranger/ext/papermanager.py | 171 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 26 |
10 files changed, 215 insertions, 256 deletions
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"]]) |