<commit_before>import {buildConsecutiveRanges, indexMapper} from "util";
import Selectable from "ui/Selectable";
import DraggableSelection from "ui/DraggableSelection";
import Track from "tracks/Track";
import {ACTION_CLICKED} from "ui/Snackbar";
import TrackView from "tracks/TrackView";
import TrackViewOptions from "tracks/TrackViewOptions";
import TrackContainerTrait from "tracks/TrackContainerTrait";
import TrackSorterTrait from "tracks/TrackSorterTrait";
import withDeps from "ApplicationDependencies";
import EventEmitter from "events";

const PLAYLIST_TRACKS_REMOVED_TAG = `playlist-tracks-removed`;
const PLAYLIST_MODE_KEY = `playlist-mode`;
const SHUFFLE_MODE = `shuffle`;

const KIND_IMPLICIT = 0;
const KIND_EXPLICIT = 1;
const MAX_ERRORS = 200;
const MAX_HISTORY = 500;
const EMPTY_ARRAY = Object.freeze([]);

const DUMMY_TRACK = {
    getIndex() {
        return -1;
    },

    isDetachedFromPlaylist() {
        return true;
    },

    hasError() {
        return false;
    }
};

const Modes = {
    normal(currentTrack) {
        let index = currentTrack.getIndex() + 1;

        let ret;
        let trials = 0;

        do {
            index = Math.max(0, index);
            if (index >= this.length) {
                index = 0;
            }

            ret = this.getTrackByIndex(index);
            index++;
            trials++;
        } while (ret && ret.hasError() && trials <= this.length);
        return ret;
    },

    shuffle(currentTrack) {
        const lastHour = Date.now() - 60 * 60 * 1000;

        function getWeight(track) {
            for (let j = 0; j < zeroWeights.length; ++j) {
                if (zeroWeights[j] === track) {
                    return 0;
                }
            }

            if (track.hasError()) {
                return 0;
            }

            const rating = track.isRated() ? track.getRating() : 3;
            let weight = Math.pow(1.5, rating - 1);

            if (track.hasBeenPlayedWithin(lastHour)) {
                weight /= Math.pow(3, 2);
            }

            return Math.ceil(weight);
        }

        const zeroWeights = [currentTrack, this.getNextTrack()].filter(track => track && !track.isDetachedFromPlaylist() && !track.hasError());

        let maxWeight = 0;
        const trackViews = this.getTrackViews();

        for (let i = 0; i < trackViews.length; ++i) {
            const trackView = trackViews[i];
            maxWeight += getWeight(trackView.track());
        }

        const target = ((Math.random() * maxWeight + 1) | 0) - 1;
        let currentWeight = -1;
        let randomlyChosenTrackView = null;
        for (let i = 0; i < trackViews.length; ++i) {
            randomlyChosenTrackView = trackViews[i];
            const weight = getWeight(randomlyChosenTrackView.track());

            if (currentWeight + weight >= target) {
                return randomlyChosenTrackView.track();
            }
            currentWeight += weight;
        }

        randomlyChosenTrackView = randomlyChosenTrackView && randomlyChosenTrackView.track().hasError() ? trackViews.last() : randomlyChosenTrackView;
        randomlyChosenTrackView = randomlyChosenTrackView && randomlyChosenTrackView.track().hasError() ? null : randomlyChosenTrackView;
        return randomlyChosenTrackView ? randomlyChosenTrackView.track() : null;
    },

    repeat(currentTrack) {
        if (currentTrack.isDetachedFromPlaylist() || currentTrack.hasError()) {
            return Modes.normal.call(this, currentTrack);
        }
        return currentTrack;
    }
};

let nextPlayId = 10;

class TrackListDeletionUndo {
    constructor(playlist) {
        this.playlist = playlist;
        this.tracksAndViews = playlist.getTrackViews().map(v => ({
            track: v.track(),
            view: v
        }));
        this.selectionIndices = playlist.getSelection().map(v => v.getIndex());
        const priorityTrackView = playlist._selectable.getPriorityTrackView();
        this.priorityTrackViewIndex = priorityTrackView ? priorityTrackView.getIndex() : -1;
    }

    destroy() {
        this.playlist.snackbar.removeByTag(PLAYLIST_TRACKS_REMOVED_TAG);
        for (let i = 0; i < this.tracksAndViews.length; ++i) {
            const {track} = this.tracksAndViews[i];
            if (track.isDetachedFromPlaylist()) {
                track.destroy();
            }
        }
    }
}

export default class Playlist extends EventEmitter {
    constructor(opts, deps) {
        super();
        this.page = deps.page;
        this.globalEvents = deps.globalEvents;
        this.recognizerContext = deps.recognizerContext;
        this.keyboardShortcuts = deps.keyboardShortcuts;
        this.env = deps.env;
        this.db = deps.db;
        this.dbValues = deps.dbValues;
        this.rippler = deps.rippler;
        this.snackbar = deps.snackbar;
        this.applicationPreferences = deps.applicationPreferences;
        this.tooltipContext = deps.tooltipContext;

        this._trackViews = [];
        this._unparsedTrackList = [];

        this._mode = Modes.hasOwnProperty(opts.mode) ? opts.mode : `normal`;
        this._currentTrack = null;
        this._trackListDeletionUndo = null;
        this._currentPlayId = -1;
        this._trackHistory = [];
        this._selectable = withDeps({page: this.page}, d => new Selectable({listView: this}, d));

        this._trackViewOptions = new TrackViewOptions(true,
                                                      opts.itemHeight,
                                                      this,
                                                      this.page,
                                                      deps.tooltipContext,
                                                      this._selectable,
                                                      null);
        this._errorCount = 0;
        this._$domNode = this.page.$(opts.target);
        this._$trackContainer = this.$().find(`.tracklist-transform-container`);
        this._nextTrack = null;

        this._fixedItemListScroller = deps.scrollerContext.createFixedItemListScroller({
            target: this.$(),
            itemList: this._trackViews,
            contentContainer: this.$trackContainer(),

            minPrerenderedItems: 15,
            maxPrerenderedItems: 75,

            shouldScroll: () => !this._draggable.isDragging(),

            scrollerOpts: {
                scrollingX: false,
                snapping: true,
                zooming: false,
                paging: false
            },
            scrollbarOpts: {
                target: this.$().find(`.scrollbar-container`),
                railSelector: `.scrollbar-rail`,
                knobSelector: `.scrollbar-knob`
            }
        });

        this._draggable = withDeps({
            recognizerContext: this.recognizerContext,
            page: this.page,
            globalEvents: this.globalEvents
        }, d => new DraggableSelection({
            target: this.$(),
            listView: this,
            scroller: this._fixedItemListScroller,
            mustNotMatchSelector: `.track-rating`,
            mustMatchSelector: `.track-container`
        }, d));<commit_msg>less items<commit_after>import {buildConsecutiveRanges, indexMapper} from "util";
import Selectable from "ui/Selectable";
import DraggableSelection from "ui/DraggableSelection";
import Track from "tracks/Track";
import {ACTION_CLICKED} from "ui/Snackbar";
import TrackView from "tracks/TrackView";
import TrackViewOptions from "tracks/TrackViewOptions";
import TrackContainerTrait from "tracks/TrackContainerTrait";
import TrackSorterTrait from "tracks/TrackSorterTrait";
import withDeps from "ApplicationDependencies";
import EventEmitter from "events";

const PLAYLIST_TRACKS_REMOVED_TAG = `playlist-tracks-removed`;
const PLAYLIST_MODE_KEY = `playlist-mode`;
const SHUFFLE_MODE = `shuffle`;

const KIND_IMPLICIT = 0;
const KIND_EXPLICIT = 1;
const MAX_ERRORS = 200;
const MAX_HISTORY = 500;
const EMPTY_ARRAY = Object.freeze([]);

const DUMMY_TRACK = {
    getIndex() {
        return -1;
    },

    isDetachedFromPlaylist() {
        return true;
    },

    hasError() {
        return false;
    }
};

const Modes = {
    normal(currentTrack) {
        let index = currentTrack.getIndex() + 1;

        let ret;
        let trials = 0;

        do {
            index = Math.max(0, index);
            if (index >= this.length) {
                index = 0;
            }

            ret = this.getTrackByIndex(index);
            index++;
            trials++;
        } while (ret && ret.hasError() && trials <= this.length);
        return ret;
    },

    shuffle(currentTrack) {
        const lastHour = Date.now() - 60 * 60 * 1000;

        function getWeight(track) {
            for (let j = 0; j < zeroWeights.length; ++j) {
                if (zeroWeights[j] === track) {
                    return 0;
                }
            }

            if (track.hasError()) {
                return 0;
            }

            const rating = track.isRated() ? track.getRating() : 3;
            let weight = Math.pow(1.5, rating - 1);

            if (track.hasBeenPlayedWithin(lastHour)) {
                weight /= Math.pow(3, 2);
            }

            return Math.ceil(weight);
        }

        const zeroWeights = [currentTrack, this.getNextTrack()].filter(track => track && !track.isDetachedFromPlaylist() && !track.hasError());

        let maxWeight = 0;
        const trackViews = this.getTrackViews();

        for (let i = 0; i < trackViews.length; ++i) {
            const trackView = trackViews[i];
            maxWeight += getWeight(trackView.track());
        }

        const target = ((Math.random() * maxWeight + 1) | 0) - 1;
        let currentWeight = -1;
        let randomlyChosenTrackView = null;
        for (let i = 0; i < trackViews.length; ++i) {
            randomlyChosenTrackView = trackViews[i];
            const weight = getWeight(randomlyChosenTrackView.track());

            if (currentWeight + weight >= target) {
                return randomlyChosenTrackView.track();
            }
            currentWeight += weight;
        }

        randomlyChosenTrackView = randomlyChosenTrackView && randomlyChosenTrackView.track().hasError() ? trackViews.last() : randomlyChosenTrackView;
        randomlyChosenTrackView = randomlyChosenTrackView && randomlyChosenTrackView.track().hasError() ? null : randomlyChosenTrackView;
        return randomlyChosenTrackView ? randomlyChosenTrackView.track() : null;
    },

    repeat(currentTrack) {
        if (currentTrack.isDetachedFromPlaylist() || currentTrack.hasError()) {
            return Modes.normal.call(this, currentTrack);
        }
        return currentTrack;
    }
};

let nextPlayId = 10;

class TrackListDeletionUndo {
    constructor(playlist) {
        this.playlist = playlist;
        this.tracksAndViews = playlist.getTrackViews().map(v => ({
            track: v.track(),
            view: v
        }));
        this.selectionIndices = playlist.getSelection().map(v => v.getIndex());
        const priorityTrackView = playlist._selectable.getPriorityTrackView();
        this.priorityTrackViewIndex = priorityTrackView ? priorityTrackView.getIndex() : -1;
    }

    destroy() {
        this.playlist.snackbar.removeByTag(PLAYLIST_TRACKS_REMOVED_TAG);
        for (let i = 0; i < this.tracksAndViews.length; ++i) {
            const {track} = this.tracksAndViews[i];
            if (track.isDetachedFromPlaylist()) {
                track.destroy();
            }
        }
    }
}

export default class Playlist extends EventEmitter {
    constructor(opts, deps) {
        super();
        this.page = deps.page;
        this.globalEvents = deps.globalEvents;
        this.recognizerContext = deps.recognizerContext;
        this.keyboardShortcuts = deps.keyboardShortcuts;
        this.env = deps.env;
        this.db = deps.db;
        this.dbValues = deps.dbValues;
        this.rippler = deps.rippler;
        this.snackbar = deps.snackbar;
        this.applicationPreferences = deps.applicationPreferences;
        this.tooltipContext = deps.tooltipContext;

        this._trackViews = [];
        this._unparsedTrackList = [];

        this._mode = Modes.hasOwnProperty(opts.mode) ? opts.mode : `normal`;
        this._currentTrack = null;
        this._trackListDeletionUndo = null;
        this._currentPlayId = -1;
        this._trackHistory = [];
        this._selectable = withDeps({page: this.page}, d => new Selectable({listView: this}, d));

        this._trackViewOptions = new TrackViewOptions(true,
                                                      opts.itemHeight,
                                                      this,
                                                      this.page,
                                                      deps.tooltipContext,
                                                      this._selectable,
                                                      null);
        this._errorCount = 0;
        this._$domNode = this.page.$(opts.target);
        this._$trackContainer = this.$().find(`.tracklist-transform-container`);
        this._nextTrack = null;

        this._fixedItemListScroller = deps.scrollerContext.createFixedItemListScroller({
            target: this.$(),
            itemList: this._trackViews,
            contentContainer: this.$trackContainer(),

            minPrerenderedItems: 6,
            maxPrerenderedItems: 12,

            shouldScroll: () => !this._draggable.isDragging(),

            scrollerOpts: {
                scrollingX: false,
                snapping: true,
                zooming: false,
                paging: false
            },
            scrollbarOpts: {
                target: this.$().find(`.scrollbar-container`),
                railSelector: `.scrollbar-rail`,
                knobSelector: `.scrollbar-knob`
            }
        });

        this._draggable = withDeps({
            recognizerContext: this.recognizerContext,
            page: this.page,
            globalEvents: this.globalEvents
        }, d => new DraggableSelection({
            target: this.$(),
            listView: this,
            scroller: this._fixedItemListScroller,
            mustNotMatchSelector: `.track-rating`,
            mustMatchSelector: `.track-container`
        }, d));
{'fraction_non_alphanumeric': 0.108876560332871, 'fraction_numerical': 0.006449375866851595, 'mean_word_length': 1.9039468385018123, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4702428', 'n_tokens_mistral': 4075, 'n_tokens_neox': 3897, 'n_words': 790}