mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix(web): Uniform random distribution during shuffle (#19902)
feat: better random distribution
This commit is contained in:
parent
54ed78d0bf
commit
6f3cb4f1bb
4 changed files with 91 additions and 23 deletions
|
|
@ -82,11 +82,6 @@ export class DayGroup {
|
||||||
return this.viewerAssets[0]?.asset;
|
return this.viewerAssets[0]?.asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRandomAsset() {
|
|
||||||
const random = Math.floor(Math.random() * this.viewerAssets.length);
|
|
||||||
return this.viewerAssets[random];
|
|
||||||
}
|
|
||||||
|
|
||||||
*assetsIterator(options: { startAsset?: TimelineAsset; direction?: Direction } = {}) {
|
*assetsIterator(options: { startAsset?: TimelineAsset; direction?: Direction } = {}) {
|
||||||
const isEarlier = (options?.direction ?? 'earlier') === 'earlier';
|
const isEarlier = (options?.direction ?? 'earlier') === 'earlier';
|
||||||
let assetIndex = options?.startAsset
|
let assetIndex = options?.startAsset
|
||||||
|
|
|
||||||
|
|
@ -233,15 +233,6 @@ export class MonthGroup {
|
||||||
addContext.changedDayGroups.add(dayGroup);
|
addContext.changedDayGroups.add(dayGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRandomDayGroup() {
|
|
||||||
const random = Math.floor(Math.random() * this.dayGroups.length);
|
|
||||||
return this.dayGroups[random];
|
|
||||||
}
|
|
||||||
|
|
||||||
getRandomAsset() {
|
|
||||||
return this.getRandomDayGroup()?.getRandomAsset()?.asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
get viewId() {
|
get viewId() {
|
||||||
const { year, month } = this.yearMonth;
|
const { year, month } = this.yearMonth;
|
||||||
return year + '-' + month;
|
return year + '-' + month;
|
||||||
|
|
|
||||||
|
|
@ -580,4 +580,60 @@ describe('TimelineManager', () => {
|
||||||
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
|
expect(timelineManager.getMonthGroupByAssetId(assetOne.id)?.yearMonth.month).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getRandomAsset', () => {
|
||||||
|
let timelineManager: TimelineManager;
|
||||||
|
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||||
|
'2024-03-01T00:00:00.000Z': timelineAssetFactory.buildList(1).map((asset) =>
|
||||||
|
deriveLocalDateTimeFromFileCreatedAt({
|
||||||
|
...asset,
|
||||||
|
fileCreatedAt: fromISODateTimeUTCToObject('2024-03-01T00:00:00.000Z'),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
'2024-02-01T00:00:00.000Z': timelineAssetFactory.buildList(10).map((asset, idx) =>
|
||||||
|
deriveLocalDateTimeFromFileCreatedAt({
|
||||||
|
...asset,
|
||||||
|
// here we make sure that not all assets are on the first day of the month
|
||||||
|
fileCreatedAt: fromISODateTimeUTCToObject(`2024-02-0${idx < 7 ? 1 : 2}T00:00:00.000Z`),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
'2024-01-01T00:00:00.000Z': timelineAssetFactory.buildList(3).map((asset) =>
|
||||||
|
deriveLocalDateTimeFromFileCreatedAt({
|
||||||
|
...asset,
|
||||||
|
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-01T00:00:00.000Z'),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
|
||||||
|
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
timelineManager = new TimelineManager();
|
||||||
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
|
{ count: 1, timeBucket: '2024-03-01' },
|
||||||
|
{ count: 10, timeBucket: '2024-02-01' },
|
||||||
|
{ count: 3, timeBucket: '2024-01-01' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||||
|
await timelineManager.updateViewport({ width: 1588, height: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets all assets once', async () => {
|
||||||
|
const assetCount = timelineManager.assetCount;
|
||||||
|
expect(assetCount).toBe(14);
|
||||||
|
const discoveredAssets: Set<string> = new Set();
|
||||||
|
for (let idx = 0; idx < assetCount; idx++) {
|
||||||
|
const asset = await timelineManager.getRandomAsset(idx);
|
||||||
|
expect(asset).toBeDefined();
|
||||||
|
const id = asset!.id;
|
||||||
|
expect(discoveredAssets.has(id)).toBeFalsy();
|
||||||
|
discoveredAssets.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(discoveredAssets.size).toBe(assetCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -451,16 +451,42 @@ export class TimelineManager {
|
||||||
return monthGroupInfo?.monthGroup;
|
return monthGroupInfo?.monthGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRandomMonthGroup() {
|
// note: the `index` input is expected to be in the range [0, assetCount). This
|
||||||
const random = Math.floor(Math.random() * this.months.length);
|
// value can be passed to make the method deterministic, which is mainly useful
|
||||||
const month = this.months[random];
|
// for testing.
|
||||||
await this.loadMonthGroup(month.yearMonth, { cancelable: false });
|
async getRandomAsset(index?: number): Promise<TimelineAsset | undefined> {
|
||||||
return month;
|
const randomAssetIndex = index ?? Math.floor(Math.random() * this.assetCount);
|
||||||
}
|
|
||||||
|
|
||||||
async getRandomAsset() {
|
let accumulatedCount = 0;
|
||||||
const month = await this.getRandomMonthGroup();
|
|
||||||
return month?.getRandomAsset();
|
let randomMonth: MonthGroup | undefined = undefined;
|
||||||
|
for (const month of this.months) {
|
||||||
|
if (randomAssetIndex < accumulatedCount + month.assetsCount) {
|
||||||
|
randomMonth = month;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedCount += month.assetsCount;
|
||||||
|
}
|
||||||
|
if (!randomMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.loadMonthGroup(randomMonth.yearMonth, { cancelable: false });
|
||||||
|
|
||||||
|
let randomDay: DayGroup | undefined = undefined;
|
||||||
|
for (const day of randomMonth.dayGroups) {
|
||||||
|
if (randomAssetIndex < accumulatedCount + day.viewerAssets.length) {
|
||||||
|
randomDay = day;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedCount += day.viewerAssets.length;
|
||||||
|
}
|
||||||
|
if (!randomDay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return randomDay.viewerAssets[randomAssetIndex - accumulatedCount].asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAssetOperation(ids: string[], operation: AssetOperation) {
|
updateAssetOperation(ids: string[], operation: AssetOperation) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue