Exemple #1
0
    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()
Exemple #2
0
    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)
Exemple #3
0
    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()
Exemple #4
0
    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)
Exemple #6
0
    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
                }
Exemple #7
0
    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)
Exemple #9
0
 def __init__(self):
     common.say("PICKER strategy: {0} ('favour_unplayed': {1})".format(
         self.__class__.__name__, 'yes' if favour_unplayed else 'no'))
Exemple #10
0
 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)))
Exemple #11
0
    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))
Exemple #12
0
 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()
Exemple #14
0
 def __init__(self):
     common.say("ORDERING permutation: {0}".format(self.__class__.__name__))
Exemple #15
0
    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))