Refactoring

This commit is contained in:
Jeff Walter 2017-12-10 13:17:09 -06:00
parent 2589b868a7
commit 75454f08b3
15 changed files with 690 additions and 556 deletions

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,4 +1,8 @@
package arloclient package arlo
import (
"github.com/pkg/errors"
)
// Account is the account data. // Account is the account data.
type Account struct { type Account struct {
@ -16,3 +20,142 @@ type Account struct {
Arlo bool `json:"arlo"` Arlo bool `json:"arlo"`
DateCreated float64 `json:"dateCreated"` DateCreated float64 `json:"dateCreated"`
} }
type Friend struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Devices DeviceOrder `json:"devices"`
LastModified float64 `json:"lastModified"`
AdminUser bool `json:"adminUser"`
Email string `json:"email"`
Id string `json:"id"`
}
func Login(user string, pass string) (*Arlo, error) {
a := newArlo(user, pass)
body := map[string]string{"email": a.user, "password": a.pass}
resp, err := a.client.Post(LoginUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "login request failed")
}
var loginResponse LoginResponse
if err := resp.Decode(&loginResponse); err != nil {
return nil, err
}
if loginResponse.Success {
// Cache the auth token.
a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token)
// Save the account info with the Arlo struct.
a.Account = &loginResponse.Data
if deviceResponse, err := a.GetDevices(); err != nil {
return nil, err
} else {
if !deviceResponse.Success {
return nil, err
}
// Cache the devices as their respective types.
a.Basestations = deviceResponse.Data.Basestations()
a.Cameras = deviceResponse.Data.Cameras()
// Set the XCloudId header for future requests. You can override this on a per-request basis if needed.
a.client.BaseHttpHeader.Add("xCloudId", deviceResponse.Data[0].XCloudId)
}
} else {
return nil, errors.New("failed to login")
}
return a, nil
}
func (a *Arlo) Logout() (*Status, error) {
resp, err := a.client.Put(LogoutUri, nil, nil)
if err != nil {
return nil, errors.WithMessage(err, "logout request failed")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
// UpdateProfile takes a first and last name, and updates the user profile with that information.
func (a *Arlo) UpdateProfile(firstName, lastName string) (*Status, error) {
body := map[string]string{"firstName": firstName, "lastName": lastName}
resp, err := a.client.Put(UserProfileUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update profile")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
func (a *Arlo) UpdatePassword(pass string) (*Status, error) {
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
resp, err := a.client.Post(UserChangePasswordUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update password")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
if status.Success {
a.pass = pass
}
return &status, nil
}
/*
This is an example of the json you would pass in the body to UpdateFriends():
{
"firstName":"Some",
"lastName":"Body",
"devices":{
"XXXXXXXXXXXXX":"Camera 1",
"XXXXXXXXXXXXX":"Camera 2 ",
"XXXXXXXXXXXXX":"Camera 3"
},
"lastModified":1463977440911,
"adminUser":true,
"email":"user@example.com",
"id":"XXX-XXXXXXX"
}
*/
func (a *Arlo) UpdateFriends(f Friend) (*Status, error) {
resp, err := a.client.Put(UserFriendsUri, f, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update friends")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}

56
arlo.go Normal file
View File

@ -0,0 +1,56 @@
package arlo
import (
"github.com/jeffreydwalter/arlo/internal/request"
)
type Arlo struct {
user string
pass string
client *request.Client
Account *Account
Basestations *Basestations
Cameras *Cameras
}
func newArlo(user string, pass string) *Arlo {
c, _ := request.NewClient(BaseUrl)
arlo := &Arlo{
user: user,
pass: pass,
client: c,
}
return arlo
}
/*
##
# This function causes the camera to record a snapshot.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) TakeSnapshot(camera):
stream_url = self.StartStream(camera)
a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
return stream_url;
##
# This function causes the camera to start recording.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) StartRecording(camera):
stream_url = self.StartStream(camera)
a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
return stream_url
##
# This function causes the camera to stop recording.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) StopRecording(camera):
return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
*/

1
arlo_test.go Normal file
View File

@ -0,0 +1 @@
package arlo

View File

@ -1,338 +0,0 @@
package arloclient
import (
"time"
"github.com/jeffreydwalter/arloclient/internal/request"
"github.com/jeffreydwalter/arloclient/internal/util"
"github.com/pkg/errors"
)
type Arlo struct {
user string
pass string
client *request.Client
Account *Account
Devices *Devices
}
func newArlo(user string, pass string) *Arlo {
c, _ := request.NewClient(BaseUrl)
arlo := &Arlo{
user: user,
pass: pass,
client: c,
}
return arlo
}
func Login(user string, pass string) (*Arlo, error) {
a := newArlo(user, pass)
body := map[string]string{"email": a.user, "password": a.pass}
resp, err := a.client.Post(LoginUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "login request failed")
}
var loginResponse LoginResponse
if err := util.Decode(resp.ParsedBody, &loginResponse); err != nil {
return nil, err
}
if loginResponse.Success {
// Cache the auth token.
a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token)
// Save the account info with the Arlo struct.
a.Account = &loginResponse.Data
if deviceResponse, err := a.GetDevices(); err != nil {
return nil, err
} else {
if !deviceResponse.Success {
return nil, err
}
a.Devices = &deviceResponse.Data
}
} else {
return nil, errors.New("failed to login")
}
return a, nil
}
func (a *Arlo) Logout() (*Status, error) {
resp, err := a.client.Put(LogoutUri, nil, nil)
if err != nil {
return nil, errors.WithMessage(err, "logout request failed")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
return &status, nil
}
func (a *Arlo) GetDevices() (*DeviceResponse, error) {
resp, err := a.client.Get(DevicesUri, nil)
if err != nil {
return nil, errors.WithMessage(err, "get devices request failed")
}
var deviceResponse DeviceResponse
if err := util.Decode(resp.ParsedBody, &deviceResponse); err != nil {
return nil, err
}
return &deviceResponse, nil
}
func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (*LibraryMetaDataResponse, error) {
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.client.Post(LibraryMetadataUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get library metadata")
}
var libraryMetaDataResponse LibraryMetaDataResponse
if err := util.Decode(resp.ParsedBody, &libraryMetaDataResponse); err != nil {
return nil, err
}
return &libraryMetaDataResponse, nil
}
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) {
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.client.Post(LibraryUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get library")
}
var libraryResponse LibraryResponse
if err := util.Decode(resp.ParsedBody, &libraryResponse); err != nil {
return nil, err
}
return &libraryResponse, nil
}
func (a *Arlo) UpdateDeviceName(d Device, name string) (*Status, error) {
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
resp, err := a.client.Put(DeviceRenameUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update device name")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
return &status, nil
return nil, errors.New("Device not found")
}
// UpdateProfile takes a first and last name, and updates the user profile with that information.
func (a *Arlo) UpdateProfile(firstName, lastName string) (*Status, error) {
body := map[string]string{"firstName": firstName, "lastName": lastName}
resp, err := a.client.Put(UserProfileUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update profile")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
return &status, nil
}
func (a *Arlo) UpdatePassword(password string) (*Status, error) {
body := map[string]string{"currentPassword": a.pass, "newPassword": password}
resp, err := a.client.Post(UserChangePasswordUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update password")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
if status.Success {
a.pass = password
}
return &status, nil
}
/*
This is an example of the json you would pass in the body to UpdateFriends():
{
"firstName":"Some",
"lastName":"Body",
"devices":{
"XXXXXXXXXXXXX":"Camera 1",
"XXXXXXXXXXXXX":"Camera 2 ",
"XXXXXXXXXXXXX":"Camera 3"
},
"lastModified":1463977440911,
"adminUser":true,
"email":"user@example.com",
"id":"XXX-XXXXXXX"
}
*/
func (a *Arlo) UpdateFriends(f Friend) (*Status, error) {
resp, err := a.client.Put(UserFriendsUri, f, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update friends")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
return &status, nil
}
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) (*Status, error) {
resp, err := a.client.Post(DeviceDisplayOrderUri, d, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update display order")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
return &status, nil
}
/*
##
# Delete a single video recording from Arlo.
#
# All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call.
#
##
*/
func (a *Arlo) DeleteRecording(r *Recording) (*Status, error) {
body := map[string]map[string]interface{}{"data": {"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}}
resp, err := a.client.Post(LibraryRecycleUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to delete recording")
}
var status Status
if err := util.Decode(resp.ParsedBody, &status); err != nil {
return nil, err
}
return &status, nil
}
/*
##
# Delete a batch of video recordings from Arlo.
#
# The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for.
# If you want to delete some other batch of videos, then you need to send an array of objects representing each video you want to delete.
#
#[
# {
# "createdDate":"20160904",
# "utcCreatedDate":1473010280395,
# "deviceId":"XXXXXXXXXXXXX"
# },
# {
# "createdDate":"20160904",
# "utcCreatedDate":1473010280395,
# "deviceId":"XXXXXXXXXXXXX"
# }
#]
##
func (a *Arlo) BatchDeleteRecordings(recording_metadata):
return a.client.Post('https://arlo.netgear.com/hmsweb/users/library/recycle', {'data':recording_metadata})
##
# Returns the whole video from the presignedContentUrl.
#
# Obviously, this function is generic and could be used to download anything. :)
##
func (a *Arlo) GetRecording(url, chunk_size=4096):
video = ''
r = requests.get(url, stream=True)
r.raise_for_status()
for chunk in r.iter_content(chunk_size):
if chunk: video += chunk
return video
##
# This function returns a json object containing the rtmps url to the requested video stream.
# You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp
#
# The request to /users/devices/startStream returns:
#{ "url":"rtmps://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" }
#
##
func (a *Arlo) StartStream(camera):
return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startStream', {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')})
##
# This function causes the camera to record a snapshot.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) TakeSnapshot(camera):
stream_url = self.StartStream(camera)
a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
return stream_url;
##
# This function causes the camera to start recording.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) StartRecording(camera):
stream_url = self.StartStream(camera)
a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
return stream_url
##
# This function causes the camera to stop recording.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) StopRecording(camera):
return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
*/

View File

@ -1 +0,0 @@
package arloclient

View File

@ -1,4 +1,4 @@
package arloclient package arlo
const ( const (
BaseUrl = "https://arlo.netgear.com/hmsweb" BaseUrl = "https://arlo.netgear.com/hmsweb"

View File

@ -1,6 +1,10 @@
package arloclient package arlo
// Device is the device data. import (
"github.com/pkg/errors"
)
// A Device is the device data, this can be a camera, basestation, arloq, etc.
type Device struct { type Device struct {
DeviceType string `json:"deviceType"` DeviceType string `json:"deviceType"`
XCloudId string `json:"xCloudId"` XCloudId string `json:"xCloudId"`
@ -26,7 +30,22 @@ type Device struct {
// Devices is an array of Device objects. // Devices is an array of Device objects.
type Devices []Device type Devices []Device
// DeviceOrder is a hash of # XXXXXXXXXXXXX is the device id of each camera. You can get this from GetDevices(). // A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
type Basestation Device
// Basestations is an array of Basestation objects.
type Basestations []Basestation
// A Camera is a Device of type "camera".
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
type Camera Device
// Cameras is an array of Camera objects.
type Cameras []Camera
// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order.
// Device order is mainly used by the UI to determine which order to show the devices.
/* /*
{ {
"devices":{ "devices":{
@ -36,9 +55,10 @@ type Devices []Device
} }
*/ */
type DeviceOrder struct { type DeviceOrder struct {
Devices map[string]int Devices map[string]int `json:"devices"`
} }
// Find returns a device with the device id passed in.
func (ds *Devices) Find(deviceId string) *Device { func (ds *Devices) Find(deviceId string) *Device {
for _, d := range *ds { for _, d := range *ds {
if d.DeviceId == deviceId { if d.DeviceId == deviceId {
@ -49,22 +69,120 @@ func (ds *Devices) Find(deviceId string) *Device {
return nil return nil
} }
func (ds *Devices) BaseStations() *Devices { // Basestations returns a Basestations object containing all devices that are NOT type "camera".
var basestations Devices // I did this because some device types, like arloq, don't have a basestation.
// So, when interacting with them you must treat them like a basestation and a camera.
// Cameras also includes decvices of this type, so you can get the same data there or cast.
func (ds *Devices) Basestations() *Basestations {
var basestations Basestations
for _, d := range *ds { for _, d := range *ds {
if d.DeviceType == "basestation" { if d.DeviceType != "camera" {
basestations = append(basestations, d) basestations = append(basestations, Basestation(d))
} }
} }
return &basestations return &basestations
} }
func (ds *Devices) Cameras() *Devices { // Cameras returns a Cameras object containing all devices that are of type "camera".
var cameras Devices // I did this because some device types, like arloq, don't have a basestation.
// So, when interacting with them you must treat them like a basestation and a camera.
// Basestations also includes decvices of this type, so you can get the same data there or cast.
func (ds *Devices) Cameras() *Cameras {
var cameras Cameras
for _, d := range *ds { for _, d := range *ds {
if d.DeviceType != "basestation" { if d.DeviceType != "basestation" {
cameras = append(cameras, d) cameras = append(cameras, Camera(d))
} }
} }
return &cameras return &cameras
} }
// GetDevices returns an array of all devices.
// When you call Login, this method is called and all devices are cached in the Arlo object.
func (a *Arlo) GetDevices() (*DeviceResponse, error) {
resp, err := a.client.Get(DevicesUri, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get devices")
}
var deviceResponse DeviceResponse
if err := resp.Decode(&deviceResponse); err != nil {
return nil, err
}
if len(deviceResponse.Data) == 0 {
return nil, errors.New("no devices found")
}
return &deviceResponse, nil
}
// UpdateDeviceName sets the name of the given device to the name argument.
func (a *Arlo) UpdateDeviceName(d Device, name string) (*Status, error) {
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
resp, err := a.client.Put(DeviceRenameUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update device name")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
return nil, errors.New("device not found")
}
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) (*Status, error) {
resp, err := a.client.Post(DeviceDisplayOrderUri, d, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update display order")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
// StartStream returns a json object containing the rtmps url to the requested video stream.
// You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp
//
// The request to /users/devices/startStream returns:
// NOTE: { "url":"rtmps://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" }
func (a *Arlo) StartStream(c Camera) (*StartStreamResponse, error) {
var n Notification
n.To = c.ParentId
n.From = c.UserId
n.Resource = "cameras/" + c.DeviceId
n.Action = "set"
n.PublishResponse = true
n.TransId = ""
n.Properties.ActivityState = "startUserStream"
n.Properties.CameraId = c.DeviceId
// {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')}
resp, err := a.client.Post(DeviceStartStreamUri, n, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to start stream")
}
var startStreamResponse StartStreamResponse
if err := resp.Decode(&startStreamResponse); err != nil {
return nil, err
}
return &startStreamResponse, nil
}

131
internal/request/client.go Normal file
View File

@ -0,0 +1,131 @@
package request
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"github.com/pkg/errors"
)
type Client struct {
BaseURL *url.URL
BaseHttpHeader http.Header
httpClient http.Client
}
func NewClient(baseurl string) (*Client, error) {
var err error
var jar *cookiejar.Jar
options := cookiejar.Options{}
if jar, err = cookiejar.New(&options); err != nil {
return nil, errors.Wrap(err, "failed to create client object")
}
var u *url.URL
if u, err = url.Parse(baseurl); err != nil {
return nil, errors.Wrap(err, "failed to create client object")
}
header := make(http.Header)
header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36")
header.Add("Content-Type", "application/json")
header.Add("Accept", "application/json")
return &Client{
BaseURL: u,
BaseHttpHeader: header,
httpClient: http.Client{Jar: jar},
}, nil
}
func (c *Client) Get(uri string, header http.Header) (*Response, error) {
req, err := c.newRequest("GET", uri, nil, header)
if err != nil {
return nil, errors.WithMessage(err, "get request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) Post(uri string, body interface{}, header http.Header) (*Response, error) {
req, err := c.newRequest("POST", uri, body, header)
if err != nil {
return nil, errors.WithMessage(err, "post request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) Put(uri string, body interface{}, header http.Header) (*Response, error) {
req, err := c.newRequest("PUT", uri, body, header)
if err != nil {
return nil, errors.WithMessage(err, "put request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) newRequest(method string, uri string, body interface{}, header http.Header) (*Request, error) {
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, errors.Wrap(err, "failed to create request object")
}
}
log.Printf("JSON: %v", buf)
u := c.BaseURL.String() + uri
req, err := http.NewRequest(method, u, buf)
if err != nil {
return nil, errors.Wrap(err, "failed to create request object")
}
for k, v := range c.BaseHttpHeader {
for _, h := range v {
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h)
}
}
for k, v := range header {
for _, h := range v {
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h)
}
}
return &Request{
Request: *req,
}, nil
}
func (c *Client) newResponse(resp *http.Response) (*Response, error) {
return &Response{
Response: *resp,
}, nil
}
func (c *Client) do(req *Request) (*Response, error) {
//fmt.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.httpClient.Jar.Cookies(req.URL))
//fmt.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
resp, err := c.httpClient.Do(&req.Request)
if err != nil {
return nil, errors.Wrap(err, "failed to execute http request")
}
if resp.StatusCode >= 400 {
defer resp.Body.Close()
return nil, errors.New("http request failed with status: " + resp.Status)
}
return c.newResponse(resp)
}

View File

@ -1,168 +1,9 @@
package request package request
import ( import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"log"
"mime"
"net/http" "net/http"
"net/http/cookiejar"
"net/url"
"github.com/pkg/errors"
) )
type Client struct {
BaseURL *url.URL
BaseHttpHeader http.Header
httpClient http.Client
}
type Request struct { type Request struct {
http.Request http.Request
} }
type Response struct {
http.Response
ParsedBody interface{}
}
func NewClient(baseurl string) (*Client, error) {
var err error
var jar *cookiejar.Jar
options := cookiejar.Options{}
if jar, err = cookiejar.New(&options); err != nil {
return nil, errors.Wrap(err, "failed to create client object")
}
var u *url.URL
if u, err = url.Parse(baseurl); err != nil {
return nil, errors.Wrap(err, "failed to create client object")
}
header := make(http.Header)
header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36")
header.Add("Content-Type", "application/json")
header.Add("Accept", "application/json")
return &Client{
BaseURL: u,
BaseHttpHeader: header,
httpClient: http.Client{Jar: jar},
}, nil
}
func (c *Client) Get(uri string, header http.Header) (*Response, error) {
req, err := c.newRequest("GET", uri, nil, header)
if err != nil {
return nil, errors.WithMessage(err, "get request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) Post(uri string, body interface{}, header http.Header) (*Response, error) {
req, err := c.newRequest("POST", uri, body, header)
if err != nil {
return nil, errors.WithMessage(err, "post request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) Put(uri string, body interface{}, header http.Header) (*Response, error) {
req, err := c.newRequest("PUT", uri, body, header)
if err != nil {
return nil, errors.WithMessage(err, "put request "+uri+" failed")
}
return c.do(req)
}
func GetContentType(ct string) (string, error) {
mediaType, _, err := mime.ParseMediaType(ct)
if err != nil {
return "", errors.Wrap(err, "failed to get content type")
}
return mediaType, nil
}
func (c *Client) newRequest(method string, uri string, body interface{}, header http.Header) (*Request, error) {
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, errors.Wrap(err, "failed to create request object")
}
}
log.Printf("JSON: %v", buf)
u := c.BaseURL.String() + uri
req, err := http.NewRequest(method, u, buf)
if err != nil {
return nil, errors.Wrap(err, "failed to create request object")
}
for k, v := range c.BaseHttpHeader {
for _, h := range v {
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h)
}
}
for k, v := range header {
for _, h := range v {
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h)
}
}
return &Request{
Request: *req,
}, nil
}
func (c *Client) newResponse(resp *http.Response) (*Response, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// log.Printf("DATA: %v", string(data))
mediaType, err := GetContentType(resp.Header.Get("Content-Type"))
if err != nil {
return nil, errors.WithMessage(err, "failed to create response object")
}
var pb interface{}
switch mediaType {
case "application/json":
err = json.Unmarshal([]byte(body), &pb)
if err != nil {
return nil, errors.Wrap(err, "failed to create response object")
}
}
return &Response{
Response: *resp,
ParsedBody: pb,
}, nil
}
func (c *Client) do(req *Request) (*Response, error) {
//fmt.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.httpClient.Jar.Cookies(req.URL))
//fmt.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
resp, err := c.httpClient.Do(&req.Request)
if err != nil {
return nil, errors.Wrap(err, "failed to execute http request")
}
defer resp.Body.Close()
return c.newResponse(resp)
}

View File

@ -0,0 +1,72 @@
package request
import (
"encoding/json"
"io"
"log"
"mime"
"net/http"
"os"
"reflect"
"github.com/pkg/errors"
)
type Response struct {
http.Response
}
func (resp *Response) GetContentType() (string, error) {
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return "", errors.Wrap(err, "failed to get content type")
}
return mediaType, nil
}
func (resp *Response) Decode(s interface{}) error {
defer resp.Body.Close()
mediaType, err := resp.GetContentType()
if err != nil {
return errors.WithMessage(err, "failed to decode response body")
}
switch mediaType {
case "application/json":
err := json.NewDecoder(resp.Body).Decode(&s)
if err != nil {
return errors.Wrap(err, "failed to create "+reflect.TypeOf(s).String()+" object")
}
default:
return errors.New("unsupported content type: " + mediaType)
}
return nil
}
func (resp *Response) Download(to string) (error, int64) {
defer resp.Body.Close()
// Create output file
newFile, err := os.Create(to)
if err != nil {
log.Fatal(err)
}
defer newFile.Close()
// Write bytes from HTTP response to file.
// response.Body satisfies the reader interface.
// newFile satisfies the writer interface.
// That allows us to use io.Copy which accepts
// any type that implements reader and writer interface
bytesWritten, err := io.Copy(newFile, resp.Body)
if err != nil {
log.Fatal(err)
}
return nil, bytesWritten
}

View File

@ -3,10 +3,6 @@ package util
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
) )
func PrettyPrint(data interface{}) string { func PrettyPrint(data interface{}) string {
@ -17,27 +13,13 @@ func PrettyPrint(data interface{}) string {
return fmt.Sprint(string(j)) return fmt.Sprint(string(j))
} }
/* const TransIdPrefix = "web"
type Timestamp time.Time
func (t *Timestamp) MarshalJSON() ([]byte, error) { func GenTransId(transType string) {
ts := time.Time(*t).Unix() /*
stamp := fmt.Sprint(ts) func divmod(numerator, denominator int64) (quotient, remainder int64) {
return []byte(stamp), nil quotient = numerator / denominator // integer division, decimals are truncated
} remainder = numerator % denominator
func (t *Timestamp) UnmarshalJSON(b []byte) error { return
ts, err := strconv.Atoi(string(b)) }*/
if err != nil {
return err
}
*t = Timestamp(time.Unix(int64(ts), 0))
return nil
}
*/
func Decode(b interface{}, s interface{}) error {
if err := mapstructure.Decode(b, s); err != nil {
return errors.Wrap(err, "failed to create "+reflect.TypeOf(s).String()+" object")
}
return nil
} }

View File

@ -1,4 +1,10 @@
package arloclient package arlo
import (
"time"
"github.com/pkg/errors"
)
// LibraryMetaData is the library meta data. // LibraryMetaData is the library meta data.
type LibraryMetaData struct { type LibraryMetaData struct {
@ -29,3 +35,82 @@ type Recording struct {
} }
type Library []Recording type Library []Recording
func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (*LibraryMetaDataResponse, error) {
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.client.Post(LibraryMetadataUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get library metadata")
}
var libraryMetaDataResponse LibraryMetaDataResponse
if err := resp.Decode(&libraryMetaDataResponse); err != nil {
return nil, err
}
return &libraryMetaDataResponse, nil
}
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) {
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.client.Post(LibraryUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get library")
}
var libraryResponse LibraryResponse
if err := resp.Decode(&libraryResponse); err != nil {
return nil, err
}
return &libraryResponse, nil
}
/*
Delete a single video recording from Arlo.
All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call.
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
*/
func (a *Arlo) DeleteRecording(r Recording) (*Status, error) {
body := map[string]Library{"data": {r}}
resp, err := a.client.Post(LibraryRecycleUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to delete recording")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
/*
Delete a batch of video recordings from Arlo.
The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for.
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
*/
func (a *Arlo) BatchDeleteRecordings(l Library) (*Status, error) {
body := map[string]Library{"data": l}
resp, err := a.client.Post(LibraryRecycleUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to delete recordings")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}

View File

@ -1,4 +1,4 @@
package arloclient package arlo
// UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call. // UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call.
type Status struct { type Status struct {
@ -7,23 +7,28 @@ type Status struct {
// LoginResponse is an intermediate struct used when parsing data from the Login() call. // LoginResponse is an intermediate struct used when parsing data from the Login() call.
type LoginResponse struct { type LoginResponse struct {
Data Account Data Account
Success bool `json:"success"` *Status
} }
// DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call. // DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call.
type DeviceResponse struct { type DeviceResponse struct {
Data Devices Data Devices
Success bool `json:"success"` *Status
} }
// LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call. // LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call.
type LibraryMetaDataResponse struct { type LibraryMetaDataResponse struct {
Data LibraryMetaData Data LibraryMetaData
Success bool `json:"success"` *Status
} }
type LibraryResponse struct { type LibraryResponse struct {
Data Library Data Library
Success bool `json:"success"` *Status
}
type StartStreamResponse struct {
Data StreamUrl
*Status
} }

View File

@ -1,4 +1,4 @@
package arloclient package arlo
/* /*
// Credentials is the login credential data. // Credentials is the login credential data.
@ -45,12 +45,45 @@ type Favorite struct {
Favorite uint8 `json:"Favorite"` Favorite uint8 `json:"Favorite"`
} }
type Friend struct { /*
FirstName string `json:"firstName"` type Device struct {
LastName string `json:"lastName"` DeviceType string `json:"deviceType"`
Devices DeviceOrder `json:"devices"` XCloudId string `json:"xCloudId"`
LastModified float64 `json:"lastModified"` DisplayOrder uint8 `json:"displayOrder"`
AdminUser bool `json:"adminUser"` State string `json:"state"`
Email string `json:"email"` ModelId string `json:"modelId"`
Id string `json:"id"` InterfaceVersion string `json:"interfaceVersion"`
ParentId string `json:"parentId"`
UserId string `json:"userId"`
DeviceName string `json:"deviceName"`
FirmwareVersion string `json:"firmwareVersion"`
MediaObjectCount uint8 `json:"mediaObjectCount"`
DateCreated float64 `json:"dateCreated"`
Owner Owner `json:"owner"`
Properties Properties `json:"properties"`
UniqueId string `json:"uniqueId"`
LastModified float64 `json:"lastModified"`
UserRole string `json:"userRole"`
InterfaceSchemaVer string `json:"interfaceSchemaVer"`
DeviceId string `json:"deviceId"`
}
*/
type StreamUrl struct {
Url string `json:"url"`
}
type NotificationProperties struct {
ActivityState string `json:"activityState"`
CameraId string `json:"cameraId"`
}
type Notification struct {
To string `json:"to"`
From string `json:"from"`
Resource string `json:"resource"`
Action string `json:"action"`
PublishResponse bool `json:"publishResourcec"`
TransId string `json:"transId"`
Properties NotificationProperties `json:"properties"`
} }