Add api to retrieve face data from the Recognize app

This commit is contained in:
Ming Ming 2023-06-15 23:31:22 +08:00
parent 4bde517813
commit c54b178e5a
9 changed files with 676 additions and 0 deletions

View file

@ -12,6 +12,7 @@ part 'face_recognition_api.dart';
part 'files_api.dart';
part 'files_sharing_api.dart';
part 'photos_api.dart';
part 'recognize_api.dart';
part 'status_api.dart';
part 'systemtag_api.dart';

View file

@ -77,6 +77,20 @@ extension _$ApiPhotosAlbumNpLog on ApiPhotosAlbum {
static final log = Logger("src.api.ApiPhotosAlbum");
}
extension _$ApiRecognizeFacesNpLog on ApiRecognizeFaces {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("src.api.ApiRecognizeFaces");
}
extension _$ApiRecognizeFaceNpLog on ApiRecognizeFace {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("src.api.ApiRecognizeFace");
}
extension _$ApiStatusNpLog on ApiStatus {
// ignore: unused_element
Logger get _log => log;

View file

@ -212,6 +212,71 @@ class Person with EquatableMixin {
final int count;
}
@toString
class RecognizeFace with EquatableMixin {
const RecognizeFace({
required this.href,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
href,
];
final String href;
}
@ToString(ignoreNull: true)
class RecognizeFaceItem with EquatableMixin {
const RecognizeFaceItem({
required this.href,
this.contentLength,
this.contentType,
this.etag,
this.lastModified,
this.faceDetections,
this.fileMetadataSize,
this.hasPreview,
this.realPath,
this.favorite,
this.fileId,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
href,
contentLength,
contentType,
etag,
lastModified,
faceDetections,
fileMetadataSize,
hasPreview,
realPath,
favorite,
fileId,
];
final String href;
final int? contentLength;
final String? contentType;
final String? etag;
final DateTime? lastModified;
final List<JsonObj>? faceDetections;
// format currently unknown
final dynamic fileMetadataSize;
final bool? hasPreview;
final String? realPath;
final bool? favorite;
final int? fileId;
}
@toString
class Share with EquatableMixin {
const Share({

View file

@ -55,6 +55,20 @@ extension _$PersonToString on Person {
}
}
extension _$RecognizeFaceToString on RecognizeFace {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "RecognizeFace {href: $href}";
}
}
extension _$RecognizeFaceItemToString on RecognizeFaceItem {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "RecognizeFaceItem {href: $href, ${contentLength == null ? "" : "contentLength: $contentLength, "}${contentType == null ? "" : "contentType: $contentType, "}${etag == null ? "" : "etag: $etag, "}${lastModified == null ? "" : "lastModified: $lastModified, "}${faceDetections == null ? "" : "faceDetections: $faceDetections, "}fileMetadataSize: $fileMetadataSize, ${hasPreview == null ? "" : "hasPreview: $hasPreview, "}${realPath == null ? "" : "realPath: $realPath, "}${favorite == null ? "" : "favorite: $favorite, "}${fileId == null ? "" : "fileId: $fileId"}}";
}
}
extension _$ShareToString on Share {
String _$toString() {
// ignore: unnecessary_string_interpolations

View file

@ -0,0 +1,154 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:np_api/src/entity/entity.dart';
import 'package:np_api/src/entity/parser.dart';
import 'package:np_common/log.dart';
import 'package:np_common/type.dart';
import 'package:xml/xml.dart';
class RecognizeFaceItemParser extends XmlResponseParser {
Future<List<RecognizeFaceItem>> parse(String response) =>
compute(_parseRecognizeFaceItemsIsolate, response);
List<RecognizeFaceItem> _parse(XmlDocument xml) =>
parseT<RecognizeFaceItem>(xml, _toRecognizeFaceItem);
/// Map <DAV:response> contents to RecognizeFaceItem
RecognizeFaceItem _toRecognizeFaceItem(XmlElement element) {
String? href;
int? contentLength;
String? contentType;
String? etag;
DateTime? lastModified;
List<JsonObj>? faceDetections;
// format currently unknown
dynamic fileMetadataSize;
bool? hasPreview;
String? realPath;
bool? favorite;
int? fileId;
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("href",
prefix: "DAV:", namespaces: namespaces)) {
href = Uri.decodeComponent(child.innerText);
} else if (child.matchQualifiedName("propstat",
prefix: "DAV:", namespaces: namespaces)) {
final status = child.children
.whereType<XmlElement>()
.firstWhere((element) => element.matchQualifiedName("status",
prefix: "DAV:", namespaces: namespaces))
.innerText;
if (!status.contains(" 200 ")) {
continue;
}
final prop = child.children.whereType<XmlElement>().firstWhere(
(element) => element.matchQualifiedName("prop",
prefix: "DAV:", namespaces: namespaces));
final propParser = _PropParser(namespaces: namespaces);
propParser.parse(prop);
contentLength = propParser.contentLength;
contentType = propParser.contentType;
etag = propParser.etag;
lastModified = propParser.lastModified;
faceDetections = propParser.faceDetections;
fileMetadataSize = propParser.fileMetadataSize;
hasPreview = propParser.hasPreview;
realPath = propParser.realPath;
favorite = propParser.favorite;
fileId = propParser.fileId;
}
}
return RecognizeFaceItem(
href: href!,
contentLength: contentLength,
contentType: contentType,
etag: etag,
lastModified: lastModified,
faceDetections: faceDetections,
fileMetadataSize: fileMetadataSize,
hasPreview: hasPreview,
realPath: realPath,
favorite: favorite,
fileId: fileId,
);
}
}
class _PropParser {
_PropParser({
this.namespaces = const {},
});
/// Parse <DAV:prop> element contents
void parse(XmlElement element) {
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("getcontentlength",
prefix: "DAV:", namespaces: namespaces)) {
_contentLength = int.parse(child.innerText);
} else if (child.matchQualifiedName("getcontenttype",
prefix: "DAV:", namespaces: namespaces)) {
_contentType = child.innerText;
} else if (child.matchQualifiedName("getetag",
prefix: "DAV:", namespaces: namespaces)) {
_etag = child.innerText.replaceAll("\"", "");
} else if (child.matchQualifiedName("getlastmodified",
prefix: "DAV:", namespaces: namespaces)) {
_lastModified = HttpDate.parse(child.innerText);
} else if (child.matchQualifiedName("face-detections",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_faceDetections = child.innerText.isEmpty
? null
: (jsonDecode(child.innerText) as List).cast<JsonObj>();
} else if (child.matchQualifiedName("file-metadata-size",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_fileMetadataSize = child.innerText;
} else if (child.matchQualifiedName("has-preview",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_hasPreview = child.innerText == "true";
} else if (child.matchQualifiedName("realpath",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_realPath = child.innerText;
} else if (child.matchQualifiedName("favorite",
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
_favorite = child.innerText != "0";
} else if (child.matchQualifiedName("fileid",
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
_fileId = int.parse(child.innerText);
}
}
}
int? get contentLength => _contentLength;
String? get contentType => _contentType;
String? get etag => _etag;
DateTime? get lastModified => _lastModified;
List<JsonObj>? get faceDetections => _faceDetections;
dynamic get fileMetadataSize => _fileMetadataSize;
bool? get hasPreview => _hasPreview;
String? get realPath => _realPath;
bool? get favorite => _favorite;
int? get fileId => _fileId;
final Map<String, String> namespaces;
int? _contentLength;
String? _contentType;
String? _etag;
DateTime? _lastModified;
List<JsonObj>? _faceDetections;
dynamic _fileMetadataSize;
bool? _hasPreview;
String? _realPath;
bool? _favorite;
int? _fileId;
}
List<RecognizeFaceItem> _parseRecognizeFaceItemsIsolate(String response) {
initLog();
final xml = XmlDocument.parse(response);
return RecognizeFaceItemParser()._parse(xml);
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import 'package:np_api/src/entity/entity.dart';
import 'package:np_api/src/entity/parser.dart';
import 'package:np_common/log.dart';
import 'package:xml/xml.dart';
class RecognizeFaceParser extends XmlResponseParser {
Future<List<RecognizeFace>> parse(String response) =>
compute(_parseRecognizeFacesIsolate, response);
List<RecognizeFace> _parse(XmlDocument xml) =>
parseT<RecognizeFace>(xml, _toRecognizeFace);
/// Map <DAV:response> contents to RecognizeFace
RecognizeFace _toRecognizeFace(XmlElement element) {
String? href;
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("href",
prefix: "DAV:", namespaces: namespaces)) {
href = Uri.decodeComponent(child.innerText);
}
}
return RecognizeFace(
href: href!,
);
}
}
List<RecognizeFace> _parseRecognizeFacesIsolate(String response) {
initLog();
final xml = XmlDocument.parse(response);
return RecognizeFaceParser()._parse(xml);
}

View file

@ -0,0 +1,124 @@
part of 'api.dart';
class ApiRecognize {
const ApiRecognize(this.api, this.userId);
ApiRecognizeFaces faces() => ApiRecognizeFaces(this);
ApiRecognizeFace face(String name) => ApiRecognizeFace(this, name);
String get _path => "remote.php/dav/recognize/$userId";
final Api api;
final String userId;
}
@npLog
class ApiRecognizeFaces {
const ApiRecognizeFaces(this.recognize);
Future<Response> propfind() async {
final endpoint = "${recognize._path}/faces";
try {
return await api.request("PROPFIND", endpoint);
} catch (e) {
_log.severe("[propfind] Failed while propfind", e);
rethrow;
}
}
Api get api => recognize.api;
final ApiRecognize recognize;
}
@npLog
class ApiRecognizeFace {
const ApiRecognizeFace(this.recognize, this.name);
Future<Response> propfind({
getcontentlength,
getcontenttype,
getetag,
getlastmodified,
faceDetections,
fileMetadataSize,
hasPreview,
realpath,
favorite,
fileid,
}) async {
final endpoint = _path;
try {
final bool hasDavNs = (getcontentlength != null ||
getcontenttype != null ||
getetag != null ||
getlastmodified != null);
final bool hasNcNs = (faceDetections != null ||
fileMetadataSize != null ||
hasPreview != null ||
realpath != null);
final bool hasOcNs = (favorite != null || fileid != null);
if (!hasDavNs && !hasOcNs && !hasNcNs) {
// no body
return await api.request("PROPFIND", endpoint);
}
final namespaces = {
"DAV:": "d",
if (hasOcNs) "http://owncloud.org/ns": "oc",
if (hasNcNs) "http://nextcloud.org/ns": "nc",
};
final builder = XmlBuilder();
builder
..processing("xml", "version=\"1.0\"")
..element("d:propfind", namespaces: namespaces, nest: () {
builder.element("d:prop", nest: () {
if (getcontentlength != null) {
builder.element("d:getcontentlength");
}
if (getcontenttype != null) {
builder.element("d:getcontenttype");
}
if (getetag != null) {
builder.element("d:getetag");
}
if (getlastmodified != null) {
builder.element("d:getlastmodified");
}
if (faceDetections != null) {
builder.element("nc:face-detections");
}
if (fileMetadataSize != null) {
builder.element("nc:file-metadata-size");
}
if (hasPreview != null) {
builder.element("nc:has-preview");
}
if (realpath != null) {
builder.element("nc:realpath");
}
if (favorite != null) {
builder.element("oc:favorite");
}
if (fileid != null) {
builder.element("oc:fileid");
}
});
});
return await api.request(
"PROPFIND",
endpoint,
header: {"Content-Type": "application/xml"},
body: builder.buildDocument().toXmlString(),
);
} catch (e) {
_log.severe("[propfind] Failed while propfind", e);
rethrow;
}
}
String get _path => "${recognize._path}/faces/$name";
Api get api => recognize.api;
final ApiRecognize recognize;
final String name;
}

View file

@ -0,0 +1,154 @@
import 'package:np_api/np_api.dart';
import 'package:np_api/src/entity/recognize_face_item_parser.dart';
import 'package:test/test.dart';
void main() {
group("RecognizeFaceItemParser", () {
group("parse", () {
test("empty", _empty);
test("image", _image);
});
});
}
Future<void> _empty() async {
const xml = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:"
xmlns:s="http://sabredav.org/ns"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/test/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
<nc:face-detections/>
<nc:file-metadata-size/>
<nc:has-preview/>
<nc:realpath/>
<oc:favorite/>
<oc:fileid/>
<oc:permissions/>
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""";
final results = await RecognizeFaceItemParser().parse(xml);
expect(
results,
const [
RecognizeFaceItem(href: "/remote.php/dav/recognize/admin/faces/test/"),
],
);
}
Future<void> _image() async {
const xml = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:"
xmlns:s="http://sabredav.org/ns"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/test/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
<nc:face-detections/>
<nc:file-metadata-size/>
<nc:has-preview/>
<nc:realpath/>
<oc:favorite/>
<oc:fileid/>
<oc:permissions/>
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/test/test1.jpg</d:href>
<d:propstat>
<d:prop>
<d:getcontentlength>12345</d:getcontentlength>
<d:getcontenttype>image/jpeg</d:getcontenttype>
<d:getetag>00000000000000000000000000000000</d:getetag>
<d:getlastmodified>Sun, 1 Jan 2023 01:02:03 GMT</d:getlastmodified>
<d:resourcetype/>
<nc:face-detections>[{&quot;id&quot;:1,&quot;userId&quot;:&quot;test&quot;,&quot;fileId&quot;:2,&quot;x&quot;:0.5,&quot;y&quot;:0.5,&quot;height&quot;:0.1,&quot;width&quot;:0.1,&quot;vector&quot;:[-0.1,0.1,0.1,-0.01],&quot;clusterId&quot;:10,&quot;title&quot;:&quot;test&quot;}]</nc:face-detections>
<nc:file-metadata-size>[]</nc:file-metadata-size>
<nc:has-preview>true</nc:has-preview>
<nc:realpath>/admin/files/test1.jpg</nc:realpath>
<oc:favorite>0</oc:favorite>
<oc:fileid>2</oc:fileid>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<oc:permissions/>
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""";
final results = await RecognizeFaceItemParser().parse(xml);
expect(
results,
[
const RecognizeFaceItem(
href: "/remote.php/dav/recognize/admin/faces/test/"),
RecognizeFaceItem(
href: "/remote.php/dav/recognize/admin/faces/test/test1.jpg",
contentLength: 12345,
contentType: "image/jpeg",
etag: "00000000000000000000000000000000",
lastModified: DateTime.utc(2023, 1, 1, 1, 2, 3),
faceDetections: [
{
"id": 1,
"userId": "test",
"fileId": 2,
"x": 0.5,
"y": 0.5,
"height": 0.1,
"width": 0.1,
"vector": [-0.1, 0.1, 0.1, -0.01],
"clusterId": 10,
"title": "test",
},
],
fileMetadataSize: "[]",
hasPreview: true,
realPath: "/admin/files/test1.jpg",
favorite: false,
fileId: 2,
),
],
);
}

View file

@ -0,0 +1,115 @@
import 'package:np_api/np_api.dart';
import 'package:np_api/src/entity/recognize_face_parser.dart';
import 'package:test/test.dart';
void main() {
group("RecognizeFaceParser", () {
group("parse", () {
test("empty", _empty);
test("unnamed", _unnamed);
test("named", _named);
});
});
}
Future<void> _empty() async {
const xml = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""";
final results = await RecognizeFaceParser().parse(xml);
expect(
results,
const [
RecognizeFace(href: "/remote.php/dav/recognize/admin/faces/"),
],
);
}
Future<void> _unnamed() async {
const xml = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/10/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""";
final results = await RecognizeFaceParser().parse(xml);
expect(
results,
const [
RecognizeFace(href: "/remote.php/dav/recognize/admin/faces/"),
RecognizeFace(href: "/remote.php/dav/recognize/admin/faces/10/"),
],
);
}
Future<void> _named() async {
const xml = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/lovely%20face/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""";
final results = await RecognizeFaceParser().parse(xml);
expect(
results,
const [
RecognizeFace(href: "/remote.php/dav/recognize/admin/faces/"),
RecognizeFace(href: "/remote.php/dav/recognize/admin/faces/lovely face/"),
],
);
}