dropcam 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +83 -0
- data/Rakefile +1 -0
- data/dropcam.gemspec +21 -0
- data/example/basics.rb +15 -0
- data/example/camera.rb +16 -0
- data/example/notifications.rb +19 -0
- data/example/settings.rb +16 -0
- data/example/stream.rb +16 -0
- data/lib/dropcam/base.rb +126 -0
- data/lib/dropcam/camera.rb +263 -0
- data/lib/dropcam/clip.rb +52 -0
- data/lib/dropcam/cuepoint.rb +11 -0
- data/lib/dropcam/error.rb +28 -0
- data/lib/dropcam/notification.rb +66 -0
- data/lib/dropcam/session.rb +47 -0
- data/lib/dropcam/setting.rb +38 -0
- data/lib/dropcam/stream.rb +96 -0
- data/lib/dropcam/version.rb +3 -0
- data/lib/dropcam.rb +65 -0
- metadata +67 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Nolan Brown
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Dropcam
|
2
|
+
|
3
|
+
RubyGem to access Dropcam account and Camera including direct live stream access
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'dropcam'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install dropcam
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
require 'dropcam'
|
22
|
+
|
23
|
+
dropcam = Dropcam::Dropcam.new("<USERNAME>","<PASSWORD>")
|
24
|
+
camera = dropcam.cameras.first
|
25
|
+
|
26
|
+
# returns jpg image data of the latest frame captured
|
27
|
+
screenshot = camera.current_image
|
28
|
+
|
29
|
+
# write data to disk
|
30
|
+
File.open("#{camera.title}.jpg", 'w') {|f| f.write(screenshot) }
|
31
|
+
|
32
|
+
# access and modify settings
|
33
|
+
# this disables the watermark on your camera stream
|
34
|
+
settings = camera.settings
|
35
|
+
settings["watermark.enabled"].set(false)
|
36
|
+
|
37
|
+
|
38
|
+
## Live Stream
|
39
|
+
|
40
|
+
**Streaming isn't directly integrated currently and it's up to you to find a player. Some of the players available:**
|
41
|
+
|
42
|
+
- VLC (RTSP/RTMP)
|
43
|
+
- RTMPDump (RTMP)
|
44
|
+
- openRTSP (RTSP)
|
45
|
+
|
46
|
+
The easiest way to record the live camera stream is with RTMPDump. Install via homebrew:
|
47
|
+
|
48
|
+
`$ brew install rtmpdump`
|
49
|
+
|
50
|
+
To save a live stream:
|
51
|
+
|
52
|
+
require 'dropcam'
|
53
|
+
dropcam = Dropcam::Dropcam.new("<USERNAME>","<PASSWORD>")
|
54
|
+
camera = dropcam.cameras.first
|
55
|
+
|
56
|
+
# record the live stream for 30 seconds
|
57
|
+
camera.stream.save_live("#{camera.title}.flv", 30)
|
58
|
+
|
59
|
+
# to get access information to use a third party application
|
60
|
+
# RTMP/Flash Streaming
|
61
|
+
camera.stream.rtmp_details
|
62
|
+
|
63
|
+
# RTSP Streaming
|
64
|
+
camera.stream.rtsp_details
|
65
|
+
|
66
|
+
|
67
|
+
Currently stream resolution is limited to 400x240.
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
## NOTES: ##
|
72
|
+
|
73
|
+
The Dropcam API is unofficial and unreleased. This code can break at anytime as Dropcam changes/updates their service.
|
74
|
+
|
75
|
+
This gem has only been tested on Mac OS 10.8 running Ruby 1.9.3
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
1. Fork it
|
80
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
81
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
82
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
83
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/dropcam.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dropcam/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "dropcam"
|
8
|
+
gem.version = Dropcam::VERSION
|
9
|
+
gem.authors = ["Nolan Brown"]
|
10
|
+
gem.email = ["nolanbrown@gmail.com"]
|
11
|
+
gem.description = %q{Access Dropcam account and cameras}
|
12
|
+
gem.summary = gem.description
|
13
|
+
gem.homepage = "https://github.com/nolanbrown/dropcam"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
|
21
|
+
end
|
data/example/basics.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#dropcam = Dropcam::Session.new("username","password")
|
2
|
+
lib = File.expand_path('../../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'dropcam'
|
6
|
+
require 'awesome_print'
|
7
|
+
dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
|
8
|
+
camera = dropcam.cameras.first
|
9
|
+
ap camera.uuid
|
10
|
+
ap camera.session_token
|
11
|
+
ap camera.rtsp_url
|
12
|
+
ap camera.rtmpdump_stream_command
|
13
|
+
|
14
|
+
settings = camera.settings
|
15
|
+
#ap settings["watermark.enabled"].set(false)
|
data/example/camera.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
lib = File.expand_path('../../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'dropcam'
|
5
|
+
require 'awesome_print'
|
6
|
+
|
7
|
+
dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
|
8
|
+
|
9
|
+
all_cameras = dropcam.cameras
|
10
|
+
all_cameras.each { |aCamera|
|
11
|
+
puts "\n------------------------------------\n"
|
12
|
+
aCamera.instance_variables.each{|variable|
|
13
|
+
print "#{variable} : "; $stdout.flush
|
14
|
+
ap aCamera.instance_variable_get(variable)
|
15
|
+
}
|
16
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
lib = File.expand_path('../../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'dropcam'
|
5
|
+
require 'awesome_print'
|
6
|
+
dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
|
7
|
+
camera = dropcam.cameras.first
|
8
|
+
camera.notification_devices.each{|notification|
|
9
|
+
# print all variable values
|
10
|
+
notification.instance_variables.each{|variable|
|
11
|
+
print "#{variable} : "; $stdout.flush
|
12
|
+
ap notification.instance_variable_get(variable)
|
13
|
+
}
|
14
|
+
|
15
|
+
# enable or disable notification
|
16
|
+
# notification.set_enabled(true)
|
17
|
+
puts "\n------------------------------------\n\n"
|
18
|
+
|
19
|
+
}
|
data/example/settings.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
lib = File.expand_path('../../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'dropcam'
|
5
|
+
require 'awesome_print'
|
6
|
+
dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
|
7
|
+
camera = dropcam.cameras.first
|
8
|
+
settings = camera.settings
|
9
|
+
|
10
|
+
ap settings
|
11
|
+
|
12
|
+
setting_name = "watermark.enabled"
|
13
|
+
setting_value = settings[setting_name].value
|
14
|
+
puts "Changing #{setting_name} from #{setting_value} to #{!setting_value}"
|
15
|
+
settings[setting_name].set(!setting_value)
|
16
|
+
puts "Changing #{setting_name} is now #{settings[setting_name].value}"
|
data/example/stream.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
lib = File.expand_path('../../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'dropcam'
|
5
|
+
|
6
|
+
dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
|
7
|
+
camera = dropcam.cameras.first
|
8
|
+
|
9
|
+
puts "\nRTSP URI"
|
10
|
+
puts camera.stream.rtsp_uri.to_s
|
11
|
+
|
12
|
+
puts "\nRTMPDump command"
|
13
|
+
puts camera.stream.rtmpdump
|
14
|
+
|
15
|
+
puts "\nSave Live Stream as FLV"
|
16
|
+
puts camera.stream.save_live("#{camera.title}.flv",30)
|
data/lib/dropcam/base.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Net
|
6
|
+
class HTTPResponse
|
7
|
+
def success?
|
8
|
+
self.code.to_i == 200
|
9
|
+
end
|
10
|
+
def not_found?
|
11
|
+
self.code.to_i == 404
|
12
|
+
end
|
13
|
+
def error?
|
14
|
+
self.code.to_i == 400
|
15
|
+
end
|
16
|
+
def not_authorized?
|
17
|
+
self.code.to_i == 403
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Dropcam
|
23
|
+
class Base
|
24
|
+
attr_accessor :session_token, :cookies
|
25
|
+
|
26
|
+
|
27
|
+
::NEXUS_API_BASE = "https://nexusapi.dropcam.com/"
|
28
|
+
|
29
|
+
::NEXUS_GET_IMAGE_PATH = "get_image" # uuid and width
|
30
|
+
::NEXUS_GET_AVAILABLE_PATH = "get_available" # start_time and uuid
|
31
|
+
::NEXUS_GET_CUEPOINT_PATH = "get_cuepoint" # start_time_uuid
|
32
|
+
::NEXUS_GET_EVENT_CLIP_PATH = "get_event_clip" # start_time_uuid
|
33
|
+
::NEXUS_GET_REVERSE_PAGINATED_CUEPOINTS_PATH = "get_reverse_paginated_cuepoint"
|
34
|
+
::API_BASE = "https://www.dropcam.com"
|
35
|
+
::API_PATH = "/api/v1"
|
36
|
+
|
37
|
+
|
38
|
+
::CAMERA_HTML_SETTINGS_BASE = "/cameras/settings/" # /uuid
|
39
|
+
|
40
|
+
::USERS_LOGIN = "#{API_PATH}/login.login"
|
41
|
+
::CAMERAS_UPDATE = "#{API_PATH}/cameras.update" # uuid, is_public
|
42
|
+
::CAMERAS_GET_BY_PUBLIC_TOKEN = "#{API_PATH}/cameras.get_by_public_token"
|
43
|
+
::CAMERAS_GET = "#{API_PATH}/cameras.get" # uuid
|
44
|
+
::CAMERAS_GET_VISIBLE = "#{API_PATH}/cameras.get_visible"
|
45
|
+
::CAMERAS_GET_PUBLIC = "#{API_PATH}/cameras.get_demo"
|
46
|
+
|
47
|
+
::DROPCAMS_GET_PROPERTIES = "#{API_PATH}/dropcams.get_properties" # uuid
|
48
|
+
::DROPCAMS_SET_PROPERTY = "#{API_PATH}/dropcams.set_property" # POST: uuid, key, value
|
49
|
+
::CAMERA_NOTIFICATION_UPDATE = "#{API_PATH}/camera_notifications.update"
|
50
|
+
::CAMERA_ADD_EMAIL_NOTIFICATION = "#{API_PATH}/users.add_email_notification_target"
|
51
|
+
::CAMERA_DELETE_NOTIFICATION = "#{API_PATH}/users.delete_notification_target"
|
52
|
+
::CAMERA_FIND_NOTIFICATIONS = "#{API_PATH}/camera_notifications.find_by_camera"
|
53
|
+
|
54
|
+
::SUBSCRIPTIONS_LIST = "#{API_PATH}/subscriptions.list" # camera_uuid
|
55
|
+
::SUBSCRIPTIONS_DELETE = "#{API_PATH}/subscriptions.delete"
|
56
|
+
::SUBSCRIPTIONS_CREATE_PUBLIC = "#{API_PATH}/subscriptions.create_public"
|
57
|
+
::USERS_GET_SESSION_TOKEN = "#{API_PATH}/users.get_session_token"
|
58
|
+
::USERS_GET_CURRENT = "#{API_PATH}/users.get_current"
|
59
|
+
|
60
|
+
::CLIP_GET_ALL = "#{API_PATH}/videos.get_owned"
|
61
|
+
::CLIP_CREATE = "#{API_PATH}/videos.request" # POST: start_date (ex. 1357598395), title, length (in seconds), uuid, description
|
62
|
+
::CLIP_DELETE = "#{API_PATH}/videos.delete" # DELETE: id = clip_id
|
63
|
+
|
64
|
+
## videos.get # id = clip_id
|
65
|
+
## videos.download # id = clip_id
|
66
|
+
## videos.play # id = clip_idß
|
67
|
+
def post(path, parameters, cookies, use_nexus=false)
|
68
|
+
|
69
|
+
http = _dropcam_http(use_nexus)
|
70
|
+
|
71
|
+
request = Net::HTTP::Post.new(path)
|
72
|
+
request.set_form_data(parameters)
|
73
|
+
|
74
|
+
request.add_field("Cookie",cookies.join('; ')) if cookies
|
75
|
+
|
76
|
+
response = http.request(request)
|
77
|
+
|
78
|
+
return response
|
79
|
+
end
|
80
|
+
|
81
|
+
def get(path, parameters,cookies, use_nexus=false)
|
82
|
+
http = _dropcam_http(use_nexus)
|
83
|
+
|
84
|
+
query_path = "#{path}"
|
85
|
+
query_path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters.length > 0
|
86
|
+
request = Net::HTTP::Get.new(query_path)
|
87
|
+
|
88
|
+
request.add_field("Cookie",cookies.join('; ')) if cookies
|
89
|
+
|
90
|
+
response = http.request(request)
|
91
|
+
return response
|
92
|
+
end
|
93
|
+
|
94
|
+
def delete(path, parameters,cookies, use_nexus=false)
|
95
|
+
http = _dropcam_http(use_nexus)
|
96
|
+
|
97
|
+
query_path = "#{path}"
|
98
|
+
query_path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters.length > 0
|
99
|
+
request = Net::HTTP::Delete.new(query_path)
|
100
|
+
|
101
|
+
request.add_field("Cookie",cookies.join('; ')) if cookies
|
102
|
+
|
103
|
+
response = http.request(request)
|
104
|
+
return response
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def _dropcam_http(use_nexus)
|
110
|
+
base = API_BASE
|
111
|
+
base = NEXUS_API_BASE if use_nexus
|
112
|
+
uri = URI.parse(base)
|
113
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
114
|
+
http.use_ssl = true
|
115
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
116
|
+
#http.set_debug_output($stdout)
|
117
|
+
|
118
|
+
return http
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'error'
|
5
|
+
require_relative 'notification'
|
6
|
+
require_relative 'setting'
|
7
|
+
require_relative 'cuepoint'
|
8
|
+
require_relative 'clip'
|
9
|
+
require_relative 'stream'
|
10
|
+
|
11
|
+
module Dropcam
|
12
|
+
class Camera < Base
|
13
|
+
|
14
|
+
attr_reader :uuid, :notification_devices, :download_host, :download_server_live, :is_streaming, :title, :public_token
|
15
|
+
attr_reader :description, :timezone_utc_offset, :timezone, :is_connected, :is_online, :is_public, :hours_of_recording_max
|
16
|
+
attr_reader :type, :id, :owner_id
|
17
|
+
attr_accessor :settings
|
18
|
+
|
19
|
+
|
20
|
+
def initialize(uuid, properties={})
|
21
|
+
@uuid = uuid
|
22
|
+
@settings = {}
|
23
|
+
self.properties = properties
|
24
|
+
end
|
25
|
+
|
26
|
+
def properties=(properties)
|
27
|
+
properties.each{|key, value|
|
28
|
+
instance_variable_set("@#{key}", value)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def get_image(width=1200, timestamp=nil)
|
34
|
+
params = {"uuid"=>@uuid, "width" => width}
|
35
|
+
params["time"] = timestamp if timestamp
|
36
|
+
|
37
|
+
response = get(::IMAGE_PATH, params, @cookies, true)
|
38
|
+
if response.success?
|
39
|
+
return response.body
|
40
|
+
elsif response.not_authorized?
|
41
|
+
raise AuthorizationError
|
42
|
+
else
|
43
|
+
raise CameraNotFoundError
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def current_image(width=1200)
|
48
|
+
return get_image(width)
|
49
|
+
end
|
50
|
+
|
51
|
+
def clips
|
52
|
+
response = get(::CLIP_GET_ALL, {}, @cookies)
|
53
|
+
if response.success?
|
54
|
+
return response.body ## returns a zip
|
55
|
+
clips = []
|
56
|
+
all_clips = JSON.parse(response.body)["items"]
|
57
|
+
for clip in all_clips
|
58
|
+
c = Clip.new(self, clip)
|
59
|
+
if c.camera_id == self.id
|
60
|
+
clips.push c
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return clips
|
64
|
+
elsif response.not_authorized?
|
65
|
+
raise AuthorizationError
|
66
|
+
else
|
67
|
+
raise CameraNotFoundError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def create_clip(length, start_date, title, description)
|
73
|
+
params = {"uuid"=>@uuid, "length" => length, "start_date" => start_date, "title" => title, "description" => description}
|
74
|
+
response = post(::VIDEOS_REQUEST, params, @cookies)
|
75
|
+
if response.success?
|
76
|
+
clip_info = JSON.parse(response.body)["items"][0]
|
77
|
+
return Clip.new(self, clip_info)
|
78
|
+
elsif response.not_authorized?
|
79
|
+
raise AuthorizationError
|
80
|
+
else
|
81
|
+
raise CameraNotFoundError
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def get_event_clip_image_archive(cuepoint_id, number_of_frames, width)
|
87
|
+
params = {"uuid"=>@uuid, "width" => width, "cuepoint_id" => cuepoint_id, "num_frames" => number_of_frames, "format" => "TAR_JPG"}
|
88
|
+
response = get(::NEXUS_GET_EVENT_CLIP_PATH, params, @cookies, true)
|
89
|
+
if response.success?
|
90
|
+
return response.body ## returns a zip
|
91
|
+
elsif response.not_authorized?
|
92
|
+
raise AuthorizationError
|
93
|
+
else
|
94
|
+
raise CameraNotFoundError
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_event_clip_video(cuepoint_id, number_of_frames, width)
|
99
|
+
params = {"uuid"=>@uuid, "width" => width, "cuepoint_id" => cuepoint_id, "num_frames" => number_of_frames, "format" => "h264"}
|
100
|
+
response = get(::NEXUS_GET_EVENT_CLIP_PATH, params, @cookies, true)
|
101
|
+
if response.success?
|
102
|
+
return response.body ## returns a zip
|
103
|
+
elsif response.not_authorized?
|
104
|
+
raise AuthorizationError
|
105
|
+
else
|
106
|
+
raise CameraNotFoundError
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_all_cuepoints(limit=2500)
|
111
|
+
params = {"uuid"=>@uuid, "max_results"=>limit}
|
112
|
+
response = get(::NEXUS_GET_REVERSE_PAGINATED_CUEPOINTS_PATH, params, @cookies, true)
|
113
|
+
if response.success?
|
114
|
+
cuepoints = []
|
115
|
+
all_cuepoints = JSON.parse(response.body)
|
116
|
+
for cuepoint in all_cuepoints
|
117
|
+
cuepoints.push Cuepoint.new(cuepoint)
|
118
|
+
end
|
119
|
+
|
120
|
+
return cuepoints
|
121
|
+
elsif response.not_authorized?
|
122
|
+
raise AuthorizationError
|
123
|
+
else
|
124
|
+
raise CameraNotFoundError
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_cuepoint(start_time)
|
129
|
+
params = {"uuid"=>@uuid, "start_time" => start_time}
|
130
|
+
response = get(::NEXUS_GET_CUEPOINT_PATH, params, @cookies, true)
|
131
|
+
if response.success?
|
132
|
+
return Cuepoint.new(JSON.parse(response.body)[0])
|
133
|
+
elsif response.not_authorized?
|
134
|
+
raise AuthorizationError
|
135
|
+
else
|
136
|
+
raise CameraNotFoundError
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def update_info
|
141
|
+
response = get(::CAMERAS_GET, {"id"=>@uuid}, @cookies)
|
142
|
+
if response.success?
|
143
|
+
self.properties = JSON.parse(response.body)["items"][0]
|
144
|
+
elsif response.not_authorized?
|
145
|
+
raise AuthorizationError
|
146
|
+
else
|
147
|
+
raise CameraNotFoundError
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
def public=(is_public)
|
154
|
+
response = post(::CAMERAS_UPDATE, {"uuid"=>@uuid, "is_public"=>is_public, "accepted_public_terms_at" => "true"}, @cookies)
|
155
|
+
if response.success?
|
156
|
+
self.properties = JSON.parse(response.body)["items"][0]
|
157
|
+
elsif response.not_authorized?
|
158
|
+
raise AuthorizationError
|
159
|
+
else
|
160
|
+
raise CameraNotFoundError
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def public?
|
165
|
+
@is_public
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_public_token(token)
|
169
|
+
response = post(::CAMERAS_UPDATE, {"uuid"=>@uuid, "token"=>token}, @cookies)
|
170
|
+
if response.success?
|
171
|
+
self.properties = JSON.parse(response.body)["items"][0]
|
172
|
+
return true
|
173
|
+
elsif response.not_authorized?
|
174
|
+
raise AuthorizationError
|
175
|
+
else
|
176
|
+
raise CameraNotFoundError
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def settings=(new_settings)
|
181
|
+
@settings = {}
|
182
|
+
new_settings.each{|key,value|
|
183
|
+
@settings[key] = Setting.new(key, value, self)
|
184
|
+
}
|
185
|
+
@settings
|
186
|
+
end
|
187
|
+
|
188
|
+
def settings(force=false)
|
189
|
+
return @settings unless force == true or @settings.length == 0 # key these cached
|
190
|
+
|
191
|
+
response = get(::DROPCAMS_GET_PROPERTIES, {"uuid"=>@uuid}, @cookies)
|
192
|
+
if response.success?
|
193
|
+
self.settings = JSON.parse(response.body)["items"][0]
|
194
|
+
return @settings
|
195
|
+
elsif response.not_authorized?
|
196
|
+
raise AuthorizationError
|
197
|
+
else
|
198
|
+
raise CameraNotFoundError
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def notification_devices
|
203
|
+
|
204
|
+
response = get(::CAMERA_FIND_NOTIFICATIONS, { "id" => @uuid }, @cookies)
|
205
|
+
if response.success?
|
206
|
+
notifications = []
|
207
|
+
|
208
|
+
all_notifications = JSON.parse(response.body)["items"]
|
209
|
+
all_notifications.each{|note|
|
210
|
+
notifications.push Notification.new(self, note["target"])
|
211
|
+
}
|
212
|
+
|
213
|
+
notifications
|
214
|
+
elsif response.not_authorized?
|
215
|
+
raise AuthorizationError
|
216
|
+
else
|
217
|
+
raise CameraNotFoundError
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
## API for Notifications doesn't include email notifcations
|
222
|
+
## The code below parses an HTML Partial to get all notifcation values
|
223
|
+
|
224
|
+
def all_notification_devices
|
225
|
+
request_path = ::CAMERA_HTML_SETTINGS_BASE + @uuid
|
226
|
+
response = get(request_path, {}, @cookies)
|
227
|
+
if response.success?
|
228
|
+
raw_html = response.body
|
229
|
+
doc = Hpricot.parse(raw_html)
|
230
|
+
|
231
|
+
notifications = []
|
232
|
+
doc.search("//div[@class='notification_target']").each { |notification_target|
|
233
|
+
puts notification_target
|
234
|
+
data_id = notification_target.get_attribute("data-id")
|
235
|
+
puts data_id
|
236
|
+
name = notification_target.at("div/span").inner_text
|
237
|
+
|
238
|
+
input = notification_target.at("div/input")
|
239
|
+
|
240
|
+
attributes = input.attributes.to_hash
|
241
|
+
data_type = attributes["data-type"]
|
242
|
+
data_value = attributes["data-value"]
|
243
|
+
checked = attributes.has_key?("checked")
|
244
|
+
|
245
|
+
|
246
|
+
notifications.push(note)
|
247
|
+
}
|
248
|
+
return notifications
|
249
|
+
elsif response.not_authorized?
|
250
|
+
raise AuthorizationError
|
251
|
+
else
|
252
|
+
raise CameraNotFoundError
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
|
258
|
+
def stream
|
259
|
+
Stream.new self
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
data/lib/dropcam/clip.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Dropcam
|
2
|
+
class Clip
|
3
|
+
attr_reader :id, :public_link, :description, :title, :is_error, :start_time, :server, :camera_id, :generated_time
|
4
|
+
attr_reader :filename, :length_in_seconds
|
5
|
+
|
6
|
+
attr_accessor :properties
|
7
|
+
def initialize(camera, properties = nil)
|
8
|
+
@camera = camera
|
9
|
+
self.properties = properties
|
10
|
+
end
|
11
|
+
|
12
|
+
def properties=(properties)
|
13
|
+
properties.each{|key, value|
|
14
|
+
instance_variable_set("@#{key}", value)
|
15
|
+
}
|
16
|
+
@properties = properties
|
17
|
+
end
|
18
|
+
|
19
|
+
def direct_link
|
20
|
+
return "https://#{@server}/#{@filename}"
|
21
|
+
end
|
22
|
+
def direct_screenshot_link
|
23
|
+
return "https://#{@server}/s3#{File.basename(@filename, File.extname(@filename))}.jpg"
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_title(title)
|
27
|
+
return false unless @id
|
28
|
+
response = post(::CLIP_DELETE, { "id" => @id, "title" => title }, @cookies)
|
29
|
+
if response.success?
|
30
|
+
return true
|
31
|
+
elsif response.not_authorized?
|
32
|
+
raise AuthorizationError
|
33
|
+
else
|
34
|
+
raise CameraNotFoundError
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete
|
40
|
+
return false unless @id
|
41
|
+
response = post(::CLIP_DELETE, { "id" => @id }, @cookies)
|
42
|
+
if response.success?
|
43
|
+
return true
|
44
|
+
elsif response.not_authorized?
|
45
|
+
raise AuthorizationError
|
46
|
+
else
|
47
|
+
raise CameraNotFoundError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Dropcam
|
2
|
+
class AuthenticationError < StandardError
|
3
|
+
def initialize(msg = "Invalid Credentials")
|
4
|
+
super(msg)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
class AuthorizationError < StandardError
|
8
|
+
def initialize(msg = "Not Authorized")
|
9
|
+
super(msg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class CameraNotFoundError < StandardError
|
13
|
+
def initialize(msg = "Camera Not Found")
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
class UnkownError < StandardError
|
18
|
+
def initialize(msg = "Unkown Error")
|
19
|
+
super(msg)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class RequestError < StandardError
|
23
|
+
def initialize(msg = "Request Error")
|
24
|
+
super(msg)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Dropcam
|
2
|
+
class Notification < Base
|
3
|
+
|
4
|
+
attr_accessor :name, :type, :value, :is_enabled, :id
|
5
|
+
|
6
|
+
def initialize(camera, properties={})
|
7
|
+
@camera = camera
|
8
|
+
@name = properties["name"]
|
9
|
+
@type = properties["type"]
|
10
|
+
@value = properties["value"]
|
11
|
+
@id = properties["id"]
|
12
|
+
@is_enabled = properties["enabled"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def find(name)
|
16
|
+
note = @camera.notification_devices.select{|note|
|
17
|
+
if note.name == name
|
18
|
+
puts "#{note.name} == #{name}"
|
19
|
+
return note
|
20
|
+
end
|
21
|
+
}
|
22
|
+
note
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(email)
|
26
|
+
# {"status": 400, "items": [], "status_description": "bad-request", "status_detail": "This notification target already exists"}
|
27
|
+
response = post(::CAMERA_ADD_EMAIL_NOTIFICATION, {"email"=>email}, @camera.cookies)
|
28
|
+
if response.success?
|
29
|
+
return Notification.new(@camera, JSON.parse(response.body)["items"][0])
|
30
|
+
elsif response.error?
|
31
|
+
raise UnkownError, JSON.parse(response.body)["status_detail"]
|
32
|
+
elsif response.not_authorized?
|
33
|
+
raise AuthorizationError
|
34
|
+
else
|
35
|
+
raise CameraNotFoundError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set(enable)
|
40
|
+
# email or gcm or apn
|
41
|
+
params = {"id"=>@camera.uuid, "is_enabled"=>enable, "device_token" => @value}
|
42
|
+
puts params
|
43
|
+
response = post(::CAMERA_NOTIFICATION_UPDATE, params, @camera.cookies)
|
44
|
+
if response.success?
|
45
|
+
return true
|
46
|
+
elsif response.not_authorized?
|
47
|
+
raise AuthorizationError
|
48
|
+
else
|
49
|
+
raise CameraNotFoundError
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete(notifcation_id=nil)
|
54
|
+
notifcation_id = @id unless notifcation_id
|
55
|
+
response = post(::CAMERA_DELETE_NOTIFICATION, {"id"=>notifcation_id}, @camera.cookies)
|
56
|
+
if response.success?
|
57
|
+
return true
|
58
|
+
elsif response.not_authorized?
|
59
|
+
raise AuthorizationError
|
60
|
+
else
|
61
|
+
raise CameraNotFoundError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
module Dropcam
|
3
|
+
class Session < Base
|
4
|
+
|
5
|
+
attr_accessor :session_token, :cookies
|
6
|
+
def initialize(username, password)
|
7
|
+
@username = username
|
8
|
+
@password = password
|
9
|
+
end
|
10
|
+
|
11
|
+
def authenticate
|
12
|
+
|
13
|
+
params = {"username" => @username, "password" => @password}
|
14
|
+
response = post(::USERS_LOGIN, params, nil)
|
15
|
+
all_cookies = response.get_fields('set-cookie') # only cookies are set on valid credentials
|
16
|
+
|
17
|
+
## for some reason, dropcam responds with 200 on invalid credentials
|
18
|
+
if response.success? and all_cookies
|
19
|
+
|
20
|
+
cookies = []
|
21
|
+
all_cookies.each { | cookie |
|
22
|
+
cookies.push(cookie.split('; ')[0])
|
23
|
+
}
|
24
|
+
|
25
|
+
@cookies = cookies
|
26
|
+
@session_token = _session_token # this value is embedded in the cookie but leaving this as is incase the API changes
|
27
|
+
|
28
|
+
else
|
29
|
+
raise AuthenticationError, "Invalid Credentials"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
protected
|
36
|
+
def _session_token
|
37
|
+
response = get(::USERS_GET_SESSION_TOKEN, {}, @cookies)
|
38
|
+
if response.success?
|
39
|
+
response_json = JSON.parse(response.body)
|
40
|
+
token = response_json["items"][0]
|
41
|
+
return token
|
42
|
+
end
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Dropcam
|
2
|
+
class Setting < Base
|
3
|
+
#
|
4
|
+
attr_accessor :name
|
5
|
+
def initialize(name, value, camera)
|
6
|
+
@camera = camera
|
7
|
+
@name = name
|
8
|
+
@current_value = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
return false if @current_value == 'false'
|
13
|
+
return true if @current_value == 'true'
|
14
|
+
|
15
|
+
@current_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"<Dropcam::Setting:#{object_id} @name=#{@name} @value=#{@current_value}>"
|
20
|
+
end
|
21
|
+
|
22
|
+
def set(value)
|
23
|
+
response = post(::DROPCAMS_SET_PROPERTY, {"uuid"=>@camera.uuid, "key" => @name, "value" => value}, @camera.cookies)
|
24
|
+
if response.success?
|
25
|
+
@current_value = value
|
26
|
+
@camera.settings = JSON.parse(response.body)["items"][0]
|
27
|
+
true
|
28
|
+
elsif response.error?
|
29
|
+
raise UnkownError, JSON.parse(response.body)["status_detail"]
|
30
|
+
elsif response.not_authorized?
|
31
|
+
raise AuthorizationError
|
32
|
+
else
|
33
|
+
raise CameraNotFoundError
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'Open3'
|
2
|
+
require 'timeout'
|
3
|
+
module Dropcam
|
4
|
+
class Stream
|
5
|
+
DEFAULT_RTSP_PORT = "554"
|
6
|
+
BUFFER_SIZE = 1024
|
7
|
+
|
8
|
+
attr_reader :camera
|
9
|
+
def initialize(camera)
|
10
|
+
@camera = camera
|
11
|
+
end
|
12
|
+
def rtsp_details
|
13
|
+
{
|
14
|
+
:protocol => "rtsp",
|
15
|
+
:user => "user",
|
16
|
+
:password => @camera.session_token,
|
17
|
+
:base => URI.parse(@camera.download_host).scheme,
|
18
|
+
:path => @camera.uuid,
|
19
|
+
:port => DEFAULT_RTSP_PORT
|
20
|
+
}
|
21
|
+
end
|
22
|
+
def rtsp_uri
|
23
|
+
URI.parse "rtsp://user:#{@camera.session_token}@#{URI.parse(@camera.download_host).scheme}:#{DEFAULT_RTSP_PORT}/#{@camera.uuid}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def rtmp_details
|
27
|
+
{
|
28
|
+
:app => "nexus",
|
29
|
+
:host => "stream.dropcam.com",
|
30
|
+
:playpath => @camera.uuid,
|
31
|
+
:variables => { "S:" => @camera.session_token }
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def rtmpdump
|
36
|
+
stream_command = "rtmpdump --live --app nexus --host stream.dropcam.com --playpath " + @camera.uuid
|
37
|
+
stream_command += " --conn S:" + @camera.session_token
|
38
|
+
return stream_command
|
39
|
+
end
|
40
|
+
|
41
|
+
def save_live(filename, duration=30)
|
42
|
+
raise StandardError, "RTMPDump is not found in your PATH" unless system("which -s rtmpdump")
|
43
|
+
# we add 10 seconds because it take about that amount of time to get the stream up and running with rtmpdump
|
44
|
+
run_with_timeout("#{self.rtmpdump} --quiet --flv #{filename}", duration+10, 1)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Runs a specified shell command in a separate thread.
|
50
|
+
# If it exceeds the given timeout in seconds, kills it.
|
51
|
+
# Returns any output produced by the command (stdout or stderr) as a String.
|
52
|
+
# Uses Kernel.select to wait up to the tick length (in seconds) between
|
53
|
+
# checks on the command's status
|
54
|
+
#
|
55
|
+
# If you've got a cleaner way of doing this, I'd be interested to see it.
|
56
|
+
# If you think you can do it with Ruby's Timeout module, think again.
|
57
|
+
|
58
|
+
## VIA https://gist.github.com/1032297
|
59
|
+
def run_with_timeout(command, timeout, tick)
|
60
|
+
output = ''
|
61
|
+
begin
|
62
|
+
# Start task in another thread, which spawns a process
|
63
|
+
stdin, stderrout, thread = Open3.popen2e(command)
|
64
|
+
# Get the pid of the spawned process
|
65
|
+
pid = thread[:pid]
|
66
|
+
start = Time.now
|
67
|
+
|
68
|
+
while (Time.now - start) < timeout and thread.alive?
|
69
|
+
# Wait up to `tick` seconds for output/error data
|
70
|
+
Kernel.select([stderrout], nil, nil, tick)
|
71
|
+
# Try to read the data
|
72
|
+
begin
|
73
|
+
#output << stderrout.read_nonblock(BUFFER_SIZE)
|
74
|
+
rescue IO::WaitReadable
|
75
|
+
# A read would block, so loop around for another select
|
76
|
+
rescue EOFError
|
77
|
+
# Command has completed, not really an error...
|
78
|
+
break
|
79
|
+
end
|
80
|
+
end
|
81
|
+
# Give Ruby time to clean up the other thread
|
82
|
+
sleep 1
|
83
|
+
|
84
|
+
if thread.alive?
|
85
|
+
# We need to kill the process, because killing the thread leaves
|
86
|
+
# the process alive but detached, annoyingly enough.
|
87
|
+
Process.kill("TERM", pid)
|
88
|
+
end
|
89
|
+
ensure
|
90
|
+
stdin.close if stdin
|
91
|
+
stderrout.close if stderrout
|
92
|
+
end
|
93
|
+
return output
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/dropcam.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "dropcam/version"
|
2
|
+
require "dropcam/base"
|
3
|
+
require "dropcam/camera"
|
4
|
+
require "dropcam/session"
|
5
|
+
|
6
|
+
module Dropcam
|
7
|
+
class Dropcam < Base
|
8
|
+
attr_reader :session
|
9
|
+
def initialize(username, password)
|
10
|
+
@session = Session.new(username, password)
|
11
|
+
@session.authenticate
|
12
|
+
##
|
13
|
+
end
|
14
|
+
def camera(uuid)
|
15
|
+
c = Camera.new(uuid)
|
16
|
+
c.cookies = @session.cookies
|
17
|
+
c.session_token = @session.session_token
|
18
|
+
c.properties = c.info
|
19
|
+
c
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_public_camera(token)
|
23
|
+
response = get(::CAMERAS_GET_BY_PUBLIC_TOKEN, {"token"=>token, "return_deleted"=>true}, @session.cookies)
|
24
|
+
if response.success?
|
25
|
+
return response.body
|
26
|
+
elsif response.not_authorized?
|
27
|
+
raise AuthorizationError
|
28
|
+
else
|
29
|
+
raise CameraNotFoundError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def public_cameras
|
34
|
+
response = get(::CAMERAS_GET_PUBLIC, {}, @session.cookies)
|
35
|
+
cameras = []
|
36
|
+
if response.success?
|
37
|
+
response_json = JSON.parse(response.body)
|
38
|
+
owned = response_json["items"][0]["owned"]
|
39
|
+
owned.each{|camera|
|
40
|
+
c = Camera.new(camera["uuid"], camera)
|
41
|
+
c.cookies = @session.cookies
|
42
|
+
c.session_token = @session.session_token
|
43
|
+
cameras.push(c)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
return cameras
|
47
|
+
end
|
48
|
+
|
49
|
+
def cameras
|
50
|
+
response = get(::CAMERAS_GET_VISIBLE, {"group_cameras" => true}, @session.cookies)
|
51
|
+
cameras = []
|
52
|
+
if response.success?
|
53
|
+
response_json = JSON.parse(response.body)
|
54
|
+
owned = response_json["items"][0]["owned"]
|
55
|
+
owned.each{|camera|
|
56
|
+
c = Camera.new(camera["uuid"], camera)
|
57
|
+
c.cookies = @session.cookies
|
58
|
+
c.session_token = @session.session_token
|
59
|
+
cameras.push(c)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
return cameras
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dropcam
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nolan Brown
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-12 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Access Dropcam account and cameras
|
15
|
+
email:
|
16
|
+
- nolanbrown@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- dropcam.gemspec
|
27
|
+
- example/basics.rb
|
28
|
+
- example/camera.rb
|
29
|
+
- example/notifications.rb
|
30
|
+
- example/settings.rb
|
31
|
+
- example/stream.rb
|
32
|
+
- lib/dropcam.rb
|
33
|
+
- lib/dropcam/base.rb
|
34
|
+
- lib/dropcam/camera.rb
|
35
|
+
- lib/dropcam/clip.rb
|
36
|
+
- lib/dropcam/cuepoint.rb
|
37
|
+
- lib/dropcam/error.rb
|
38
|
+
- lib/dropcam/notification.rb
|
39
|
+
- lib/dropcam/session.rb
|
40
|
+
- lib/dropcam/setting.rb
|
41
|
+
- lib/dropcam/stream.rb
|
42
|
+
- lib/dropcam/version.rb
|
43
|
+
homepage: https://github.com/nolanbrown/dropcam
|
44
|
+
licenses: []
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.8.24
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Access Dropcam account and cameras
|
67
|
+
test_files: []
|