Skip to content

Commit 4b4d1e0

Browse files
committed
merge main
2 parents 1b75271 + 3b0a803 commit 4b4d1e0

File tree

91 files changed

+3558
-497
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+3558
-497
lines changed
7.86 KB
Binary file not shown.

e2e/src/api/specs/person.e2e-spec.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@ describe('/people', () => {
1111
let hiddenPerson: PersonResponseDto;
1212
let multipleAssetsPerson: PersonResponseDto;
1313

14+
let nameAlicePerson: PersonResponseDto;
15+
let nameBobPerson: PersonResponseDto;
16+
let nameCharliePerson: PersonResponseDto;
17+
let nameNullPerson: PersonResponseDto;
18+
1419
beforeAll(async () => {
1520
await utils.resetDatabase();
1621
admin = await utils.adminSetup();
1722

18-
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
23+
[
24+
visiblePerson,
25+
hiddenPerson,
26+
multipleAssetsPerson,
27+
nameCharliePerson,
28+
nameBobPerson,
29+
nameAlicePerson,
30+
nameNullPerson,
31+
] = await Promise.all([
1932
utils.createPerson(admin.accessToken, {
2033
name: 'visible_person',
2134
}),
@@ -26,17 +39,40 @@ describe('/people', () => {
2639
utils.createPerson(admin.accessToken, {
2740
name: 'multiple_assets_person',
2841
}),
42+
// --- Setup for the specific sorting test ---
43+
utils.createPerson(admin.accessToken, {
44+
name: 'Charlie',
45+
}),
46+
utils.createPerson(admin.accessToken, {
47+
name: 'Bob',
48+
}),
49+
utils.createPerson(admin.accessToken, {
50+
name: 'Alice',
51+
}),
52+
utils.createPerson(admin.accessToken, {
53+
name: '',
54+
}),
2955
]);
3056

3157
const asset1 = await utils.createAsset(admin.accessToken);
3258
const asset2 = await utils.createAsset(admin.accessToken);
59+
const asset3 = await utils.createAsset(admin.accessToken);
3360

3461
await Promise.all([
3562
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
3663
utils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }),
3764
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
3865
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
3966
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
67+
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }),
68+
// Named persons
69+
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
70+
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
71+
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
72+
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
73+
// Null-named person
74+
utils.createFace({ assetId: asset1.id, personId: nameNullPerson.id }),
75+
utils.createFace({ assetId: asset2.id, personId: nameNullPerson.id }), // 2 assets
4076
]);
4177
});
4278

@@ -51,26 +87,53 @@ describe('/people', () => {
5187
expect(status).toBe(200);
5288
expect(body).toEqual({
5389
hasNextPage: false,
54-
total: 3,
90+
total: 7,
5591
hidden: 1,
5692
people: [
5793
expect.objectContaining({ name: 'multiple_assets_person' }),
94+
expect.objectContaining({ name: 'Bob' }),
95+
expect.objectContaining({ name: 'Alice' }),
96+
expect.objectContaining({ name: 'Charlie' }),
5897
expect.objectContaining({ name: 'visible_person' }),
5998
expect.objectContaining({ name: 'hidden_person' }),
6099
],
61100
});
62101
});
63102

103+
it('should sort visible people by asset count (desc), then by name (asc, nulls last)', async () => {
104+
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
105+
106+
expect(status).toBe(200);
107+
expect(body.hasNextPage).toBe(false);
108+
expect(body.total).toBe(7); // All persons
109+
expect(body.hidden).toBe(1); // 'hidden_person'
110+
111+
const people = body.people as PersonResponseDto[];
112+
113+
expect(people.map((p) => p.id)).toEqual([
114+
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
115+
nameBobPerson.id, // name: 'Bob', count: 2
116+
nameAlicePerson.id, // name: 'Alice', count: 1
117+
nameCharliePerson.id, // name: 'Charlie', count: 1
118+
visiblePerson.id, // name: 'visible_person', count: 1
119+
]);
120+
121+
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
122+
});
123+
64124
it('should return only visible people', async () => {
65125
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
66126

67127
expect(status).toBe(200);
68128
expect(body).toEqual({
69129
hasNextPage: false,
70-
total: 3,
130+
total: 7,
71131
hidden: 1,
72132
people: [
73133
expect.objectContaining({ name: 'multiple_assets_person' }),
134+
expect.objectContaining({ name: 'Bob' }),
135+
expect.objectContaining({ name: 'Alice' }),
136+
expect.objectContaining({ name: 'Charlie' }),
74137
expect.objectContaining({ name: 'visible_person' }),
75138
],
76139
});
@@ -80,12 +143,12 @@ describe('/people', () => {
80143
const { status, body } = await request(app)
81144
.get('/people')
82145
.set('Authorization', `Bearer ${admin.accessToken}`)
83-
.query({ withHidden: true, page: 2, size: 1 });
146+
.query({ withHidden: true, page: 5, size: 1 });
84147

85148
expect(status).toBe(200);
86149
expect(body).toEqual({
87150
hasNextPage: true,
88-
total: 3,
151+
total: 7,
89152
hidden: 1,
90153
people: [expect.objectContaining({ name: 'visible_person' })],
91154
});
@@ -128,7 +191,7 @@ describe('/people', () => {
128191
.set('Authorization', `Bearer ${admin.accessToken}`);
129192

130193
expect(status).toBe(200);
131-
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
194+
expect(body).toEqual(expect.objectContaining({ assets: 3 }));
132195
});
133196
});
134197

i18n/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@
244244
"storage_template_migration_info": "The storage template will convert all extensions to lowercase. Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",
245245
"storage_template_migration_job": "Storage Template Migration Job",
246246
"storage_template_more_details": "For more details about this feature, refer to the <template-link>Storage Template</template-link> and its <implications-link>implications</implications-link>",
247-
"storage_template_onboarding_description": "When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the feature has been turned off by default. For more information, please see the <link>documentation</link>.",
247+
"storage_template_onboarding_description_v2": "When enabled, this feature will auto-organize files based on a user-defined template. For more information, please see the <link>documentation</link>.",
248248
"storage_template_path_length": "Approximate path length limit: <b>{length, number}</b>/{limit, number}",
249249
"storage_template_settings": "Storage Template",
250250
"storage_template_settings_description": "Manage the folder structure and file name of the upload asset",

mobile/analysis_options.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ custom_lint:
5656
allowed:
5757
# required / wanted
5858
- 'lib/infrastructure/repositories/album_media.repository.dart'
59-
- 'lib/infrastructure/repositories/storage.repository.dart'
59+
- 'lib/infrastructure/repositories/{storage,asset_media}.repository.dart'
6060
- 'lib/repositories/{album,asset,file}_media.repository.dart'
6161
# acceptable exceptions for the time being
6262
- lib/entities/asset.entity.dart # to provide local AssetEntity for now

mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ open class NativeSyncApiImplBase(context: Context) {
8383
continue
8484
}
8585

86-
val mediaType = c.getInt(mediaTypeColumn)
86+
val mediaType = when (c.getInt(mediaTypeColumn)) {
87+
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1
88+
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2
89+
else -> 0
90+
}
8791
val name = c.getString(nameColumn)
8892
// Date taken is milliseconds since epoch, Date added is seconds since epoch
8993
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))

mobile/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ targets:
1717
main: lib/infrastructure/repositories/db.repository.dart
1818
generate_for: &drift_generate_for
1919
- lib/infrastructure/entities/*.dart
20+
- lib/infrastructure/entities/*.drift
2021
- lib/infrastructure/repositories/db.repository.dart
2122
drift_dev:modular:
2223
enabled: true

mobile/drift_schemas/main/drift_schema_v1.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mobile/ios/Runner/Sync/MessagesImpl.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class NativeSyncApiImpl: NativeSyncApi {
3636
private let defaults: UserDefaults
3737
private let changeTokenKey = "immich:changeToken"
3838
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
39+
private let recoveredAlbumSubType = 1000000219
3940

4041
private let hashBufferSize = 2 * 1024 * 1024
4142

@@ -91,9 +92,17 @@ class NativeSyncApiImpl: NativeSyncApi {
9192

9293
albumTypes.forEach { type in
9394
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
94-
collections.enumerateObjects { (album, _, _) in
95+
for i in 0..<collections.count {
96+
let album = collections.object(at: i)
97+
98+
// Ignore recovered album
99+
if(album.assetCollectionSubtype.rawValue == self.recoveredAlbumSubType) {
100+
continue;
101+
}
102+
95103
let options = PHFetchOptions()
96104
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
105+
options.includeHiddenAssets = false
97106
let assets = PHAsset.fetchAssets(in: album, options: options)
98107
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
99108

@@ -149,7 +158,9 @@ class NativeSyncApiImpl: NativeSyncApi {
149158

150159
if (updated.isEmpty) { continue }
151160

152-
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
161+
let options = PHFetchOptions()
162+
options.includeHiddenAssets = false
163+
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
153164
for i in 0..<result.count {
154165
let asset = result.object(at: i)
155166

@@ -187,6 +198,7 @@ class NativeSyncApiImpl: NativeSyncApi {
187198
collections.enumerateObjects { (album, _, _) in
188199
let options = PHFetchOptions()
189200
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
201+
options.includeHiddenAssets = false
190202
let result = PHAsset.fetchAssets(in: album, options: options)
191203
result.enumerateObjects { (asset, _, _) in
192204
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
@@ -203,7 +215,9 @@ class NativeSyncApiImpl: NativeSyncApi {
203215
}
204216

205217
var ids: [String] = []
206-
let assets = PHAsset.fetchAssets(in: album, options: nil)
218+
let options = PHFetchOptions()
219+
options.includeHiddenAssets = false
220+
let assets = PHAsset.fetchAssets(in: album, options: options)
207221
assets.enumerateObjects { (asset, _, _) in
208222
ids.append(asset.localIdentifier)
209223
}
@@ -219,6 +233,7 @@ class NativeSyncApiImpl: NativeSyncApi {
219233
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
220234
let options = PHFetchOptions()
221235
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
236+
options.includeHiddenAssets = false
222237
let assets = PHAsset.fetchAssets(in: album, options: options)
223238
return Int64(assets.count)
224239
}
@@ -230,6 +245,7 @@ class NativeSyncApiImpl: NativeSyncApi {
230245
}
231246

232247
let options = PHFetchOptions()
248+
options.includeHiddenAssets = false
233249
if(updatedTimeCond != nil) {
234250
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
235251
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)

mobile/lib/constants/constants.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ const String kSecuredPinCode = "secured_pin_code";
2020
const kManualUploadGroup = 'manual_upload_group';
2121
const kBackupGroup = 'backup_group';
2222
const kBackupLivePhotoGroup = 'backup_live_photo_group';
23+
24+
// Timeline constants
25+
const int kTimelineNoneSegmentSize = 120;
26+
const int kTimelineAssetLoadBatchSize = 256;
27+
const int kTimelineAssetLoadOppositeSize = 64;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import 'dart:typed_data';
2+
import 'dart:ui';
3+
4+
abstract interface class IAssetMediaRepository {
5+
Future<Uint8List?> getThumbnail(
6+
String id, {
7+
int quality = 80,
8+
Size size = const Size.square(256),
9+
});
10+
}

0 commit comments

Comments
 (0)