def _make_initial_selection(self): # Create an initial selection sel_time = 0 curr_bin = 0 max_bin = len(self.bin_boundaries) - 1 curr_run = 0 max_run = 100 while (self.duration - sel_time) > self.max_allowed_time_difference: curr_run += 1 if curr_run > max_run: common.say("MAX HIT!") break low, high = self._get_bin_boundaries(curr_bin) index = self._get_random_item_between_boundaries(low, high) if index is not None: item: Item = self.items[index] item_len = item.get("length") time_diff = abs(sel_time - self.duration) new_diff = abs((sel_time + item_len) - self.duration) if new_diff < time_diff: sel_data = { "index": index, "bin": curr_bin, "length": item_len, "play_count": item.get("play_count", 0) } self.selection.append(sel_data) sel_time += item_len curr_bin = curr_bin + 1 \ if curr_bin < max_bin else max_bin common.say("{} INITIAL SELECTION: FINISHED".format("=" * 60)) self.show_selection_status()
def display_library_items(self, items, fields, prefix=""): fmt = prefix for field in fields: if field in ["artist", "album", "title"]: fmt += "- {{{0}}} ".format(field) else: fmt += "[{0}: {{{0}}}] ".format(field) common.say("{}".format("=" * 120), log_only=False) for item in items: kwargs = {} for field in fields: fld_val = None if field == "play_count" and not hasattr(item, field): item[field] = 0 if hasattr(item, field): fld_val = item[field] if type(fld_val) in [float]: fld_val = round(fld_val, 3) fld_val = "{:7.3f}".format(fld_val) if type(fld_val) in [int]: fld_val = round(fld_val, 1) fld_val = "{:7.1f}".format(fld_val) kwargs[field] = fld_val try: self._say(fmt.format(**kwargs), log_only=False) except IndexError: pass common.say("{}".format("=" * 120), log_only=False)
def func(self, lib: Library, options, arguments): self.cfg_quiet = options.quiet self.cfg_count = options.count self.cfg_dry_run = options.dry_run self.lib = lib self.query = decargs(arguments) # Determine if -v option was set for more verbose logging logger = logging.getLogger('beets') self.verbose_log = True if logger.level == logging.DEBUG else False # TEMPORARY: Verify configuration upgrade! # There is a major backward incompatible upgrade in version 1.1.1 try: self.verify_configuration_upgrade() except RuntimeError as e: self._say("*" * 80) self._say( "******************** INCOMPATIBLE PLUGIN CONFIGURATION " "*********************") self._say("*" * 80) self._say( "* Your configuration has been created for an older version " "of the plugin.") self._say( "* Since version 1.1.1 the plugin has implemented changes " "that require your " "current configuration to be updated.") self._say( "* Please read the updated documentation here and update your " "configuration.") self._say( "* Documentation: " "https://github.com/adamjakab/BeetsPluginGoingRunning/blob" "/master/README.md" "#configuration") self._say("* I promise it will not happen again ;)") self._say("* " + str(e)) self._say("* The plugin will exit now.") common.say("*" * 80) return # You must either pass a training name or request listing if len(self.query) < 1 and not (options.list or options.version): self.parser.print_help() return if options.version: self.show_version_information() return elif options.list: self.list_trainings() return self.handle_training()
def _setup_bin_boundaries(self): self.bin_boundaries = [] if len(self.items) <= 1: raise ValueError("There is only one song in the selection!") _min, _max, _sum, _avg = common.get_min_max_sum_avg_for_items( self.items, "length") if not _avg: raise ValueError("Average song length is zero!") num_bins = round(self.duration / _avg) bin_size = math.floor(len(self.items) / num_bins) common.say("Number of bins: {}".format(num_bins)) common.say("Bin size: {}".format(bin_size)) if not bin_size or bin_size * num_bins > len(self.items): low = 0 high = len(self.items) - 1 self.bin_boundaries.append([low, high]) else: for bi in range(0, num_bins): is_last_bin = bi == (num_bins - 1) low = bi * bin_size high = low + bin_size - 1 if is_last_bin: high = len(self.items) - 1 self.bin_boundaries.append([low, high]) common.say("Bin boundaries: {}".format(self.bin_boundaries))
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 _score_items(self): common.say("Scoring {} items...".format(len(self.items))) # Score the library items for item in self.items: item["ordering_score"] = 0 item["ordering_info"] = {} for field_name in self.order_info.keys(): field_info = self.order_info[field_name] try: field_value = round(float(item.get(field_name, None)), 3) except ValueError: field_value = None except TypeError: field_value = None if field_value is None: field_value = _get_field_info_value( field_info, self.no_value_strategy) distance_from_min = round(field_value - field_info["min"], 6) # Linear - field_score should always be between 0 and 100 field_score = round(distance_from_min * field_info["step"], 6) field_score = field_score if field_score > 0 else 0 field_score = field_score if field_score < 100 else 100 weighted_field_score = round( field_info["weight"] * field_score / 100, 6) item["ordering_score"] = round( item["ordering_score"] + weighted_field_score, 6) item["ordering_info"][field_name] = { "distance_from_min": distance_from_min, "field_score": field_score, "weighted_field_score": weighted_field_score }
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 __init__(self): common.say("PICKER strategy: {0} ('favour_unplayed': {1})".format( self.__class__.__name__, 'yes' if favour_unplayed else 'no'))
def show_selection_status(self): sel_time = sum(l["length"] for l in self.selection) time_diff = sel_time - self.duration common.say("TOTAL(sec):{} SELECTED(sec):{} DIFFERENCE(sec):{}".format( self.duration, round(sel_time), round(time_diff)))
def _improve_selection(self): # Try to get as close to duration as possible max_overtime = 10 sel_time = sum(l["length"] for l in self.selection) curr_sel = 0 curr_run = 0 max_run = len(self.bin_boundaries) * 3 exclusions = {} if not self.selection: common.say("IMPROVEMENTS: SKIPPED (No initial selection)") return # iterate through initial selection items and try to find a better # alternative for them while sel_time < self.duration or sel_time > self.duration + \ max_overtime: curr_run += 1 if curr_run > max_run: common.say("MAX HIT!") break # common.say("{} IMPROVEMENT RUN: {}/{}". # format("=" * 60, curr_run, max_run)) curr_bin = self.selection[curr_sel]["bin"] curr_index = self.selection[curr_sel]["index"] curr_len = self.selection[curr_sel]["length"] # if positive we need shorter songs if negative then longer time_diff = abs(round(sel_time - self.duration)) min_len = curr_len - time_diff max_len = curr_len + time_diff if curr_bin not in exclusions.keys(): exclusions[curr_bin] = [curr_index] exclude = exclusions[curr_bin] index = self._get_item_within_length(curr_bin, min_len, max_len, exclude_indices=exclude) if index is not None: exclude.append(index) item: Item = self.items[index] item_len = item.get("length") new_diff = abs((sel_time - curr_len + item_len) - self.duration) if new_diff < time_diff: sel_data = { "index": index, "bin": curr_bin, "length": item_len, "play_count": item.get("play_count", 0) } del self.selection[curr_sel] self.selection.insert(curr_sel, sel_data) sel_time = round(sum(l["length"] for l in self.selection)) common.say("{} IMPROVEMENT RUN: {}/{}".format( "=" * 60, curr_run, max_run)) common.say("PROPOSAL[bin: {}](index: {}): {} -> {}".format( curr_bin, index, round(curr_len), round(item_len))) common.say("IMPROVED BY: {} sec".format( round(time_diff - new_diff))) self.show_selection_status() if curr_sel < len(self.selection) - 1: curr_sel += 1 else: curr_sel = 0 common.say("{} IMPROVEMENTS: FINISHED".format("=" * 60)) self.show_selection_status() common.say("SELECTION: {}".format(self.selection))
def _say(msg, log_only=True, is_error=False): common.say(msg, log_only, is_error)
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()
def __init__(self): common.say("ORDERING permutation: {0}".format(self.__class__.__name__))
def test_say(self): test_message = "one two three" with capture_log() as logs: common.say(test_message) self.assertIn(test_message, '\n'.join(logs))