xref: /MusicFree/src/core/musicSheet/sortedMusicList.ts (revision 7fb901109d23a379901d3a07c4fcb24021621c5d)
1import {SortType} from '@/constants/commonConst.ts';
2import {isSameMediaItem} from '@/utils/mediaItem.ts';
3import {createMediaIndexMap} from '@/utils/mediaIndexMap.ts';
4
5// Bug: localeCompare is slow sometimes https://github.com/facebook/hermes/issues/867
6const collator = new Intl.Collator('zh');
7
8/// Compare Functions
9
10const compareTitle = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) =>
11    collator.compare(a.title, b.title);
12const compareArtist = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) =>
13    collator.compare(a.artist, b.artist);
14const compareAlbum = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) =>
15    collator.compare(a.album, b.album);
16const compareTimeNewToOld = (b: IMusic.IMusicItem, a: IMusic.IMusicItem) => {
17    const timestamp = (a.$timestamp || 0) - (b.$timestamp || 0);
18    if (timestamp === 0) {
19        return (a.$sortIndex || 0) - (b.$sortIndex || 0);
20    } else {
21        return timestamp;
22    }
23};
24
25const compareTimeOldToNew = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) => {
26    const timestamp = (a.$timestamp || 0) - (b.$timestamp || 0);
27    if (timestamp === 0) {
28        return (a.$sortIndex || 0) - (b.$sortIndex || 0);
29    } else {
30        return timestamp;
31    }
32};
33
34const compareFunctionMap = {
35    [SortType.Title]: compareTitle,
36    [SortType.Artist]: compareArtist,
37    [SortType.Album]: compareAlbum,
38    [SortType.Newest]: compareTimeNewToOld,
39    [SortType.Oldest]: compareTimeOldToNew,
40} as const;
41
42export default class SortedMusicList {
43    private array: IMusic.IMusicItem[] = [];
44
45    private sortType: SortType = SortType.None;
46
47    private countMap = new Map<string, Set<string>>();
48
49    get musicList() {
50        return this.array;
51    }
52
53    get firstMusic() {
54        return this.array[0] || null;
55    }
56
57    get platforms() {
58        return [...this.countMap.keys()];
59    }
60
61    get length() {
62        return this.array.length;
63    }
64
65    constructor(
66        musicItems: IMusic.IMusicItem[],
67        sortType = SortType.None,
68        skipInitialSort = false,
69    ) {
70        this.array = [...musicItems];
71        this.addToCountMap(this.array);
72        this.sortType = sortType;
73
74        if (!skipInitialSort) {
75            this.resort();
76        }
77    }
78
79    at(index: number) {
80        return this.array[index] || null;
81    }
82
83    has(musicItem: IMusic.IMusicItem | null) {
84        if (!musicItem) {
85            return false;
86        }
87        const platform = musicItem.platform.toString();
88        const id = musicItem.id.toString();
89
90        return this.countMap.get(platform)?.has(id) || false;
91    }
92
93    // 设置排序类型
94    setSortType(sortType: SortType) {
95        if (
96            this.sortType === sortType &&
97            this.sortType !== SortType.None &&
98            this.sortType
99        ) {
100            return;
101        }
102        this.sortType = sortType;
103        this.resort();
104    }
105
106    manualSort(newMusicItems: IMusic.IMusicItem[]) {
107        this.array = newMusicItems;
108        this.sortType = SortType.None;
109    }
110
111    add(musicItems: IMusic.IMusicItem[]) {
112        musicItems = musicItems.filter(it => !this.has(it));
113        if (!musicItems.length) {
114            return 0;
115        }
116        this.addToCountMap(musicItems);
117
118        if (!compareFunctionMap[this.sortType]) {
119            this.array = musicItems.concat(this.array);
120            return musicItems.length;
121        }
122
123        // 如果歌单内歌曲比较少
124        if (
125            this.array.length + musicItems.length < 500 ||
126            musicItems.length / (this.array.length + 1) > 10
127        ) {
128            this.array = musicItems.concat(this.array);
129            this.resort();
130            return musicItems.length;
131        }
132        // 如果歌单内歌曲比较多
133        musicItems.sort(compareFunctionMap[this.sortType]);
134        this.array = this.mergeArray(musicItems, this.array, this.sortType);
135        return musicItems.length;
136    }
137
138    remove(musicItems: IMusic.IMusicItem[]) {
139        const indexMap = createMediaIndexMap(musicItems);
140
141        this.array = this.array.filter(it => !indexMap.has(it));
142        this.removeFromCountMap(musicItems);
143    }
144
145    removeByIndex(indices: number[]) {
146        const indicesSet = new Set(indices);
147        const removedItems: IMusic.IMusicItem[] = [];
148
149        this.array = this.array.filter((it, index) => {
150            if (indicesSet.has(index)) {
151                removedItems.push(it);
152                return false;
153            }
154            return true;
155        });
156
157        this.removeFromCountMap(removedItems);
158    }
159
160    clearAll() {
161        this.array = [];
162    }
163
164    private addToCountMap(musicItems: IMusic.IMusicItem[]) {
165        for (let musicItem of musicItems) {
166            const platform = musicItem.platform.toString();
167            const id = musicItem.id.toString();
168
169            if (this.countMap.has(platform)) {
170                this.countMap.get(platform)!.add(id);
171            } else {
172                this.countMap.set(platform, new Set([id]));
173            }
174        }
175    }
176
177    private removeFromCountMap(musicItems: IMusic.IMusicItem[]) {
178        for (let musicItem of musicItems) {
179            const platform = musicItem.platform.toString();
180            const id = musicItem.id.toString();
181
182            if (this.countMap.has(platform)) {
183                const set = this.countMap.get(platform)!;
184                set.delete(id);
185                if (set.size === 0) {
186                    this.countMap.delete(platform);
187                }
188            }
189        }
190    }
191
192    /**
193     * 合并两个有序列表
194     * @param musicList1
195     * @param musicList2
196     * @param sortType
197     * @private
198     */
199    private mergeArray(
200        musicList1: IMusic.IMusicItem[],
201        musicList2: IMusic.IMusicItem[],
202        sortType: SortType,
203    ) {
204        // 无序
205        const compareFn = compareFunctionMap[sortType];
206
207        if (!compareFn) {
208            return musicList1.concat(musicList2);
209        }
210
211        let [p1, p2] = [0, 0];
212        let resultArray: IMusic.IMusicItem[] = [];
213        let peek1: IMusic.IMusicItem, peek2: IMusic.IMusicItem;
214        while (p1 < musicList1.length && p2 < musicList2.length) {
215            peek1 = musicList1[p1];
216            peek2 = musicList2[p2];
217
218            if (compareFn(peek1, peek2) < 0) {
219                resultArray.push(peek1);
220                ++p1;
221            } else {
222                resultArray.push(peek2);
223                ++p2;
224            }
225        }
226
227        if (p1 < musicList1.length) {
228            return resultArray.concat(musicList1.slice(p1));
229        }
230        if (p2 < musicList2.length) {
231            return resultArray.concat(musicList2.slice(p2));
232        }
233
234        return resultArray;
235    }
236
237    /**
238     * 寻找musicItem
239     * @param musicItem
240     * @private
241     */
242    public findIndex(musicItem: IMusic.IMusicItem) {
243        if (!compareFunctionMap[this.sortType]) {
244            return this.array.find(it => isSameMediaItem(it, musicItem));
245        }
246        let [left, right] = [0, this.array.length];
247        let mid: number;
248
249        while (left < right) {
250            mid = Math.floor((left + right) / 2);
251            let compareResult = compareFunctionMap[this.sortType](
252                this.array[mid],
253                musicItem,
254            );
255
256            if (compareResult < 0) {
257                left = mid + 1;
258            } else if (compareResult === 0) {
259                left = mid;
260                break;
261            } else {
262                right = mid;
263            }
264        }
265
266        return left === right ? -1 : left;
267    }
268
269    // 重新排序
270    private resort() {
271        const compareFn = compareFunctionMap[this.sortType];
272
273        if (!compareFn) {
274            return;
275        }
276        this.array.sort(compareFn);
277        this.array = [...this.array];
278        return;
279    }
280}
281