From 648de10f105ec85d964ce0de3596755c7a1ab2c1 Mon Sep 17 00:00:00 2001 From: Jeff Walter Date: Sat, 22 Sep 2018 14:22:42 -0500 Subject: [PATCH] More cleanup. Added copyright notice. --- Gopkg.lock | 6 +- arlo.go | 51 +++++++---- arlo_test.go | 16 ++++ arlobaby.go | 16 ++++ basestation.go | 18 +++- camera.go | 173 +++++++++++++++++++---------------- const.go | 20 +++- devices.go | 34 +++++-- events_stream.go | 18 +++- internal/request/client.go | 53 ++++++++--- internal/request/request.go | 16 ++++ internal/request/response.go | 16 ++++ internal/util/util.go | 16 ++++ library.go | 68 ++++++++++---- responses.go | 20 +++- types.go | 28 ++++-- util.go | 76 ++++++++------- 17 files changed, 465 insertions(+), 180 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 1cb5a7d..d2d90c5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,12 +2,12 @@ [[projects]] - digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca" + branch = "master" + digest = "1:4a28080faacdaabfe54a78ba0bfb41468744cbcda6af15748b917ca725d3749c" name = "github.com/pkg/errors" packages = ["."] pruneopts = "" - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" + revision = "c059e472caf75dbe73903f6521a20abac245b17f" [[projects]] branch = "master" diff --git a/arlo.go b/arlo.go index 2e996f8..ff40ea0 100644 --- a/arlo.go +++ b/arlo.go @@ -1,6 +1,23 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( + "net/http" "sync" "github.com/jeffreydwalter/arlo-golang/internal/request" @@ -20,13 +37,14 @@ type Arlo struct { func newArlo(user string, pass string) (arlo *Arlo) { - c, _ := request.NewClient(BaseUrl) - // Add important headers. - c.BaseHttpHeader.Add("DNT", "1") - c.BaseHttpHeader.Add("schemaVersion", "1") - c.BaseHttpHeader.Add("Host", "arlo.netgear.com") - c.BaseHttpHeader.Add("Referer", "https://arlo.netgear.com/") + baseHeaders := make(http.Header) + baseHeaders.Add("DNT", "1") + baseHeaders.Add("schemaVersion", "1") + baseHeaders.Add("Host", "arlo.netgear.com") + baseHeaders.Add("Referer", "https://arlo.netgear.com/") + + c, _ := request.NewClient(BaseUrl, baseHeaders) return &Arlo{ user: user, @@ -40,8 +58,8 @@ func Login(user string, pass string) (arlo *Arlo, err error) { body := map[string]string{"email": arlo.user, "password": arlo.pass} resp, err := arlo.post(LoginUri, "", body, nil) - if err := checkHttpRequest(resp, err, "login request failed"); err != nil { - return nil, err + if err != nil { + return nil, errors.WithMessage(err, "failed to login") } defer resp.Body.Close() @@ -52,7 +70,7 @@ func Login(user string, pass string) (arlo *Arlo, err error) { if loginResponse.Success { // Cache the auth token. - arlo.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token) + arlo.client.AddHeader("Authorization", loginResponse.Data.Token) // Save the account info with the arlo struct. arlo.Account = loginResponse.Data @@ -75,10 +93,10 @@ func (a *Arlo) Logout() error { // 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() (devices Devices, err error) { +func (a *Arlo) GetDevices() (devices *Devices, err error) { resp, err := a.get(DevicesUri, "", nil) - if err := checkHttpRequest(resp, err, "failed to get devices"); err != nil { - return nil, err + if err != nil { + return nil, errors.WithMessage(err, "failed to get devices") } defer resp.Body.Close() @@ -95,11 +113,12 @@ func (a *Arlo) GetDevices() (devices Devices, err error) { return nil, errors.New("no devices found") } + // Cache a pointer to the arlo object with each device. for i := range deviceResponse.Data { deviceResponse.Data[i].arlo = a } - // disconnect all of the basestations from the EventStream. + // Disconnect all of the basestations from the EventStream. for i := range a.Basestations { if err := a.Basestations[i].Disconnect(); err != nil { return nil, errors.WithMessage(err, "failed to get devices") @@ -108,8 +127,8 @@ func (a *Arlo) GetDevices() (devices Devices, err error) { a.rwmutex.Lock() // Cache the devices as their respective types. - a.Cameras = deviceResponse.Data.GetCameras() - a.Basestations = deviceResponse.Data.GetBasestations() + a.Cameras = *deviceResponse.Data.GetCameras() + a.Basestations = *deviceResponse.Data.GetBasestations() a.rwmutex.Unlock() // subscribe each basestation to the EventStream. @@ -119,7 +138,7 @@ func (a *Arlo) GetDevices() (devices Devices, err error) { } } - return deviceResponse.Data, nil + return &deviceResponse.Data, nil } // UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given. diff --git a/arlo_test.go b/arlo_test.go index d0aa6f2..3bfbd77 100644 --- a/arlo_test.go +++ b/arlo_test.go @@ -1 +1,17 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo diff --git a/arlobaby.go b/arlobaby.go index 97b1574..6403c23 100644 --- a/arlobaby.go +++ b/arlobaby.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( diff --git a/basestation.go b/basestation.go index 622753b..2c0fac4 100644 --- a/basestation.go +++ b/basestation.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( @@ -17,7 +33,7 @@ type Basestation struct { eventStream *eventStream } -// Basestations is an array of Basestation objects. +// Basestations is a slice of Basestation objects. type Basestations []Basestation // Find returns a basestation with the device id passed in. diff --git a/camera.go b/camera.go index 7eab0bd..a332997 100644 --- a/camera.go +++ b/camera.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( @@ -11,7 +27,7 @@ import ( // 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. +// Cameras is a slice of Camera objects. type Cameras []Camera // Find returns a camera with the device id passed in. @@ -103,83 +119,6 @@ func (c *Camera) SetBrightness(brightness int) (response *EventStreamResponse, e return b.makeEventStreamRequest(payload, msg) } -// 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":"rtsp://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" } -func (c *Camera) StartStream() (response *StreamResponse, err error) { - payload := EventStreamPayload{ - Action: "set", - Resource: fmt.Sprintf("cameras/%s", c.DeviceId), - PublishResponse: true, - Properties: map[string]string{ - "activityState": "startUserStream", - "cameraId": c.DeviceId, - }, - TransId: genTransId(), - From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), - To: c.ParentId, - } - - msg := "failed to start stream" - - resp, err := c.arlo.post(DeviceStartStreamUri, c.XCloudId, payload, nil) - if err := checkHttpRequest(resp, err, msg); err != nil { - return nil, err - } - defer resp.Body.Close() - - if err := resp.Decode(response); err != nil { - return nil, err - } - - if !response.Success { - return nil, errors.WithMessage(errors.New("status was false"), msg) - } - - response.Data.Url = strings.Replace(response.Data.Url, "rtsp://", "rtsps://", 1) - - return response, nil -} - -// TakeSnapshot causes the camera to record a snapshot. -func (c *Camera) TakeSnapshot() (response *StreamResponse, err error) { - msg := "failed to take snapshot" - - response, err = c.StartStream() - if err != nil { - return nil, errors.WithMessage(err, msg) - } - - body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone} - resp, err := c.arlo.post(DeviceTakeSnapshotUri, c.XCloudId, body, nil) - if err := checkRequest(resp, err, "failed to update device name"); err != nil { - return nil, errors.WithMessage(err, msg) - } - - return response, nil -} - -// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg -// or something similar. -func (c *Camera) StartRecording() (response *StreamResponse, err error) { - msg := "failed to start recording" - - response, err = c.StartStream() - if err != nil { - return nil, errors.WithMessage(err, msg) - } - - body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone} - resp, err := c.arlo.post(DeviceStartRecordUri, c.XCloudId, body, nil) - if err := checkRequest(resp, err, "failed to update device name"); err != nil { - return nil, errors.WithMessage(err, msg) - } - - return response, nil -} - func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", @@ -321,3 +260,81 @@ func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (r } return b.makeEventStreamRequest(payload, msg) } + +// 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":"rtsp://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" } +func (c *Camera) StartStream() (url string, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: map[string]string{ + "activityState": "startUserStream", + "cameraId": c.DeviceId, + }, + TransId: genTransId(), + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to start stream" + + resp, err := c.arlo.post(DeviceStartStreamUri, c.XCloudId, payload, nil) + if err != nil { + return "", errors.WithMessage(err, msg) + } + defer resp.Body.Close() + + response := new(StreamResponse) + if err := resp.Decode(response); err != nil { + return "", err + } + + if !response.Success { + return "", errors.WithMessage(errors.New("status was false"), msg) + } + + response.URL = strings.Replace(response.URL, "rtsp://", "rtsps://", 1) + + return response.URL, nil +} + +// TakeSnapshot causes the camera to record a snapshot. +func (c *Camera) TakeSnapshot() (url string, err error) { + msg := "failed to take snapshot" + + url, err = c.StartStream() + if err != nil { + return "", errors.WithMessage(err, msg) + } + + body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone} + resp, err := c.arlo.post(DeviceTakeSnapshotUri, c.XCloudId, body, nil) + if err := checkRequest(resp, err, "failed to update device name"); err != nil { + return "", errors.WithMessage(err, msg) + } + + return url, nil +} + +// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg +// or something similar. +func (c *Camera) StartRecording() (url string, err error) { + msg := "failed to start recording" + + url, err = c.StartStream() + if err != nil { + return "", errors.WithMessage(err, msg) + } + + body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone} + resp, err := c.arlo.post(DeviceStartRecordUri, c.XCloudId, body, nil) + if err := checkRequest(resp, err, "failed to update device name"); err != nil { + return "", errors.WithMessage(err, msg) + } + + return url, nil +} diff --git a/const.go b/const.go index 8d2c860..5e64dfe 100644 --- a/const.go +++ b/const.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo const ( @@ -7,7 +23,6 @@ const ( SubscribeUri = "/client/subscribe?token=%s" UnsubscribeUri = "/client/unsubscribe" NotifyUri = "/users/devices/notify/%s" - ResetUri = "/users/library/reset" ServiceLevelUri = "/users/serviceLevel" OffersUri = "/users/payment/offers" UserProfileUri = "/users/profile" @@ -18,8 +33,9 @@ const ( UserLocationsUri = "/users/locations" UserLocationUri = "/users/locations/%s" LibraryUri = "/users/library" - LibraryRecycleUri = "/users/library/recycle" LibraryMetadataUri = "/users/library/metadata" + LibraryRecycleUri = "/users/library/recycle" + LibraryResetUri = "/users/library/reset" DevicesUri = "/users/devices" DeviceRenameUri = "/users/devices/renameDevice" DeviceDisplayOrderUri = "/users/devices/displayOrder" diff --git a/devices.go b/devices.go index 4b293ae..8d0c862 100644 --- a/devices.go +++ b/devices.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo // A Device is the device data, this can be a camera, basestation, arloq, etc. @@ -42,7 +58,7 @@ type Device struct { XCloudId string `json:"xCloudId"` } -// Devices is an array of Device objects. +// Devices is a slice of Device objects. type Devices []Device // A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order. @@ -70,9 +86,9 @@ func (ds *Devices) Find(deviceId string) *Device { return nil } -func (ds *Devices) FindCameras(basestationId string) Cameras { +func (ds Devices) FindCameras(basestationId string) Cameras { cs := new(Cameras) - for _, d := range *ds { + for _, d := range ds { if d.ParentId == basestationId { *cs = append(*cs, Camera(d)) } @@ -93,11 +109,11 @@ func (d Device) IsCamera() bool { // 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 devices of this type, so you can get the same data there or cast. -func (ds Devices) GetBasestations() Basestations { - var basestations Basestations +func (ds Devices) GetBasestations() *Basestations { + basestations := new(Basestations) for _, d := range ds { if d.IsBasestation() || !d.IsCamera() { - basestations = append(basestations, Basestation{Device: d}) + *basestations = append(*basestations, Basestation{Device: d}) } } return basestations @@ -107,11 +123,11 @@ func (ds Devices) GetBasestations() Basestations { // 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 devices of this type, so you can get the same data there or cast. -func (ds Devices) GetCameras() Cameras { - var cameras Cameras +func (ds Devices) GetCameras() *Cameras { + cameras := new(Cameras) for _, d := range ds { if d.IsCamera() || !d.IsBasestation() { - cameras = append(cameras, Camera(d)) + *cameras = append(*cameras, Camera(d)) } } return cameras diff --git a/events_stream.go b/events_stream.go index 6d85cc6..e946236 100644 --- a/events_stream.go +++ b/events_stream.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( @@ -74,7 +90,7 @@ func (e *eventStream) listen() (connected chan bool) { /* fmt.Println("Got event message.") fmt.Printf("EVENT: %s\n", event.Event) - fmt.Printf("DATA: %s\n", event.Data) + fmt.Printf("DATA: %s\n", event.URL) */ if event.Data != nil { diff --git a/internal/request/client.go b/internal/request/client.go index de3b48a..9d1882d 100644 --- a/internal/request/client.go +++ b/internal/request/client.go @@ -1,12 +1,30 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package request import ( "bytes" "encoding/json" "io" + "log" "net/http" "net/http/cookiejar" "net/url" + "sync" "github.com/pkg/errors" @@ -14,12 +32,13 @@ import ( ) type Client struct { - BaseURL *url.URL - BaseHttpHeader *http.Header - HttpClient *http.Client + BaseURL *url.URL + BaseHeaders *http.Header + HttpClient *http.Client + rwmutex sync.RWMutex } -func NewClient(baseurl string) (*Client, error) { +func NewClient(baseURL string, baseHeaders http.Header) (*Client, error) { var err error var jar *cookiejar.Jar @@ -30,7 +49,7 @@ func NewClient(baseurl string) (*Client, error) { } var u *url.URL - if u, err = url.Parse(baseurl); err != nil { + if u, err = url.Parse(baseURL); err != nil { return nil, errors.Wrap(err, "failed to create client object") } @@ -40,12 +59,18 @@ func NewClient(baseurl string) (*Client, error) { header.Set("Accept", "application/json") return &Client{ - BaseURL: u, - BaseHttpHeader: &header, - HttpClient: &http.Client{Jar: jar}, + BaseURL: u, + BaseHeaders: &header, + HttpClient: &http.Client{Jar: jar}, }, nil } +func (c *Client) AddHeader(key, value string) { + c.rwmutex.Lock() + c.BaseHeaders.Set(key, value) + c.rwmutex.Unlock() +} + func (c *Client) Get(uri string, header http.Header) (*Response, error) { req, err := c.newRequest("GET", uri, nil, header) if err != nil { @@ -80,7 +105,7 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header return nil, errors.Wrap(err, "failed to create request object") } } - // log.Printf("\n\nBODY (%s): %s\n\n", uri, buf) + log.Printf("\n\nBODY (%s): %s\n\n", uri, buf) u := c.BaseURL.String() + uri req, err := http.NewRequest(method, u, buf) @@ -88,7 +113,11 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header return nil, errors.Wrap(err, "failed to create request object") } - for k, v := range *c.BaseHttpHeader { + c.rwmutex.RLock() + baseHeaders := *c.BaseHeaders + c.rwmutex.RUnlock() + + for k, v := range baseHeaders { for _, h := range v { //log.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) req.Header.Set(k, h) @@ -116,8 +145,8 @@ func (c *Client) newResponse(resp *http.Response) (*Response, error) { func (c *Client) do(req *Request) (*Response, error) { - //log.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL)) - //log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header) + log.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL)) + log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header) resp, err := c.HttpClient.Do(&req.Request) if err != nil { diff --git a/internal/request/request.go b/internal/request/request.go index ec5ebc5..ffa85a0 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package request import ( diff --git a/internal/request/response.go b/internal/request/response.go index 35a807f..1efc4cd 100644 --- a/internal/request/response.go +++ b/internal/request/response.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package request import ( diff --git a/internal/util/util.go b/internal/util/util.go index 58f9932..98921fa 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package util import ( diff --git a/library.go b/library.go index 5a4ec33..238bd70 100644 --- a/library.go +++ b/library.go @@ -1,7 +1,26 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( + "fmt" "time" + + "github.com/pkg/errors" ) // LibraryMetaData is the library meta data. @@ -30,38 +49,53 @@ type Recording struct { UtcCreatedDate int64 `json:"utcCreatedDate"` CurrentState string `json:"currentState"` MediaDuration string `json:"mediaDuration"` + UniqueId string `json:"uniqueId"` } type Library []Recording -func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (response *LibraryMetaDataResponse, err error) { +func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) { + msg := "failed to get library metadata" body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} resp, err := a.post(LibraryMetadataUri, "", body, nil) - if err := checkHttpRequest(resp, err, "failed to get library metadata"); err != nil { + if err != nil { + return nil, errors.WithMessage(err, msg) + } + defer resp.Body.Close() + + response := new(LibraryMetaDataResponse) + if err := resp.Decode(&response); err != nil { return nil, err } - if err := resp.Decode(response); err != nil { - return nil, err + if !response.Success { + return nil, errors.New(msg) } - return response, nil + return &response.Data, nil } -func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (response *LibraryResponse, err error) { +func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) { + msg := "failed to get library" body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} resp, err := a.post(LibraryUri, "", body, nil) - if err := checkHttpRequest(resp, err, "failed to get library"); err != nil { + if err != nil { + return nil, errors.WithMessage(err, msg) + } + defer resp.Body.Close() + + response := new(LibraryResponse) + if err := resp.Decode(&response); err != nil { return nil, err } - if err := resp.Decode(response); err != nil { - return nil, err + if !response.Success { + return nil, errors.New(msg) } - return response, nil + return &response.Data, nil } /* @@ -71,9 +105,9 @@ func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (response *LibraryResponse NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. */ -func (a *Arlo) DeleteRecording(r Recording) error { +func (a *Arlo) DeleteRecording(r *Recording) error { - body := map[string]Library{"data": {r}} + body := map[string]Library{"data": {*r}} resp, err := a.post(LibraryRecycleUri, "", body, nil) return checkRequest(resp, err, "failed to delete recording") } @@ -85,9 +119,11 @@ func (a *Arlo) DeleteRecording(r Recording) error { NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. */ -func (a *Arlo) BatchDeleteRecordings(l Library) error { +func (a *Arlo) BatchDeleteRecordings(l *Library) error { - body := map[string]Library{"data": l} - resp, err := a.post(LibraryRecycleUri, "", body, nil) - return checkRequest(resp, err, "failed to delete recordings") + body := map[string]Library{"data": *l} + fmt.Printf("%+v\n", body) + return nil + //resp, err := a.post(LibraryRecycleUri, "", body, nil) + //return checkRequest(resp, err, "failed to delete recordings") } diff --git a/responses.go b/responses.go index 0af51e3..a4458d6 100644 --- a/responses.go +++ b/responses.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo // LoginResponse is an intermediate struct used when parsing data from the Login() call. @@ -24,12 +40,12 @@ type LibraryResponse struct { } type StreamResponse struct { - Data StreamUrl + URL string `json:"url"` Status } type RecordingResponse struct { - Data StreamUrl + URL string `json:"url"` Status } diff --git a/types.go b/types.go index 345eb7d..ac2b5cd 100644 --- a/types.go +++ b/types.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo /* @@ -59,7 +75,7 @@ type Connectivity struct { ActiveNetwork string `json:"activeNetwork,omitempty"` APN string `json:"apn,omitempty"` CarrierFw string `json:"carrierFw,omitempty"` - Connected bool `json:"connected"` + Connected bool `json:"connected,omitempty"` FWVersion string `json:"fwVersion,omitempty"` ICCID string `json:"iccid,omitempty"` IMEI string `json:"imei,omitempty"` @@ -115,10 +131,6 @@ type Favorite struct { Favorite uint8 `json:"Favorite"` } -type StreamUrl struct { - Url string `json:"url"` -} - type BaseDetectionProperties struct { Armed bool `json:"armed"` Sensitivity int `json:"sensitivity"` @@ -240,8 +252,8 @@ type EventStreamPayload struct { To string `json:"to"` } -// Data is part of the Status message fragment returned by most calls to the Arlo API. -// Data is only populated when Success is false. +// URL is part of the Status message fragment returned by most calls to the Arlo API. +// URL is only populated when Success is false. type Data struct { Message string `json:"message,omitempty"` Reason string `json:"reason,omitempty"` @@ -250,6 +262,6 @@ type Data struct { // Status is the message fragment returned from most http calls to the Arlo API. type Status struct { - Data `json:"Data,omitempty"` + Data `json:"URL,omitempty"` Success bool `json:"success"` } diff --git a/util.go b/util.go index b10b027..27079b6 100644 --- a/util.go +++ b/util.go @@ -1,10 +1,28 @@ +/* + * Copyright (c) 2018 Jeffrey Walter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package arlo import ( "fmt" + "io" "math" "math/rand" "net/http" + "os" "strconv" "strings" "time" @@ -15,25 +33,12 @@ import ( "github.com/pkg/errors" ) -func checkHttpRequest(resp *request.Response, err error, msg string) error { - if resp.StatusCode != 200 { - return errors.WithMessage(errors.New(fmt.Sprintf("http request failed: %s (%d)", resp.Status, resp.StatusCode)), msg) - } - +func checkRequest(resp *request.Response, err error, msg string) error { if err != nil { return errors.WithMessage(err, msg) } - - return nil -} - -func checkRequest(resp *request.Response, err error, msg string) error { defer resp.Body.Close() - if err := checkHttpRequest(resp, err, msg); err != nil { - return err - } - var status Status if err := resp.Decode(&status); err != nil { return err @@ -59,31 +64,38 @@ func genTransId() string { } func (a *Arlo) get(uri, xCloudId string, header http.Header) (*request.Response, error) { - if len(xCloudId) > 0 { - a.rwmutex.Lock() - a.client.BaseHttpHeader.Set("xcloudId", xCloudId) - a.rwmutex.Unlock() - } - + a.client.AddHeader("xcloudId", xCloudId) return a.client.Get(uri, header) } func (a *Arlo) put(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) { - if len(xCloudId) > 0 { - a.rwmutex.Lock() - a.client.BaseHttpHeader.Set("xcloudId", xCloudId) - a.rwmutex.Unlock() - } - + a.client.AddHeader("xcloudId", xCloudId) return a.client.Put(uri, body, header) } func (a *Arlo) post(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) { - if len(xCloudId) > 0 { - a.rwmutex.Lock() - a.client.BaseHttpHeader.Set("xcloudId", xCloudId) - a.rwmutex.Unlock() - } - + a.client.AddHeader("xcloudId", xCloudId) return a.client.Post(uri, body, header) } + +func (a *Arlo) DownloadFile(url, to string) error { + msg := fmt.Sprintf("failed to download file (%s) => (%s)", url, to) + resp, err := a.get(url, "", nil) + if err != nil { + return errors.WithMessage(err, msg) + } + defer resp.Body.Close() + + f, err := os.Create(to) + if err != nil { + return errors.WithMessage(err, msg) + } + + _, err = io.Copy(f, resp.Body) + defer f.Close() + if err != nil { + return errors.WithMessage(err, msg) + } + + return nil +}