Skip to content

Commit

Permalink
fix: events parsing from the server (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdlukaa authored Aug 14, 2023
2 parents 5b3bb3f + 792f357 commit 5a6831a
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 199 deletions.
195 changes: 3 additions & 192 deletions lib/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
import 'dart:convert';

import 'package:bluecherry_client/api/api_helpers.dart';
import 'package:bluecherry_client/api/ptz.dart';
import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/utils/constants.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:xml2json/xml2json.dart';

export 'events.dart';
export 'ptz.dart';

class API {
static final API instance = API();

Expand Down Expand Up @@ -138,90 +138,6 @@ class API {
return null;
}

/// Gets the [Event]s present on the [server].
///
/// If server is offline, then it returns an empty list.
Future<Iterable<Event>> getEvents(Server server) async {
if (!server.online) {
debugPrint('Can not get events of an offline server: $server');
return [];
}

return compute(_getEvents, server);
}

static Future<Iterable<Event>> _getEvents(Server server) async {
if (!server.online) {
debugPrint('Can not get events of an offline server: $server');
return [];
}

DevHttpOverrides.configureCertificates();

assert(server.serverUUID != null && server.cookie != null);
final response = await get(
Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/events/',
{
'XML': '1',
'limit': '$kEventsLimit',
},
),
headers: {
'Cookie': server.cookie!,
},
);

try {
debugPrint('Getting events for server ${server.name}');
// debugPrint(response.body);

final parser = Xml2Json()..parse(response.body);
final events = (jsonDecode(parser.toGData())['feed']['entry'] as List)
.map((e) {
if (!e.containsKey('content')) debugPrint(e.toString());
return Event(
server,
int.parse(e['id']['raw']),
int.parse((e['category']['term'] as String).split('/').first),
e['title']['\$t'],
e['published'] == null || e['published']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['published']['\$t']).toLocal(),
e['updated'] == null || e['updated']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['updated']['\$t']).toLocal(),
e['category']['term'],
!e.containsKey('content')
? null
: int.parse(e['content']['media_id']),
!e.containsKey('content')
? null
: Uri.parse(
e['content'][r'$t'].replaceAll(
'https://',
'https://${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@',
),
),
);
})
.where((e) => e.duration > const Duration(minutes: 1))
.cast<Event>();

debugPrint('Loaded ${events.length} events for server ${server.name}');
return events;
} on Xml2JsonException {
debugPrint('Failed to parse XML response from server $server');
debugPrint(response.body);
} catch (exception, stacktrace) {
debugPrint('Failed to getEvents on server $server');
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
return <Event>[];
}

/// Returns the notification API endpoint.
///
/// Returns the endpoint as [String] if the request was successful, otherwise returns `null`.
Expand Down Expand Up @@ -323,109 +239,4 @@ class API {
return false;
}
}

/// * <https://bluecherry-apps.readthedocs.io/en/latest/development.html#controlling-ptz-cameras>
Future<bool> ptz({
required Device device,
required Movement movement,
PTZCommand command = PTZCommand.move,
int panSpeed = 1,
int tiltSpeed = 1,
int duration = 250,
}) async {
if (!device.hasPTZ) return false;

final server = device.server;

final url = Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/media/ptz.php',
{
'id': '${device.id}',
'command': command.name,

// commands
if (movement == Movement.moveNorth)
'tilt': 'u' //up
else if (movement == Movement.moveSouth)
'tilt': 'd' //down
else if (movement == Movement.moveWest)
'pan': 'l' //left
else if (movement == Movement.moveEast)
'pan': 'r' //right
else if (movement == Movement.moveWide)
'zoom': 'w' //wide
else if (movement == Movement.moveTele)
'zoom': 't', //tight

// speeds
if (command == PTZCommand.move) ...{
if (panSpeed > 0) 'panspeed': '$panSpeed',
if (tiltSpeed > 0) 'tiltspeed': '$tiltSpeed',
if (duration >= -1) 'duration': '$duration',
},
},
);

debugPrint(url.toString());

final response = await get(
url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': server.cookie!,
},
);

debugPrint('${command.name} ${response.statusCode}');

if (response.statusCode == 200) {
return true;
}

return false;
}

/// * <https://bluecherry-apps.readthedocs.io/en/latest/development.html#controlling-ptz-cameras>
Future<bool> presets({
required Device device,
required PresetCommand command,
String? presetId,
String? presetName,
}) async {
if (!device.hasPTZ) return false;

final server = device.server;

assert(presetName != null || command != PresetCommand.save);

final url = Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/media/ptz.php',
{
'id': '${device.id}',
'command': command.name,
if (presetId != null) 'preset': presetId,
if (presetName != null) 'name': presetName,
},
);

debugPrint(url.toString());

final response = await get(
url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': server.cookie!,
},
);

debugPrint('${command.name} ${response.body} ${response.statusCode}');

if (response.statusCode == 200) {
return true;
}

return false;
}
}
142 changes: 142 additions & 0 deletions lib/api/events.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import 'dart:convert';

import 'package:bluecherry_client/api/api.dart';
import 'package:bluecherry_client/api/api_helpers.dart';
import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/utils/constants.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:xml2json/xml2json.dart';

extension EventsExtension on API {
/// Gets the [Event]s present on the [server].
///
/// If server is offline, then it returns an empty list.
Future<Iterable<Event>> getEvents(Server server) async {
if (!server.online) {
debugPrint('Can not get events of an offline server: $server');
return [];
}

return compute(_getEvents, server);
}

static Future<Iterable<Event>> _getEvents(Server server) async {
if (!server.online) {
debugPrint('Can not get events of an offline server: $server');
return [];
}

DevHttpOverrides.configureCertificates();

assert(server.serverUUID != null && server.cookie != null);
final response = await http.get(
Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/events/',
{
'XML': '1',
'limit': '$kEventsLimit',
},
),
headers: {
'Cookie': server.cookie!,
},
);

Iterable<Event> events = <Event>[];

try {
debugPrint('Getting events for server ${server.name}');

final parser = Xml2Json()..parse(response.body);
events = (jsonDecode(parser.toGData())['feed']['entry'] as List)
.map((e) {
if (!e.containsKey('content')) debugPrint(e.toString());
return Event(
server,
int.parse(e['id']['raw']),
int.parse((e['category']['term'] as String).split('/').first),
e['title']['\$t'],
e['published'] == null || e['published']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['published']['\$t']).toLocal(),
e['updated'] == null || e['updated']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['updated']['\$t']).toLocal(),
e['category']['term'],
!e.containsKey('content')
? null
: int.parse(e['content']['media_id']),
!e.containsKey('content')
? null
: Uri.parse(
e['content'][r'$t'].replaceAll(
'https://',
'https://${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@',
),
),
);
})
.where((e) => e.duration > const Duration(minutes: 1))
.cast<Event>();
} on Xml2JsonException {
debugPrint('Failed to parse XML response from server ${server.name}');
debugPrint('Attempting to parse using JSON');

events = ((jsonDecode(response.body) as Map)['entry'] as List)
.cast<Map>()
.map((eventObject) {
final published = DateTime.parse(eventObject['published']).toLocal();
final event = Event.factory(
server: server,
id: () {
final idObject = eventObject['id'].toString();

if (int.tryParse(idObject) != null) {
return int.parse(idObject);
}

final parts = idObject.split('id=');
if (parts.isEmpty) {
return -1;
}

return int.parse(parts.last);
}(),
deviceID: int.parse(
(eventObject['category']['term'] as String?)?.split('/').first ??
'-1',
),
title: eventObject['title'],
published: published,
updated: eventObject['updated'] == null
? published
: DateTime.parse(eventObject['updated']).toLocal(),
category: eventObject['category']['term'],
mediaID: eventObject.containsKey('content')
? int.parse(eventObject['content']['media_id'])
: null,
mediaURL: eventObject.containsKey('content')
? Uri.parse(
(eventObject['content']['content'] as String).replaceAll(
'https://',
'https://${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@',
),
)
: null,
);
return event;
});
} catch (exception, stacktrace) {
debugPrint('Failed to getEvents on server ${server.name}');
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}

debugPrint('Loaded ${events.length} events for server ${server.name}');

return events;
}
}
Loading

0 comments on commit 5a6831a

Please sign in to comment.