diff --git a/beets/importer/__init__.py b/beets/importer/__init__.py new file mode 100644 index 000000000..586b238e6 --- /dev/null +++ b/beets/importer/__init__.py @@ -0,0 +1,38 @@ +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Provides the basic, interface-agnostic workflow for importing and +autotagging music files. +""" + +from .session import ImportAbortError, ImportSession +from .tasks import ( + Action, + ArchiveImportTask, + ImportTask, + SentinelImportTask, + SingletonImportTask, +) + +# Note: Stages are not exposed to the public API + +__all__ = [ + "ImportSession", + "ImportAbortError", + "Action", + "ImportTask", + "ArchiveImportTask", + "SentinelImportTask", + "SingletonImportTask", +] diff --git a/beets/importer/session.py b/beets/importer/session.py index 620eb688e..e45644fa3 100644 --- a/beets/importer/session.py +++ b/beets/importer/session.py @@ -18,10 +18,10 @@ import time from typing import TYPE_CHECKING, Sequence from beets import config, dbcore, library, logging, plugins, util -from beets.importer.tasks import action +from beets.importer.tasks import Action from beets.util import displayable_path, normpath, pipeline, syspath -from .stages import * +from . import stages as stagefuncs from .state import ImportState if TYPE_CHECKING: @@ -162,15 +162,15 @@ class ImportSession: # Duplicate: log all three choices (skip, keep both, and trump). if task.should_remove_duplicates: self.tag_log("duplicate-replace", paths) - elif task.choice_flag in (action.ASIS, action.APPLY): + elif task.choice_flag in (Action.ASIS, Action.APPLY): self.tag_log("duplicate-keep", paths) - elif task.choice_flag is action.SKIP: + elif task.choice_flag is Action.SKIP: self.tag_log("duplicate-skip", paths) else: # Non-duplicate: log "skip" and "asis" choices. - if task.choice_flag is action.ASIS: + if task.choice_flag is Action.ASIS: self.tag_log("asis", paths) - elif task.choice_flag is action.SKIP: + elif task.choice_flag is Action.SKIP: self.tag_log("skip", paths) def should_resume(self, path: PathBytes): @@ -192,17 +192,17 @@ class ImportSession: # Set up the pipeline. if self.query is None: - stages = [read_tasks(self)] + stages = [stagefuncs.read_tasks(self)] else: - stages = [query_tasks(self)] + stages = [stagefuncs.query_tasks(self)] # In pretend mode, just log what would otherwise be imported. if self.config["pretend"]: - stages += [log_files(self)] + stages += [stagefuncs.log_files(self)] else: if self.config["group_albums"] and not self.config["singletons"]: # Split directory tasks into one task for each album. - stages += [group_albums(self)] + stages += [stagefuncs.group_albums(self)] # These stages either talk to the user to get a decision or, # in the case of a non-autotagged import, just choose to @@ -210,17 +210,20 @@ class ImportSession: # also add the music to the library database, so later # stages need to read and write data from there. if self.config["autotag"]: - stages += [lookup_candidates(self), user_query(self)] + stages += [ + stagefuncs.lookup_candidates(self), + stagefuncs.user_query(self), + ] else: - stages += [import_asis(self)] + stages += [stagefuncs.import_asis(self)] # Plugin stages. for stage_func in plugins.early_import_stages(): - stages.append(plugin_stage(self, stage_func)) + stages.append(stagefuncs.plugin_stage(self, stage_func)) for stage_func in plugins.import_stages(): - stages.append(plugin_stage(self, stage_func)) + stages.append(stagefuncs.plugin_stage(self, stage_func)) - stages += [manipulate_files(self)] + stages += [stagefuncs.manipulate_files(self)] pl = pipeline.Pipeline(stages) diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 52b2a221a..5b3540db4 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -22,7 +22,7 @@ from beets import config, plugins from beets.util import MoveOperation, displayable_path, pipeline from .tasks import ( - action, + Action, ImportTask, ImportTaskFactory, SentinelImportTask, @@ -173,7 +173,7 @@ def user_query(session: ImportSession, task: ImportTask): plugins.send("import_task_choice", session=session, task=task) # As-tracks: transition to singleton workflow. - if task.choice_flag is action.TRACKS: + if task.choice_flag is Action.TRACKS: # Set up a little pipeline for dealing with the singletons. def emitter(task): for item in task.items: @@ -186,7 +186,7 @@ def user_query(session: ImportSession, task: ImportTask): ) # As albums: group items by albums and create task for each album - if task.choice_flag is action.ALBUMS: + if task.choice_flag is Action.ALBUMS: return _extend_pipeline( [task], group_albums(session), @@ -194,7 +194,7 @@ def user_query(session: ImportSession, task: ImportTask): user_query(session), ) - resolve_duplicates(session, task) + _resolve_duplicates(session, task) if task.should_merge_duplicates: # Create a new task for tagging the current items @@ -216,7 +216,7 @@ def user_query(session: ImportSession, task: ImportTask): [merged_task], lookup_candidates(session), user_query(session) ) - apply_choice(session, task) + _apply_choice(session, task) return task @@ -231,8 +231,8 @@ def import_asis(session: ImportSession, task: ImportTask): return log.info("{}", displayable_path(task.paths)) - task.set_choice(action.ASIS) - apply_choice(session, task) + task.set_choice(Action.ASIS) + _apply_choice(session, task) @pipeline.mutator_stage @@ -312,7 +312,7 @@ def manipulate_files(session: ImportSession, task: ImportTask): # Private functions only used in the stages above -def apply_choice(session: ImportSession, task: ImportTask): +def _apply_choice(session: ImportSession, task: ImportTask): """Apply the task's choice to the Album or Item it contains and add it to the library. """ @@ -335,11 +335,11 @@ def apply_choice(session: ImportSession, task: ImportTask): task.set_fields(session.lib) -def resolve_duplicates(session: ImportSession, task: ImportTask): +def _resolve_duplicates(session: ImportSession, task: ImportTask): """Check if a task conflicts with items or albums already imported and ask the session to resolve this. """ - if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): + if task.choice_flag in (Action.ASIS, Action.APPLY, Action.RETAG): found_duplicates = task.find_duplicates(session.lib) if found_duplicates: log.debug( @@ -360,7 +360,7 @@ def resolve_duplicates(session: ImportSession, task: ImportTask): if duplicate_action == "s": # Skip new. - task.set_choice(action.SKIP) + task.set_choice(Action.SKIP) elif duplicate_action == "k": # Keep both. Do nothing; leave the choice intact. pass diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 2d3dc44e8..4ca5cf89f 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -31,7 +31,7 @@ from beets import autotag, config, dbcore, library, plugins, util from .state import ImportState if TYPE_CHECKING: - from .session import ImportSession + from .session import ImportSession, PathBytes # Global logger. log = logging.getLogger("beets") @@ -62,18 +62,26 @@ REIMPORT_FRESH_FIELDS_ITEM = list(REIMPORT_FRESH_FIELDS_ALBUM) log = logging.getLogger("beets") -action = Enum("action", ["SKIP", "ASIS", "TRACKS", "APPLY", "ALBUMS", "RETAG"]) -# The RETAG action represents "don't apply any match, but do record -# new metadata". It's not reachable via the standard command prompt but -# can be used by plugins. - - class ImportAbortError(Exception): """Raised when the user aborts the tagging operation.""" pass +class Action(Enum): + """Enumeration of possible actions for an import task.""" + + SKIP = "SKIP" + ASIS = "ASIS" + TRACKS = "TRACKS" + APPLY = "APPLY" + ALBUMS = "ALBUMS" + RETAG = "RETAG" + # The RETAG action represents "don't apply any match, but do record + # new metadata". It's not reachable via the standard command prompt but + # can be used by plugins. + + class BaseImportTask: """An abstract base class for importer tasks. @@ -143,7 +151,7 @@ class ImportTask(BaseImportTask): system. """ - choice_flag: action | None = None + choice_flag: Action | None = None match: autotag.AlbumMatch | autotag.TrackMatch | None = None # Keep track of the current task item @@ -165,7 +173,7 @@ class ImportTask(BaseImportTask): self.search_ids = [] # user-supplied candidate IDs. def set_choice( - self, choice: action | autotag.AlbumMatch | autotag.TrackMatch + self, choice: Action | autotag.AlbumMatch | autotag.TrackMatch ): """Given an AlbumMatch or TrackMatch object or an action constant, indicates that an action has been selected for this task. @@ -174,20 +182,20 @@ class ImportTask(BaseImportTask): use isinstance to check for them. """ # Not part of the task structure: - assert choice != action.APPLY # Only used internally. + assert choice != Action.APPLY # Only used internally. if choice in ( - action.SKIP, - action.ASIS, - action.TRACKS, - action.ALBUMS, - action.RETAG, + Action.SKIP, + Action.ASIS, + Action.TRACKS, + Action.ALBUMS, + Action.RETAG, ): # TODO: redesign to stricten the type self.choice_flag = choice # type: ignore[assignment] self.match = None else: - self.choice_flag = action.APPLY # Implicit choice. + self.choice_flag = Action.APPLY # Implicit choice. self.match = choice # type: ignore[assignment] def save_progress(self): @@ -205,11 +213,11 @@ class ImportTask(BaseImportTask): @property def apply(self): - return self.choice_flag == action.APPLY + return self.choice_flag == Action.APPLY @property def skip(self): - return self.choice_flag == action.SKIP + return self.choice_flag == Action.SKIP # Convenient data. @@ -219,10 +227,10 @@ class ImportTask(BaseImportTask): (in which case the data comes from the files' current metadata) or APPLY (in which case the data comes from the choice). """ - if self.choice_flag in (action.ASIS, action.RETAG): + if self.choice_flag in (Action.ASIS, Action.RETAG): likelies, consensus = autotag.current_metadata(self.items) return likelies - elif self.choice_flag is action.APPLY and self.match: + elif self.choice_flag is Action.APPLY and self.match: return self.match.info.copy() assert False @@ -232,9 +240,9 @@ class ImportTask(BaseImportTask): If the tasks applies an album match the method only returns the matched items. """ - if self.choice_flag in (action.ASIS, action.RETAG): + if self.choice_flag in (Action.ASIS, Action.RETAG): return list(self.items) - elif self.choice_flag == action.APPLY and isinstance( + elif self.choice_flag == Action.APPLY and isinstance( self.match, autotag.AlbumMatch ): return list(self.match.mapping.keys()) @@ -401,7 +409,7 @@ class ImportTask(BaseImportTask): """ changes = {} - if self.choice_flag == action.ASIS: + if self.choice_flag == Action.ASIS: # Taking metadata "as-is". Guess whether this album is VA. plur_albumartist, freq = util.plurality( [i.albumartist or i.artist for i in self.items] @@ -418,7 +426,7 @@ class ImportTask(BaseImportTask): changes["albumartist"] = config["va_name"].as_str() changes["comp"] = True - elif self.choice_flag in (action.APPLY, action.RETAG): + elif self.choice_flag in (Action.APPLY, Action.RETAG): # Applying autotagged metadata. Just get AA from the first # item. if not self.items[0].albumartist: @@ -473,7 +481,7 @@ class ImportTask(BaseImportTask): # old paths. item.move(operation) - if write and (self.apply or self.choice_flag == action.RETAG): + if write and (self.apply or self.choice_flag == Action.RETAG): item.try_write() with session.lib.transaction(): @@ -490,7 +498,7 @@ class ImportTask(BaseImportTask): self.remove_replaced(lib) self.album = lib.add_album(self.imported_items()) - if self.choice_flag == action.APPLY and isinstance( + if self.choice_flag == Action.APPLY and isinstance( self.match, autotag.AlbumMatch ): # Copy album flexible fields to the DB @@ -672,10 +680,10 @@ class SingletonImportTask(ImportTask): (in which case the data comes from the files' current metadata) or APPLY (in which case the data comes from the choice). """ - assert self.choice_flag in (action.ASIS, action.RETAG, action.APPLY) - if self.choice_flag in (action.ASIS, action.RETAG): + assert self.choice_flag in (Action.ASIS, Action.RETAG, Action.APPLY) + if self.choice_flag in (Action.ASIS, Action.RETAG): return dict(self.item) - elif self.choice_flag is action.APPLY: + elif self.choice_flag is Action.APPLY: return self.match.info.copy() def imported_items(self): diff --git a/beets/test/helper.py b/beets/test/helper.py index 85ea6bcf7..67ae1cfcf 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -658,9 +658,9 @@ class ImportSessionFixture(ImportSession): >>> lib = Library(':memory:') >>> importer = ImportSessionFixture(lib, paths=['/path/to/import']) - >>> importer.add_choice(importer.action.SKIP) - >>> importer.add_choice(importer.action.ASIS) - >>> importer.default_choice = importer.action.APPLY + >>> importer.add_choice(importer.Action.SKIP) + >>> importer.add_choice(importer.Action.ASIS) + >>> importer.default_choice = importer.Action.APPLY >>> importer.run() This imports ``/path/to/import`` into `lib`. It skips the first @@ -673,7 +673,7 @@ class ImportSessionFixture(ImportSession): self._choices = [] self._resolutions = [] - default_choice = importer.action.APPLY + default_choice = importer.Action.APPLY def add_choice(self, choice): self._choices.append(choice) @@ -687,7 +687,7 @@ class ImportSessionFixture(ImportSession): except IndexError: choice = self.default_choice - if choice == importer.action.APPLY: + if choice == importer.Action.APPLY: return task.candidates[0] elif isinstance(choice, int): return task.candidates[choice - 1] @@ -707,7 +707,7 @@ class ImportSessionFixture(ImportSession): res = self.default_resolution if res == self.Resolution.SKIP: - task.set_choice(importer.action.SKIP) + task.set_choice(importer.Action.SKIP) elif res == self.Resolution.REMOVE: task.should_remove_duplicates = True elif res == self.Resolution.MERGE: @@ -720,7 +720,7 @@ class TerminalImportSessionFixture(TerminalImportSession): super().__init__(*args, **kwargs) self._choices = [] - default_choice = importer.action.APPLY + default_choice = importer.Action.APPLY def add_choice(self, choice): self._choices.append(choice) @@ -742,15 +742,15 @@ class TerminalImportSessionFixture(TerminalImportSession): except IndexError: choice = self.default_choice - if choice == importer.action.APPLY: + if choice == importer.Action.APPLY: self.io.addinput("A") - elif choice == importer.action.ASIS: + elif choice == importer.Action.ASIS: self.io.addinput("U") - elif choice == importer.action.ALBUMS: + elif choice == importer.Action.ALBUMS: self.io.addinput("G") - elif choice == importer.action.TRACKS: + elif choice == importer.Action.TRACKS: self.io.addinput("T") - elif choice == importer.action.SKIP: + elif choice == importer.Action.SKIP: self.io.addinput("S") else: self.io.addinput("M") diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 12ea4c94d..7b7554546 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -811,12 +811,12 @@ def _summary_judgment(rec): if config["import"]["quiet"]: if rec == Recommendation.strong: - return importer.action.APPLY + return importer.Action.APPLY else: action = config["import"]["quiet_fallback"].as_choice( { - "skip": importer.action.SKIP, - "asis": importer.action.ASIS, + "skip": importer.Action.SKIP, + "asis": importer.Action.ASIS, } ) elif config["import"]["timid"]: @@ -824,17 +824,17 @@ def _summary_judgment(rec): elif rec == Recommendation.none: action = config["import"]["none_rec_action"].as_choice( { - "skip": importer.action.SKIP, - "asis": importer.action.ASIS, + "skip": importer.Action.SKIP, + "asis": importer.Action.ASIS, "ask": None, } ) else: return None - if action == importer.action.SKIP: + if action == importer.Action.SKIP: print_("Skipping.") - elif action == importer.action.ASIS: + elif action == importer.Action.ASIS: print_("Importing as-is.") return action @@ -1064,7 +1064,7 @@ class TerminalImportSession(importer.ImportSession): # Take immediate action if appropriate. action = _summary_judgment(task.rec) - if action == importer.action.APPLY: + if action == importer.Action.APPLY: match = task.candidates[0] show_change(task.cur_artist, task.cur_album, match) return match @@ -1074,7 +1074,7 @@ class TerminalImportSession(importer.ImportSession): # Loop until we have a choice. while True: # Ask for a choice from the user. The result of - # `choose_candidate` may be an `importer.action`, an + # `choose_candidate` may be an `importer.Action`, an # `AlbumMatch` object for a specific selection, or a # `PromptChoice`. choices = self._get_choices(task) @@ -1089,7 +1089,7 @@ class TerminalImportSession(importer.ImportSession): ) # Basic choices that require no more action here. - if choice in (importer.action.SKIP, importer.action.ASIS): + if choice in (importer.Action.SKIP, importer.Action.ASIS): # Pass selection to main control flow. return choice @@ -1097,7 +1097,7 @@ class TerminalImportSession(importer.ImportSession): # function. elif choice in choices: post_choice = choice.callback(self, task) - if isinstance(post_choice, importer.action): + if isinstance(post_choice, importer.Action): return post_choice elif isinstance(post_choice, autotag.Proposal): # Use the new candidates and continue around the loop. @@ -1121,7 +1121,7 @@ class TerminalImportSession(importer.ImportSession): # Take immediate action if appropriate. action = _summary_judgment(task.rec) - if action == importer.action.APPLY: + if action == importer.Action.APPLY: match = candidates[0] show_item_change(task.item, match) return match @@ -1135,12 +1135,12 @@ class TerminalImportSession(importer.ImportSession): candidates, True, rec, item=task.item, choices=choices ) - if choice in (importer.action.SKIP, importer.action.ASIS): + if choice in (importer.Action.SKIP, importer.Action.ASIS): return choice elif choice in choices: post_choice = choice.callback(self, task) - if isinstance(post_choice, importer.action): + if isinstance(post_choice, importer.Action): return post_choice elif isinstance(post_choice, autotag.Proposal): candidates = post_choice.candidates @@ -1203,7 +1203,7 @@ class TerminalImportSession(importer.ImportSession): if sel == "s": # Skip new. - task.set_choice(importer.action.SKIP) + task.set_choice(importer.Action.SKIP) elif sel == "k": # Keep both. Do nothing; leave the choice intact. pass @@ -1239,16 +1239,16 @@ class TerminalImportSession(importer.ImportSession): """ # Standard, built-in choices. choices = [ - PromptChoice("s", "Skip", lambda s, t: importer.action.SKIP), - PromptChoice("u", "Use as-is", lambda s, t: importer.action.ASIS), + PromptChoice("s", "Skip", lambda s, t: importer.Action.SKIP), + PromptChoice("u", "Use as-is", lambda s, t: importer.Action.ASIS), ] if task.is_album: choices += [ PromptChoice( - "t", "as Tracks", lambda s, t: importer.action.TRACKS + "t", "as Tracks", lambda s, t: importer.Action.TRACKS ), PromptChoice( - "g", "Group albums", lambda s, t: importer.action.ALBUMS + "g", "Group albums", lambda s, t: importer.Action.ALBUMS ), ] choices += [ diff --git a/beets/util/__init__.py b/beets/util/__init__.py index e17de1f51..845d967d6 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -68,6 +68,10 @@ BytesOrStr = Union[str, bytes] PathLike = Union[BytesOrStr, Path] Replacements: TypeAlias = "Sequence[tuple[Pattern[str], str]]" +# Here for now to allow for a easy replace later on +# once we can move to a PathLike (mainly used in importer) +PathBytes = bytes + class HumanReadableError(Exception): """An Exception that can include a human-readable error message to diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index f93f03d5e..0903ebabf 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -194,7 +194,7 @@ class BadFiles(BeetsPlugin): sel = ui.input_options(["aBort", "skip", "continue"]) if sel == "s": - return importer.action.SKIP + return importer.Action.SKIP elif sel == "c": return None elif sel == "b": diff --git a/beetsplug/edit.py b/beetsplug/edit.py index 51b36bdab..b92c48839 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -24,7 +24,7 @@ import yaml from beets import plugins, ui, util from beets.dbcore import types -from beets.importer import action +from beets.importer import Action from beets.ui.commands import PromptChoice, _do_query # These "safe" types can avoid the format/parse cycle that most fields go @@ -380,9 +380,9 @@ class EditPlugin(plugins.BeetsPlugin): # Save the new data. if success: - # Return action.RETAG, which makes the importer write the tags + # Return Action.RETAG, which makes the importer write the tags # to the files if needed without re-applying metadata. - return action.RETAG + return Action.RETAG else: # Edit cancelled / no edits made. Revert changes. for obj in task.items: diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a1bd26055..5451b4dbb 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -1306,12 +1306,12 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): ): # Album already has art (probably a re-import); skip it. return - if task.choice_flag == importer.action.ASIS: + if task.choice_flag == importer.Action.ASIS: # For as-is imports, don't search Web sources for art. local = True elif task.choice_flag in ( - importer.action.APPLY, - importer.action.RETAG, + importer.Action.APPLY, + importer.Action.RETAG, ): # Search everywhere for art. local = False diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index 35788ea05..d6357294d 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -15,7 +15,7 @@ """Warns you about things you hate (or even blocks import).""" -from beets.importer import action +from beets.importer import Action from beets.library import Album, Item, parse_query_string from beets.plugins import BeetsPlugin @@ -65,11 +65,11 @@ class IHatePlugin(BeetsPlugin): skip_queries = self.config["skip"].as_str_seq() warn_queries = self.config["warn"].as_str_seq() - if task.choice_flag == action.APPLY: + if task.choice_flag == Action.APPLY: if skip_queries or warn_queries: self._log.debug("processing your hate") if self.do_i_hate_this(task, skip_queries): - task.choice_flag = action.SKIP + task.choice_flag = Action.SKIP self._log.info("skipped: {0}", summary(task)) return if self.do_i_hate_this(task, warn_queries): diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 5d1244dec..7ee624ce7 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -19,7 +19,7 @@ import re import confuse from mediafile import MediaFile -from beets.importer import action +from beets.importer import Action from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, input_yn @@ -105,7 +105,7 @@ class ZeroPlugin(BeetsPlugin): self.fields_to_progs[field] = [] def import_task_choice_event(self, session, task): - if task.choice_flag == action.ASIS and not self.warned: + if task.choice_flag == Action.ASIS and not self.warned: self._log.warning('cannot zero in "as-is" mode') self.warned = True # TODO request write in as-is mode diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 0ebff3231..2d30f86c9 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -648,6 +648,6 @@ by the choices on the core importer prompt, and hence should not be used: ``a``, ``s``, ``u``, ``t``, ``g``, ``e``, ``i``, ``b``. Additionally, the callback function can optionally specify the next action to -be performed by returning a ``importer.action`` value. It may also return a +be performed by returning a ``importer.Action`` value. It may also return a ``autotag.Proposal`` value to update the set of current proposals to be considered. diff --git a/test/plugins/test_importadded.py b/test/plugins/test_importadded.py index 608afb399..5c26fdca4 100644 --- a/test/plugins/test_importadded.py +++ b/test/plugins/test_importadded.py @@ -59,7 +59,7 @@ class ImportAddedTest(PluginMixin, ImportTestCase): self.matcher = AutotagStub().install() self.matcher.matching = AutotagStub.IDENT self.importer = self.setup_importer() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) def tearDown(self): super().tearDown() diff --git a/test/test_importer.py b/test/test_importer.py index a28b646cf..34dea6df8 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -34,7 +34,7 @@ from mediafile import MediaFile from beets import config, importer, logging, util from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo -from beets.importer import albums_in_dir +from beets.importer.tasks import albums_in_dir from beets.test import _common from beets.test.helper import ( NEEDS_REFLINK, @@ -324,52 +324,52 @@ class ImportSingletonTest(ImportTestCase): def test_apply_asis_adds_track(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.items().get().title == "Tag Track 1" def test_apply_asis_does_not_add_album(self): assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get() is None def test_apply_asis_adds_singleton_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() self.assert_file_in_lib(b"singletons", b"Tag Track 1.mp3") def test_apply_candidate_adds_track(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "Applied Track 1" def test_apply_candidate_does_not_add_album(self): - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get() is None def test_apply_candidate_adds_singleton_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assert_file_in_lib(b"singletons", b"Applied Track 1.mp3") def test_skip_does_not_add_first_track(self): - self.importer.add_choice(importer.action.SKIP) + self.importer.add_choice(importer.Action.SKIP) self.importer.run() assert self.lib.items().get() is None def test_skip_adds_other_tracks(self): self.prepare_album_for_import(2) - self.importer.add_choice(importer.action.SKIP) - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.SKIP) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert len(self.lib.items()) == 1 @@ -385,8 +385,8 @@ class ImportSingletonTest(ImportTestCase): self.setup_importer() self.importer.paths = import_files - self.importer.add_choice(importer.action.ASIS) - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert len(self.lib.items()) == 2 @@ -406,7 +406,7 @@ class ImportSingletonTest(ImportTestCase): # As-is item import. assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() for item in self.lib.items(): @@ -421,7 +421,7 @@ class ImportSingletonTest(ImportTestCase): # Autotagged. assert self.lib.albums().get() is None self.importer.clear_choices() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() for item in self.lib.items(): @@ -449,41 +449,41 @@ class ImportTest(ImportTestCase): def test_apply_asis_adds_album(self): assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().album == "Tag Album" def test_apply_asis_adds_tracks(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.items().get().title == "Tag Track 1" def test_apply_asis_adds_album_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() self.assert_file_in_lib(b"Tag Artist", b"Tag Album", b"Tag Track 1.mp3") def test_apply_candidate_adds_album(self): assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get().album == "Applied Album" def test_apply_candidate_adds_tracks(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "Applied Track 1" def test_apply_candidate_adds_album_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assert_file_in_lib( b"Applied Artist", b"Applied Album", b"Applied Track 1.mp3" @@ -496,14 +496,14 @@ class ImportTest(ImportTestCase): mediafile.genre = "Tag Genre" mediafile.save() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().genre == "" def test_apply_from_scratch_keeps_format(self): config["import"]["from_scratch"] = True - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().format == "MP3" @@ -511,7 +511,7 @@ class ImportTest(ImportTestCase): config["import"]["from_scratch"] = True bitrate = 80000 - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().bitrate == bitrate @@ -521,7 +521,7 @@ class ImportTest(ImportTestCase): import_file = os.path.join(self.import_dir, b"album", b"track_1.mp3") self.assertExists(import_file) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assertNotExists(import_file) @@ -531,26 +531,26 @@ class ImportTest(ImportTestCase): import_file = os.path.join(self.import_dir, b"album", b"track_1.mp3") self.assertExists(import_file) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assertNotExists(import_file) def test_skip_does_not_add_track(self): - self.importer.add_choice(importer.action.SKIP) + self.importer.add_choice(importer.Action.SKIP) self.importer.run() assert self.lib.items().get() is None def test_skip_non_album_dirs(self): self.assertIsDir(os.path.join(self.import_dir, b"album")) self.touch(b"cruft", dir=self.import_dir) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert len(self.lib.albums()) == 1 def test_unmatched_tracks_not_added(self): self.prepare_album_for_import(2) self.matcher.matching = self.matcher.MISSING - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert len(self.lib.items()) == 1 @@ -577,7 +577,7 @@ class ImportTest(ImportTestCase): def test_asis_no_data_source(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() with pytest.raises(AttributeError): @@ -599,7 +599,7 @@ class ImportTest(ImportTestCase): # As-is album import. assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() for album in self.lib.albums(): @@ -621,7 +621,7 @@ class ImportTest(ImportTestCase): # Autotagged. assert self.lib.albums().get() is None self.importer.clear_choices() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() for album in self.lib.albums(): @@ -656,9 +656,9 @@ class ImportTracksTest(ImportTestCase): assert self.lib.items().get() is None assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.TRACKS) - self.importer.add_choice(importer.action.APPLY) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.TRACKS) + self.importer.add_choice(importer.Action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "Applied Track 1" assert self.lib.albums().get() is None @@ -666,9 +666,9 @@ class ImportTracksTest(ImportTestCase): def test_apply_tracks_adds_singleton_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.TRACKS) - self.importer.add_choice(importer.action.APPLY) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.TRACKS) + self.importer.add_choice(importer.Action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assert_file_in_lib(b"singletons", b"Applied Track 1.mp3") @@ -687,7 +687,7 @@ class ImportCompilationTest(ImportTestCase): self.matcher.restore() def test_asis_homogenous_sets_albumartist(self): - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Tag Artist" for item in self.lib.items(): @@ -699,7 +699,7 @@ class ImportCompilationTest(ImportTestCase): self.import_media[1].artist = "Another Artist" self.import_media[1].save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Various Artists" for item in self.lib.items(): @@ -711,7 +711,7 @@ class ImportCompilationTest(ImportTestCase): self.import_media[1].artist = "Another Artist" self.import_media[1].save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() for item in self.lib.items(): assert item.comp @@ -722,7 +722,7 @@ class ImportCompilationTest(ImportTestCase): self.import_media[1].artist = "Other Artist" self.import_media[1].save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Other Artist" for item in self.lib.items(): @@ -736,7 +736,7 @@ class ImportCompilationTest(ImportTestCase): mediafile.mb_albumartistid = "Album Artist ID" mediafile.save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Album Artist" assert self.lib.albums().get().mb_albumartistid == "Album Artist ID" @@ -755,7 +755,7 @@ class ImportCompilationTest(ImportTestCase): mediafile.mb_albumartistid = "Album Artist ID" mediafile.save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Album Artist" assert self.lib.albums().get().albumartists == [ @@ -802,7 +802,7 @@ class ImportExistingTest(ImportTestCase): self.importer.run() assert len(self.lib.items()) == 1 - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() assert len(self.lib.items()) == 1 @@ -810,18 +810,18 @@ class ImportExistingTest(ImportTestCase): self.importer.run() assert len(self.lib.albums()) == 1 - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() assert len(self.lib.albums()) == 1 def test_does_not_duplicate_singleton_track(self): - self.importer.add_choice(importer.action.TRACKS) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.TRACKS) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert len(self.lib.items()) == 1 - self.reimporter.add_choice(importer.action.TRACKS) - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.TRACKS) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() assert len(self.lib.items()) == 1 @@ -831,7 +831,7 @@ class ImportExistingTest(ImportTestCase): medium.title = "New Title" medium.save() - self.reimporter.add_choice(importer.action.ASIS) + self.reimporter.add_choice(importer.Action.ASIS) self.reimporter.run() assert self.lib.items().get().title == "New Title" @@ -846,7 +846,7 @@ class ImportExistingTest(ImportTestCase): ) self.assert_file_in_lib(old_path) - self.reimporter.add_choice(importer.action.ASIS) + self.reimporter.add_choice(importer.Action.ASIS) self.reimporter.run() self.assert_file_in_lib( b"Applied Artist", b"Applied Album", b"New Title.mp3" @@ -865,7 +865,7 @@ class ImportExistingTest(ImportTestCase): self.assert_file_in_lib(old_path) config["import"]["copy"] = False - self.reimporter.add_choice(importer.action.ASIS) + self.reimporter.add_choice(importer.Action.ASIS) self.reimporter.run() self.assert_file_not_in_lib( b"Applied Artist", b"Applied Album", b"New Title.mp3" @@ -880,7 +880,7 @@ class ImportExistingTest(ImportTestCase): ) self.reimporter = self.setup_importer() - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() new_path = os.path.join( b"Applied Artist", b"Applied Album", b"Applied Track 1.mp3" @@ -899,7 +899,7 @@ class ImportExistingTest(ImportTestCase): ) self.reimporter = self.setup_importer(move=True) - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() self.assertNotExists(self.import_media[0].path) @@ -913,9 +913,9 @@ class GroupAlbumsImportTest(ImportTestCase): self.setup_importer() # Split tracks into two albums and use both as-is - self.importer.add_choice(importer.action.ALBUMS) - self.importer.add_choice(importer.action.ASIS) - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ALBUMS) + self.importer.add_choice(importer.Action.ASIS) + self.importer.add_choice(importer.Action.ASIS) def tearDown(self): super().tearDown() @@ -972,7 +972,7 @@ class GlobalGroupAlbumsImportTest(GroupAlbumsImportTest): def setUp(self): super().setUp() self.importer.clear_choices() - self.importer.default_choice = importer.action.ASIS + self.importer.default_choice = importer.Action.ASIS config["import"]["group_albums"] = True @@ -1019,7 +1019,7 @@ class InferAlbumDataTest(BeetsTestCase): ) def test_asis_homogenous_single_artist(self): - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() assert not self.items[0].comp assert self.items[0].albumartist == self.items[2].artist @@ -1027,7 +1027,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_asis_heterogenous_va(self): self.items[0].artist = "another artist" self.items[1].artist = "some other artist" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1037,7 +1037,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_asis_comp_applied_to_all_items(self): self.items[0].artist = "another artist" self.items[1].artist = "some other artist" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1047,7 +1047,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_asis_majority_artist_single_artist(self): self.items[0].artist = "another artist" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1060,7 +1060,7 @@ class InferAlbumDataTest(BeetsTestCase): for item in self.items: item.albumartist = "some album artist" item.mb_albumartistid = "some album artist id" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1089,7 +1089,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_small_single_artist_album(self): self.items = [self.items[0]] self.task.items = self.items - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() assert not self.items[0].comp @@ -1599,7 +1599,7 @@ class ReimportTest(ImportTestCase): def _setup_session(self, singletons=False): self.setup_importer(import_dir=self.libdir, singletons=singletons) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) def _album(self): return self.lib.albums().get() @@ -1845,7 +1845,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): search_ids=[self.MB_RELEASE_PREFIX + self.ID_RELEASE_0] ) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get().album == "VALID_RELEASE_0" @@ -1858,7 +1858,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): ) self.importer.add_choice(2) # Pick the 2nd best match (release 1). - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get().album == "VALID_RELEASE_1" @@ -1867,7 +1867,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): search_ids=[self.MB_RECORDING_PREFIX + self.ID_RECORDING_0] ) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "VALID_RECORDING_0" @@ -1880,7 +1880,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): ) self.importer.add_choice(2) # Pick the 2nd best match (recording 1). - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "VALID_RECORDING_1" diff --git a/test/test_plugins.py b/test/test_plugins.py index d273de698..25f1f3c66 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -24,10 +24,10 @@ from mediafile import MediaFile from beets import config, plugins, ui from beets.dbcore import types from beets.importer import ( + Action, ArchiveImportTask, SentinelImportTask, SingletonImportTask, - action, ) from beets.library import Item from beets.plugins import MetadataSourcePlugin @@ -389,7 +389,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "aBort", ) + ("Foo", "baR") - self.importer.add_choice(action.SKIP) + self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_once_with( opts, default="a", require=ANY @@ -424,7 +424,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): ) + ("Foo", "baR") config["import"]["singletons"] = True - self.importer.add_choice(action.SKIP) + self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_with( opts, default="a", require=ANY @@ -461,7 +461,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "enter Id", "aBort", ) + ("baZ",) - self.importer.add_choice(action.SKIP) + self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_once_with( opts, default="a", require=ANY @@ -523,7 +523,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): return [ui.commands.PromptChoice("f", "Foo", self.foo)] def foo(self, session, task): - return action.SKIP + return Action.SKIP self.register_plugin(DummyPlugin) # Default options + extra choices by the plugin ('Foo', 'Bar')