diff --git a/beetsplug/importhistory.py b/beetsplug/importhistory.py new file mode 100644 index 0000000000..0497f4d4e1 --- /dev/null +++ b/beetsplug/importhistory.py @@ -0,0 +1,157 @@ +"""Adds a `source_path` attribute to imported albums indicating from what path +the album was imported from. Also suggests removing that source path in case +you've removed the album from the library. + +""" + +import os +from shutil import rmtree + +from beets import library +from beets.plugins import BeetsPlugin +from beets.ui import colorize as colorize_text +from beets.ui import input_options +from beets.util import syspath, displayable_path + + +class ImportHistPlugin(BeetsPlugin): + """Main plugin class.""" + + def __init__(self): + """Initialize the plugin and read configuration.""" + super(ImportHistPlugin, self).__init__() + self.config.add( + { + "suggest_removal": False, + } + ) + self.import_stages = [self.import_stage] + self.register_listener("item_removed", self.suggest_removal) + # In order to stop future removal suggestions for an album we keep + # track of `mb_albumid`s in this set. + self.stop_suggestions_for_albums = set() + # During reimports (import --library) both the import_task_choice and + # the item_removed event are triggered. The item_removed event is + # triggered first. For the import_task_choice event we prevent removal + # suggestions using the existing stop_suggestions_for_album mechanism. + self.register_listener( + "import_task_choice", self.prevent_suggest_removal + ) + + def prevent_suggest_removal(self, session, task): + for item in task.imported_items(): + if "mb_albumid" in item: + self.stop_suggestions_for_albums.add(item.mb_albumid) + + def import_stage(self, _, task): + """Event handler for albums import finished.""" + for item in task.imported_items(): + # During reimports (import --library), we prevent overwriting the + # source_path attribute with the path from the music library + if "source_path" in item: + self._log.info( + "Preserving source_path of reimported item {}", item.id + ) + continue + item["source_path"] = item.path + item.try_sync(write=True, move=False) + + def suggest_removal(self, item): + """Prompts the user to delete the original path the item was imported from.""" + if not self.config["suggest_removal"]: + return + if "source_path" not in item: + self._log.warning( + "Item without source_path (probably imported before plugin " + "usage): {}", + displayable_path(item.path), + ) + return + if item.mb_albumid in self.stop_suggestions_for_albums: + return + if not os.path.isfile(syspath(item.source_path)): + self._log.warning( + "Item with source_path that doesn't exist: {}", + displayable_path(item.source_path), + ) + return + source_dir = os.path.dirname(syspath(item.source_path)) + if not ( + os.access(syspath(item.source_path), os.W_OK) + and os.access(source_dir, os.W_OK | os.X_OK) + ): + self._log.warning( + "Item with source_path not deletable: {}", + displayable_path(item.source_path), + ) + return + # We ask the user whether they'd like to delete the item's source + # directory + print( + "The item:\n{path}\nis originated from:\n{source}\n" + "What would you like to do?".format( + path=colorize_text("text_warning", displayable_path(item.path)), + source=colorize_text( + "text_warning", displayable_path(item.source_path) + ), + ) + ) + resp = input_options( + [ + "Delete the item's source", + "Recursively delete the source's directory", + "do Nothing", + "do nothing and Stop suggesting to delete items from this album", + ], + require=True, + ) + if resp == "d": + self._log.info( + "Deleting the item's source file: {}", + displayable_path(item.source_path) + ) + os.remove(syspath(item.source_path)) + elif resp == "r": + self._log.info( + "Searching for other items with a source_path attr containing: {}", + source_dir, + ) + source_dir_query = library.PathQuery( + "source_path", + source_dir, + # The "source_path" attribute may not be present in all + # items of the library, so we avoid errors with this: + fast=False, + ) + print("Doing so will delete the following items' sources as well:") + for searched_item in item._db.items(source_dir_query): + print( + colorize_text( + "text_warning", + displayable_path(searched_item["path"]) + ) + ) + print("Would you like to continue?") + continue_resp = input_options( + ["Yes", "delete None", "delete just the File"], + require=False, # Yes is the a default + ) + if continue_resp == "y": + self._log.info( + "Deleting the item's source directory: {}", + displayable_path(source_dir) + ) + rmtree(source_dir) + elif continue_resp == "n": + self._log.info("doing nothing - aborting hook function") + return + elif continue_resp == "f": + self._log.info( + "removing just the item's original source: {}", + displayable_path(item.source_path), + ) + os.remove(item.source_path) + elif resp == "s": + self.stop_suggestions_for_albums.add(item.mb_albumid) + else: + self._log.info("Doing nothing") diff --git a/docs/changelog.rst b/docs/changelog.rst index 46fa3b64e1..61a34aa624 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -103,6 +103,7 @@ Other changes: New features: +* :doc:`plugins/importhistory`: Added plugin * New template function added: ``%capitalize``. Converts the first letter of the text to uppercase and the rest to lowercase. * Ability to query albums with track db fields and vice-versa, for example diff --git a/docs/plugins/importhistory.rst b/docs/plugins/importhistory.rst new file mode 100644 index 0000000000..01067565a1 --- /dev/null +++ b/docs/plugins/importhistory.rst @@ -0,0 +1,76 @@ +ImportHistory Plugin +==================== + +The ``importhistory`` plugin adds a `source_path` field to every item imported +to the library which stores the original media files' paths. Using this plugin +makes most sense when the general importing workflow is to use ``beet import +--copy``. + +Another feature of the plugin is suggesting to delete those original source +files as well whenever items are removed from the Beets library. + +To use the ``importhistory`` plugin, enable it in your configuration (see +:ref:`using-plugins`). + +`source_path` Usage +------------------- + +The first use case of the `source_path` field is in the following scenario: You +imported all of the directories in your current `$PWD`:: + + beet import --flat --copy */ + +Then, something went wrong, and you need to rerun this command. But, you don't +want to tell beets to read again the already successfully imported directories +again. So, you can view which files were successfully imported, using:: + + beet ls source_path:$PWD --format='$source_path' + +You can of course pipe this command to other standard UNIX utilities:: + + # The following prints the directories without the l + beet ls source_path:$PWD --format='$source_path' | \ + sed "s#$(dirname $PWD)/\([^/]*\)/.*#\1#" | \ + sort -u + +The above will print only the directories you successfully finished importing +with `beet import --flat --copy */`. + +Removal Suggestion Usage +------------------------ + +A second use case of the plugin is described in the following scenario: Imagine +you imported an album using:: + + beet import --copy --flat ~/Desktop/interesting-album-to-check/ + +Then you listened to that album and decided it wasn't good and you want to +delete it from your library, and from your `~/Desktop`, so you run:: + + beet remove --delete source_path:$HOME/Desktop/interesting-album-to-check + +After you'll approve the deletion, this plugin will ask you:: + + The item: + /Interesting Album/01 Interesting Song.flac + is originated from: + /Desktop/interesting-album-to-check/01-interesting-song.flac + What would you like to do? + Delete the item's source, Recursively delete the source's directory, + do Nothing, + do nothing and Stop suggesting to delete items from this album? + +Thus the plugin helps you delete the files from the beets library and from +their source as one. + +Configuration +------------- + +To configure the plugin, make an ``importhistory:`` section in your +configuration file. There is one option available: + +- **suggest_removal**: By default ``importhistory`` suggests to remove the + original directories / files from which the items were imported whenever + library items (and files) are removed. To disable these prompts set this + option to ``no``. + Default: ``yes``. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 6705344c9d..98bd643356 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -93,6 +93,7 @@ following to your configuration:: ihate importadded importfeeds + importhistory info inline ipfs