def test_get_training_attribute(self): cfg = { "trainings": { "fallback": { "query": { "bpm": "120..", }, "target": "MPD1", }, "10K": { "query": { "bpm": "180..", "length": "60..240", }, "use_flavours": ["f1", "f2"], } } } config = get_plugin_configuration(cfg) training = config["trainings"]["10K"] # Direct self.assertEqual(cfg["trainings"]["10K"]["query"], common.get_training_attribute(training, "query")) self.assertEqual( cfg["trainings"]["10K"]["use_flavours"], common.get_training_attribute(training, "use_flavours")) # Fallback self.assertEqual(cfg["trainings"]["fallback"]["target"], common.get_training_attribute(training, "target")) # Inexistent self.assertIsNone(common.get_training_attribute(training, "hoppa"))
def list_training_attributes(self, training_name: str): if not self.config["trainings"].exists() or not \ self.config["trainings"][training_name].exists(): self._say("Training[{0}] does not exist.".format(training_name), is_error=True) return # Just output the list of the names of the available trainings if not self.verbose_log: self._say("{0}".format(training_name), log_only=False) return display_name = "[ {} ]".format(training_name) self._say("\n{0}".format(display_name.center(80, "=")), log_only=False) training: Subview = self.config["trainings"][training_name] training_keys = list( set(common.MUST_HAVE_TRAINING_KEYS) | set(training.keys())) final_keys = [ "duration", "query", "use_flavours", "combined_query", "ordering", "target" ] final_keys.extend(tk for tk in training_keys if tk not in final_keys) for key in final_keys: val = common.get_training_attribute(training, key) # Handle non-existent (made up) keys if key == "combined_query" and common.get_training_attribute( training, "use_flavours"): val = self._gather_query_elements(training) if val is None: continue if key == "duration": val = common.get_human_readable_time(val * 60) elif key == "ordering": val = dict(val) elif key == "query": pass if isinstance(val, dict): value = [] for k in val: value.append("{key}({val})".format(key=k, val=val[k])) val = ", ".join(value) self._say("{0}: {1}".format(key, val), log_only=False)
def get_items_for_duration(training: Subview, items, duration): """Returns the items picked by the Picker strategy specified bu the `pick_strategy` key """ picker = common.get_training_attribute(training, "pick_strategy") if not picker or picker not in pickers: picker = default_picker picker_info = pickers[picker] instance: BasePicker = common.get_class_instance(picker_info["module"], picker_info["class"]) instance.setup(training, items, duration) return instance.get_picked_items()
def _gather_query_elements(self, training: Subview): """Sum all query elements into one big list ordered from strongest to weakest: command -> training -> flavours """ command_query = self.query training_query = [] flavour_query = [] # Append the query elements from the configuration tconf = common.get_training_attribute(training, "query") if tconf: for key in tconf.keys(): nqe = common.get_normalized_query_element(key, tconf.get(key)) if type(nqe) == list: training_query.extend(nqe) else: training_query.append(nqe) # Append the query elements from the flavours defined on the training flavours = common.get_training_attribute(training, "use_flavours") if flavours: flavours = [flavours] if type(flavours) == str else flavours for flavour_name in flavours: flavour: Subview = self.config["flavours"][flavour_name] flavour_query += common.get_flavour_elements(flavour) self._say("Command query elements: {}".format(command_query), log_only=True) self._say("Training query elements: {}".format(training_query), log_only=True) self._say("Flavour query elements: {}".format(flavour_query), log_only=True) raw_combined_query = command_query + training_query + flavour_query self._say("Combined query elements: {}".format(raw_combined_query), log_only=True) return raw_combined_query
def get_ordered_items(training: Subview, items): """Returns the items ordered by the strategy specified in the `ordering_strategy` key """ strategy = common.get_training_attribute(training, "ordering_strategy") if not strategy or strategy not in permutations: strategy = default_strategy perm = permutations[strategy] instance: BasePermutation = common.get_class_instance( perm["module"], perm["class"]) instance.setup(training, items) return instance.get_ordered_items()
def _clean_target(self): training_name = self._get_cleaned_training_name() target_name = common.get_training_attribute(self.training, "target") clean_target = common.get_target_attribute_for_training( self.training, "clean_target") if clean_target is False: return dst_path = Path(common.get_destination_path_for_training( self.training)) # Clean entire target dst_sub_dir = dst_path # Only clean specific training folder if clean_target == "training": dst_sub_dir = dst_path.joinpath(training_name) common.say("Cleaning target[{0}]: {1}".format(target_name, dst_sub_dir)) if os.path.isdir(dst_sub_dir): shutil.rmtree(dst_sub_dir) os.mkdir(dst_sub_dir) # Clean additional files additional_files = common.get_target_attribute_for_training( self.training, "delete_from_device") if additional_files and len(additional_files) > 0: root = common.get_target_attribute_for_training( self.training, "device_root") root = Path(root).expanduser() common.say( "Deleting additional files: {0}".format(additional_files)) for path in additional_files: path = Path(str.strip(path, "/")) dst_path = os.path.realpath(root.joinpath(path)) if not os.path.isfile(dst_path): common.say( "The file to delete does not exist: {0}".format(path), log_only=True) continue common.say("Deleting: {}".format(dst_path)) if not self.cfg_dry_run: os.remove(dst_path)
def _build_order_info(self): cfg_ordering = common.get_training_attribute(self.training, "ordering") fields = [] if cfg_ordering: fields = list(cfg_ordering.keys()) default_field_data = { "min": 99999999.9, "max": 0.0, "delta": 0.0, "step": 0.0, "weight": 100 } # Build Order Info dictionary self.order_info = {} for field in fields: field_name = field.strip() self.order_info[field_name] = default_field_data.copy() self.order_info[field_name]["weight"] = cfg_ordering[field] # Populate Order Info for field_name in self.order_info.keys(): field_info = self.order_info[field_name] _min, _max, _sum, _avg = common.get_min_max_sum_avg_for_items( self.items, field_name) field_info["min"] = _min field_info["max"] = _max # Calculate other values in Order Info for field_name in self.order_info.keys(): field_info = self.order_info[field_name] field_info["delta"] = round(field_info["max"] - field_info["min"], 6) if field_info["delta"] > 0: field_info["step"] = round(100 / field_info["delta"], 6) else: field_info["step"] = 0 common.say("ORDER INFO: {0}".format(self.order_info))
def _generate_playist(self): training_name = self._get_cleaned_training_name() playlist_name = self._get_training_name() target_name = common.get_training_attribute(self.training, "target") if not common.get_target_attribute_for_training( self.training, "generate_playlist"): common.say("Playlist generation to target[{0}] was skipped " "(generate_playlist=no).".format(target_name), log_only=False) return dst_path = Path(common.get_destination_path_for_training( self.training)) dst_sub_dir = dst_path.joinpath(training_name) playlist_filename = "{}.m3u".format(playlist_name) dst = dst_sub_dir.joinpath(playlist_filename) lines = [ "# Playlist generated for training '{}' on {}". \ format(training_name, datetime.now()) ] for item in self.items: path = util.displayable_path( item.get("exportpath", item.get("path"))) if path: path = util.syspath(path) line = "{path}".format(path=path) lines.append(line) with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as ntf: tmp_playlist = ntf.name for line in lines: ntf.write("{}\n".format(line).encode("utf-8")) common.say("Created playlist: {0}".format(dst), log_only=True) util.copy(tmp_playlist, dst) util.remove(tmp_playlist)
def handle_training(self): training_name = self.query.pop(0) training: Subview = self.config["trainings"][training_name] self._say("Handling training: {0}".format(training_name), log_only=False) # todo: create a sanity checker for training to check all attributes if not training.exists(): self._say("There is no training with this name[{0}]!".format( training_name), log_only=False) return # Verify target device path path if not common.get_destination_path_for_training(training): self._say("Invalid target!", log_only=False) return # Get the library items lib_items: Results = self._retrieve_library_items(training) # Show count only if self.cfg_count: self._say("Number of songs available: {}".format(len(lib_items)), log_only=False) return # Check count if len(lib_items) < 1: self._say("No songs in your library match this training!", log_only=False) return duration = common.get_training_attribute(training, "duration") if not duration: self._say("There is no duration set for the selected training!", log_only=False) return # 1) order items by `ordering_strategy` sorted_items = itemorder.get_ordered_items(training, lib_items) # flds = ["ordering_score", "artist", "title"] # self.display_library_items(sorted_items, flds, prefix="SORTED: ") # 2) select items that cover the training duration itempick.favour_unplayed = \ common.get_training_attribute(training, "favour_unplayed") sel_items = itempick.get_items_for_duration(training, sorted_items, duration * 60) # 3) Show some info total_time = common.get_duration_of_items(sel_items) self._say("Available songs: {}".format(len(lib_items))) self._say("Selected songs: {}".format(len(sel_items))) self._say("Planned training duration: {0}".format( common.get_human_readable_time(duration * 60))) self._say("Total song duration: {}".format( common.get_human_readable_time(total_time))) # 4) Show the selected songs flds = self._get_training_query_element_keys(training) flds += ["play_count", "artist", "title"] self.display_library_items(sel_items, flds, prefix="Selected: ") # 5) Clean, Copy, Playlist, Run itemexport.generate_output(training, sel_items, self.cfg_dry_run) self._say("Run!", log_only=False)
def _copy_items(self): training_name = self._get_cleaned_training_name() target_name = common.get_training_attribute(self.training, "target") # The copy_files is only False when it is explicitly declared so copy_files = common.get_target_attribute_for_training( self.training, "copy_files") copy_files = False if copy_files == False else True if not copy_files: common.say( "Copying to target[{0}] was skipped (copy_files=no).".format( target_name)) return increment_play_count = common.get_training_attribute( self.training, "increment_play_count") dst_path = Path(common.get_destination_path_for_training( self.training)) dst_sub_dir = dst_path.joinpath(training_name) if not os.path.isdir(dst_sub_dir): os.mkdir(dst_sub_dir) common.say("Copying to target[{0}]: {1}".format( target_name, dst_sub_dir)) cnt = 0 # todo: disable alive bar when running in verbose mode # from beets import logging as beetslogging # beets_log = beetslogging.getLogger("beets") # print(beets_log.getEffectiveLevel()) with alive_bar(len(self.items)) as bar: for item in self.items: src = util.displayable_path(item.get("path")) if not os.path.isfile(src): # todo: this is bad enough to interrupt! create option # for this common.say("File does not exist: {}".format(src)) continue fn, ext = os.path.splitext(src) gen_filename = "{0}_{1}{2}" \ .format(str(cnt).zfill(6), common.get_random_string(), ext) dst = dst_sub_dir.joinpath(gen_filename) # dst = "{0}/{1}".format(dst_path, gen_filename) common.say("Copying[{1}]: {0}".format(src, gen_filename)) if not self.cfg_dry_run: util.copy(src, dst) # store the file_name for the playlist item["exportpath"] = util.bytestring_path(gen_filename) if increment_play_count: common.increment_play_count_on_item(item) cnt += 1 bar()