vhx-ruby 0.0.2

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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +2 -0
  5. data/README.md +59 -0
  6. data/Rakefile +1 -0
  7. data/lib/vhx.rb +66 -0
  8. data/lib/vhx/client.rb +86 -0
  9. data/lib/vhx/error.rb +24 -0
  10. data/lib/vhx/middleware/error_response.rb +32 -0
  11. data/lib/vhx/middleware/oauth2.rb +22 -0
  12. data/lib/vhx/oauth_token.rb +11 -0
  13. data/lib/vhx/objects/authorization.rb +5 -0
  14. data/lib/vhx/objects/collection.rb +8 -0
  15. data/lib/vhx/objects/collection_item.rb +4 -0
  16. data/lib/vhx/objects/customer.rb +8 -0
  17. data/lib/vhx/objects/product.rb +6 -0
  18. data/lib/vhx/objects/site.rb +4 -0
  19. data/lib/vhx/objects/user.rb +10 -0
  20. data/lib/vhx/objects/video.rb +7 -0
  21. data/lib/vhx/objects/video_file.rb +4 -0
  22. data/lib/vhx/utilities/api_operations/create.rb +22 -0
  23. data/lib/vhx/utilities/api_operations/delete.rb +16 -0
  24. data/lib/vhx/utilities/api_operations/list.rb +21 -0
  25. data/lib/vhx/utilities/api_operations/request.rb +17 -0
  26. data/lib/vhx/utilities/api_operations/update.rb +15 -0
  27. data/lib/vhx/utilities/vhx_helper.rb +20 -0
  28. data/lib/vhx/utilities/vhx_list_object.rb +44 -0
  29. data/lib/vhx/utilities/vhx_object.rb +106 -0
  30. data/lib/vhx/version.rb +3 -0
  31. data/spec/client_spec.rb +69 -0
  32. data/spec/fixtures/sample_array_list.json +15 -0
  33. data/spec/fixtures/sample_file_response.json +18 -0
  34. data/spec/fixtures/sample_hash_list.json +23 -0
  35. data/spec/fixtures/sample_package_response.json +185 -0
  36. data/spec/fixtures/sample_site_response.json +26 -0
  37. data/spec/fixtures/sample_user_response.json +21 -0
  38. data/spec/fixtures/sample_video_response.json +51 -0
  39. data/spec/middleware/error_response_spec.rb +11 -0
  40. data/spec/middleware/oauth2_spec.rb +14 -0
  41. data/spec/objects/file_spec.rb +29 -0
  42. data/spec/objects/package_spec.rb +74 -0
  43. data/spec/objects/site_spec.rb +44 -0
  44. data/spec/objects/user_spec.rb +51 -0
  45. data/spec/objects/video_spec.rb +36 -0
  46. data/spec/spec_helper.rb +18 -0
  47. data/spec/test_data.rb +55 -0
  48. data/spec/utilities/vhx_collection_spec.rb +27 -0
  49. data/spec/utilities/vhx_helper_spec.rb +47 -0
  50. data/spec/utilities/vhx_object_spec.rb +53 -0
  51. data/spec/vcr/Vhx_Client/application_user/_refresh_access_token/access_token_changed.yml +57 -0
  52. data/spec/vcr/Vhx_Client/application_user/_refresh_access_token/oauth_token_refreshed.yml +57 -0
  53. data/spec/vcr/Vhx_File/associations/are_present.yml +409 -0
  54. data/spec/vcr/Vhx_Middleware_ErrorResponse/unauthorized_user_credentials.yml +122 -0
  55. data/spec/vcr/Vhx_Middleware_OAuth2/access_token_refresh.yml +797 -0
  56. data/spec/vcr/Vhx_Package/_add_video/with_hypermedia/returns_package_object.yml +51 -0
  57. data/spec/vcr/Vhx_Package/_add_video/with_id/returns_package_object.yml +51 -0
  58. data/spec/vcr/Vhx_Package/_create/returns_package_object.yml +140 -0
  59. data/spec/vcr/Vhx_Package/_find/with_hypermedia.yml +243 -0
  60. data/spec/vcr/Vhx_Package/_find/with_id.yml +243 -0
  61. data/spec/vcr/Vhx_Package/_remove_video/with_hypermedia/returns_success.yml +51 -0
  62. data/spec/vcr/Vhx_Package/_remove_video/with_id/returns_success.yml +51 -0
  63. data/spec/vcr/Vhx_Package/associations/are_present.yml +195 -0
  64. data/spec/vcr/Vhx_Site/_create/returns_site_object.yml +91 -0
  65. data/spec/vcr/Vhx_Site/_find/with_hypermedia.yml +195 -0
  66. data/spec/vcr/Vhx_Site/_find/with_id.yml +195 -0
  67. data/spec/vcr/Vhx_User/_find/with_hypermedia.yml +81 -0
  68. data/spec/vcr/Vhx_User/_find/with_id.yml +81 -0
  69. data/spec/vcr/Vhx_User/_me/returns_user_object.yml +720 -0
  70. data/spec/vcr/Vhx_User/_update/returns_user_object.yml +212 -0
  71. data/spec/vcr/Vhx_VhxObject/associations/cache/retreive_if_available.yml +195 -0
  72. data/spec/vcr/Vhx_VhxObject/associations/falls_back_to_links.yml +310 -0
  73. data/spec/vcr/Vhx_Video/_create/returns_video_object.yml +125 -0
  74. data/spec/vcr/Vhx_Video/associations/are_present.yml +294 -0
  75. data/vhx.gemspec +27 -0
  76. metadata +204 -0
@@ -0,0 +1,21 @@
1
+ require_relative '../vhx_list_object'
2
+
3
+ module Vhx
4
+ module ApiOperations
5
+ module List
6
+ module ClassMethods
7
+ def all(payload = {})
8
+ response = Vhx.connection.get do |req|
9
+ req.url '/' + get_klass.downcase + 's'
10
+ req.body = payload
11
+ end
12
+ VhxListObject.new(response.body, get_klass.downcase + 's')
13
+ end
14
+ end
15
+
16
+ def self.included(klass)
17
+ klass.extend(ClassMethods)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Vhx
2
+ module ApiOperations
3
+ module Request
4
+ module ClassMethods
5
+ def find(identifier)
6
+ response = Vhx.connection.get(get_hypermedia(identifier))
7
+ self.new(response.body)
8
+ end
9
+ end
10
+
11
+ def self.included(klass)
12
+ klass.extend(Vhx::HelperMethods)
13
+ klass.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Vhx
2
+ module ApiOperations
3
+ module Update
4
+ module InstanceMethods
5
+ def update(payload)
6
+ Vhx.connection.put(self.href, payload)
7
+ end
8
+ end
9
+
10
+ def self.included(klass)
11
+ klass.include(InstanceMethods)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Vhx
2
+ module HelperMethods
3
+ def get_klass
4
+ if self.is_a?(Class)
5
+ self.to_s.split("::").last
6
+ else
7
+ self.class.to_s.split("::").last
8
+ end
9
+ end
10
+
11
+ def get_hypermedia(identifier, klass = nil)
12
+ if identifier.class.to_s.match(/Integer|Fixnum/)
13
+ klass ||= get_klass
14
+ return Vhx.client.api_base_url + '/' + klass.downcase + 's' + '/' + identifier.to_s #This url is based purely on VHX's API convention (not nested).
15
+ end
16
+
17
+ identifier
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ module Vhx
2
+ class VhxListObject < Array
3
+
4
+ def initialize(obj, list_type)
5
+ @obj = obj
6
+
7
+ if @obj.is_a?(Array)
8
+ ar = @obj.map{|association_hash| Object.const_get("Vhx::#{vhx_object_type(list_type)}").new(association_hash)}
9
+ elsif @obj.is_a?(Hash)
10
+ @previous, @next = @obj['_links']['prev']['href'], @obj['_links']['next']['href']
11
+ @total = @obj['total']
12
+ ar = @obj['_embedded'][list_type].map{|association_hash| Object.const_get("Vhx::#{vhx_object_type(list_type)}").new(association_hash)}
13
+ end
14
+
15
+ super(ar)
16
+ end
17
+
18
+ def previous
19
+ @previous # TODO
20
+ end
21
+
22
+ def next
23
+ @next # TODO
24
+ end
25
+
26
+ def total
27
+ @total
28
+ end
29
+
30
+ protected
31
+
32
+ def vhx_object_type(list_type)
33
+ case list_type
34
+ when 'items'
35
+ 'Collection::Item'
36
+ when 'files'
37
+ 'Video::File'
38
+ else
39
+ list_type.gsub(/s\z/, '').capitalize
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,106 @@
1
+ module Vhx
2
+ class VhxObject
3
+ include HelperMethods
4
+ ASSOCIATION_WHITELIST = ['products', 'sites', 'site', 'videos', 'video', 'subscription', 'files', 'items', 'customer']
5
+
6
+ def initialize(obj_hash)
7
+ @obj_hash = obj_hash
8
+
9
+ validate_class(obj_hash)
10
+ create_readers(obj_hash)
11
+ create_associations(obj_hash)
12
+ end
13
+
14
+ def to_json
15
+ @obj_hash.to_json
16
+ end
17
+
18
+ def to_hash
19
+ @obj_hash
20
+ end
21
+
22
+ def href
23
+ @obj_hash['_links']['self']['href']
24
+ end
25
+
26
+ def links
27
+ data = {}
28
+ @obj_hash['_links'].each do |k, v|
29
+ data[k] = v['href']
30
+ end
31
+ return OpenStruct.new(data)
32
+ end
33
+
34
+ protected
35
+ def validate_class(obj_hash)
36
+ return nil unless obj_hash['_links'].fetch('self', nil)
37
+
38
+ href = obj_hash['_links']['self']['href']
39
+ klass = self.class.to_s.split("::")
40
+ klass.shift #strip out Vhx
41
+ klass = klass.join("_").downcase
42
+
43
+ is_valid_match = case klass
44
+ when 'collection_item'
45
+ href.match('video|file|collection')
46
+ when 'video_file'
47
+ href.match('file')
48
+ else
49
+ href.match(klass)
50
+ end
51
+
52
+ unless is_valid_match
53
+ raise InvalidResourceError.new 'The resource returned from the API does not match the resource requested'
54
+ end
55
+ end
56
+
57
+ def create_readers(obj_hash)
58
+ obj_hash.keys.each do |key|
59
+ next if key.match(/embedded|links/)
60
+ self.class.send(:define_method, key) do
61
+ return obj_hash[key]
62
+ end
63
+ end
64
+ end
65
+
66
+ def create_associations(obj_hash)
67
+ associations = (obj_hash.fetch('_links', {}).keys | obj_hash.fetch('_embedded', {}).keys).select{|k| ASSOCIATION_WHITELIST.include?(k)}
68
+ associations.each do |association_method|
69
+ self.class.send(:define_method, association_method) do |payload = {}|
70
+ if payload.empty? && obj_hash['_embedded'] && obj_hash['_embedded'].fetch(association_method, []).length > 0
71
+ return fetch_embedded_association(obj_hash, association_method)
72
+ end
73
+
74
+ if obj_hash['_links'] && obj_hash['_links'].fetch(association_method, []).length > 0
75
+ return fetch_linked_association(obj_hash, association_method, payload)
76
+ end
77
+
78
+ raise InvalidResourceError.new 'Association does not exist'
79
+ end
80
+ end
81
+ end
82
+
83
+ def fetch_embedded_association(obj_hash, association_method)
84
+ association_obj = obj_hash['_embedded'][association_method]
85
+ build_association(association_obj, association_method)
86
+ end
87
+
88
+ def fetch_linked_association(obj_hash, association_method, payload = {})
89
+ response = Vhx.connection.get do |req|
90
+ req.url(obj_hash['_links'][association_method]['href'].gsub(Vhx.client.api_base_url, ""))
91
+ req.body = payload
92
+ end
93
+
94
+ build_association(response.body, association_method)
95
+ end
96
+
97
+ def build_association(association_obj, association_method)
98
+ # Support for legacy arrays, and new list objects starting with Video resource
99
+ if association_obj.is_a?(Array) || association_obj.has_key?('total')
100
+ return VhxListObject.new(association_obj, association_method)
101
+ end
102
+
103
+ Object.const_get("Vhx::#{association_method.gsub(/s\z/, '').capitalize}").new(association_obj)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,3 @@
1
+ module Vhx
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vhx::Client, :vcr do
4
+ describe 'faraday configuration' do
5
+ subject(:vhx_client){ Vhx::Client.new(application_only_credentials)}
6
+
7
+ it 'uses custom error response middleware' do
8
+ expect(vhx_client.connection.builder.handlers).to include(Vhx::Middleware::ErrorResponse)
9
+ end
10
+
11
+ it 'uses api host' do
12
+ expect(vhx_client.connection.url_prefix.host).to eq 'api.crystal.dev'
13
+ end
14
+ end
15
+
16
+ context 'application_only' do
17
+ subject(:vhx_client){ Vhx::Client.new(application_only_credentials)}
18
+
19
+ describe 'connection' do
20
+ it 'initializes faraday connection' do
21
+ expect(vhx_client.connection).to be_an_instance_of(Faraday::Connection)
22
+ end
23
+
24
+ it 'authorization header presence' do
25
+ expect(vhx_client.connection.headers['Authorization']).to_not be_nil
26
+ end
27
+
28
+ it 'basic auth presence' do
29
+ expect(vhx_client.connection.headers['Authorization']).to match /Basic/
30
+ end
31
+ end
32
+ end
33
+
34
+ context 'application_user' do
35
+ subject(:vhx_client){ Vhx::Client.new(application_user_credentials)}
36
+
37
+ it 'oauth_token presence' do
38
+ expect(vhx_client.oauth_token).to be_an_instance_of(OAuthToken)
39
+ end
40
+
41
+ describe 'connection' do
42
+ it 'initializes faraday connection' do
43
+ expect(vhx_client.connection).to be_an_instance_of(Faraday::Connection)
44
+ end
45
+
46
+ it 'authorization header presence' do
47
+ expect(vhx_client.connection.headers['Authorization']).to_not be_nil
48
+ end
49
+
50
+ it 'Bearer auth presence' do
51
+ expect(vhx_client.connection.headers['Authorization']).to match /Bearer/
52
+ end
53
+ end
54
+
55
+ describe '#refresh_access_token!' do
56
+ subject(:oauth_token){ vhx_client.oauth_token }
57
+ let!(:original_access_token){ oauth_token.access_token }
58
+ before { vhx_client.refresh_access_token! }
59
+
60
+ it 'oauth_token refreshed' do
61
+ expect(vhx_client.oauth_token.refreshed).to eq(true)
62
+ end
63
+
64
+ it 'access_token changed' do
65
+ expect(vhx_client.oauth_token.access_token).to_not eq(original_access_token)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ [{
2
+ "_links": {
3
+ "self": { "href": "https://api.vhx.tv/videos/1/files?quality=adaptive&format=m3u8" },
4
+ "video": { "href": "https://api.vhx.tv/videos/1" },
5
+ "site": { "href": "https://api.vhx.tv/sites/1" }
6
+ },
7
+ "quality": "adaptive",
8
+ "format": "m3u8",
9
+ "method": "hls",
10
+ "size": {
11
+ "bytes": 1073741824,
12
+ "formatted": "1 GB"
13
+ },
14
+ "mime_type": "application/x-mpegURL"
15
+ }]
@@ -0,0 +1,18 @@
1
+ {
2
+ "_links": {
3
+ "self": { "href": "http://api.crystal.dev/videos/3008/files?quality=adaptive&format=m3u8" },
4
+ "video": { "href": "http://api.crystal.dev/videos/3008" },
5
+ "site": { "href": "http://api.crystal.dev/sites/1900" },
6
+ "source": { "href": "http://video.crystal.dev/mymovie/adaptive.m3u8?token=f4v3v3i4c2o209_3" }
7
+ },
8
+ "quality": "adaptive",
9
+ "format": "m3u8",
10
+ "method": "hls",
11
+ "size": {
12
+ "bytes": 1073741824,
13
+ "formatted": "1 GB"
14
+ },
15
+ "mime_type": "application/x-mpegURL",
16
+ "created_at": "2014-02-25T20:19:30Z",
17
+ "updated_at": "2014-02-25T20:19:30Z"
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "_links": {
3
+ "prev": {"href": "pending"},
4
+ "next": {"href": "pending"}
5
+ },
6
+ "_embedded": {
7
+ "files": [{
8
+ "_links": {
9
+ "self": { "href": "https://api.vhx.tv/videos/1/files?quality=adaptive&format=m3u8" },
10
+ "video": { "href": "https://api.vhx.tv/videos/1" },
11
+ "site": { "href": "https://api.vhx.tv/sites/1" }
12
+ },
13
+ "quality": "adaptive",
14
+ "format": "m3u8",
15
+ "method": "hls",
16
+ "size": {
17
+ "bytes": 1073741824,
18
+ "formatted": "1 GB"
19
+ },
20
+ "mime_type": "application/x-mpegURL"
21
+ }]
22
+ }
23
+ }
@@ -0,0 +1,185 @@
1
+ {
2
+ "_links": {
3
+ "self": {
4
+ "href": "http://api.crystal.dev/packages/1025"
5
+ },
6
+ "site": {
7
+ "href": "http://api.crystal.dev/sites/1900"
8
+ },
9
+ "videos": {
10
+ "href": "http://api.crystal.dev/packages/1025/videos"
11
+ },
12
+ "buy_page": {
13
+ "href": "http://sagartestmovie.crystal.dev/buy/testing123"
14
+ },
15
+ "home_page": {
16
+ "href": "http://sagartestmovie.crystal.dev/packages/testing123"
17
+ }
18
+ },
19
+ "_embedded": {
20
+ "site": {
21
+ "_links": {
22
+ "self": {
23
+ "href": "http://api.crystal.dev/sites/1900"
24
+ },
25
+ "home_page": {
26
+ "href": "http://sagartestmovie.crystal.dev"
27
+ },
28
+ "followers": {
29
+ "href": "http://api.crystal.dev/sites/1900/followers"
30
+ }
31
+ },
32
+ "id": 1900,
33
+ "title": "SagarTestMovie",
34
+ "description": "This is my test movie. ",
35
+ "domain": "sagartestmovie.crystal.dev",
36
+ "subdomain": "sagartestmovie",
37
+ "key": "sagartestmovie",
38
+ "color": "#28DBF7",
39
+ "facebook_url": null,
40
+ "twitter_name": null,
41
+ "google_analytics_id": "",
42
+ "packages_count": 2,
43
+ "videos_count": 4,
44
+ "followers_count": 31,
45
+ "created_at": "2013-12-27T00:20:28Z",
46
+ "updated_at": "2015-05-04T20:46:01Z"
47
+ },
48
+ "videos": [
49
+ {
50
+ "_links": {
51
+ "self": {
52
+ "href": "http://api.crystal.dev/videos/3008"
53
+ },
54
+ "site": {
55
+ "href": "http://api.crystal.dev/sites/1900"
56
+ },
57
+ "files": {
58
+ "href": "http://api.crystal.dev/videos/3008/files"
59
+ }
60
+ },
61
+ "_embedded": {
62
+ "subtitles": [
63
+ {
64
+ "_links": {
65
+ "self": {
66
+ "href": "http://api.crystal.dev/videos/7726"
67
+ },
68
+ "video": {
69
+ "href": "http://api.crystal.dev/videos/3008"
70
+ },
71
+ "site": {
72
+ "href": "http://api.crystal.dev/sites/1900"
73
+ }
74
+ },
75
+ "language": "Bengali",
76
+ "type": "video"
77
+ },
78
+ {
79
+ "_links": {
80
+ "self": {
81
+ "href": "http://api.crystal.dev/videos/5558"
82
+ },
83
+ "video": {
84
+ "href": "http://api.crystal.dev/videos/3008"
85
+ },
86
+ "site": {
87
+ "href": "http://api.crystal.dev/sites/1900"
88
+ }
89
+ },
90
+ "language": "English",
91
+ "type": "video"
92
+ }
93
+ ]
94
+ },
95
+ "id": 3008,
96
+ "title": "Test Movie",
97
+ "description": "Testing",
98
+ "status": "uploaded",
99
+ "duration": {
100
+ "seconds": 19,
101
+ "formatted": "00:00:19"
102
+ },
103
+ "thumbnail": {
104
+ "small": "https://dv6n47kh00ig2.cloudfront.net/sagartestmovie/assets/images/video-3008-1403716255-240x135.jpg",
105
+ "medium": "https://dv6n47kh00ig2.cloudfront.net/sagartestmovie/assets/images/video-3008-1403716255-640x360.jpg",
106
+ "large": "https://dv6n47kh00ig2.cloudfront.net/sagartestmovie/assets/images/video-3008-1403716255-1280x720.jpg"
107
+ },
108
+ "is_drm_enabled": false,
109
+ "subtitles_count": 2,
110
+ "files_count": 5,
111
+ "created_at": "2014-02-06T05:30:18Z",
112
+ "updated_at": "2015-02-18T16:21:24Z",
113
+ "position": 0
114
+ },
115
+ {
116
+ "_links": {
117
+ "self": {
118
+ "href": "http://api.crystal.dev/videos/7830"
119
+ },
120
+ "site": {
121
+ "href": "http://api.crystal.dev/sites/1900"
122
+ },
123
+ "files": {
124
+ "href": "http://api.crystal.dev/videos/7830/files"
125
+ }
126
+ },
127
+ "_embedded": {
128
+ "subtitles": [
129
+
130
+ ]
131
+ },
132
+ "id": 7830,
133
+ "title": "test movie",
134
+ "description": null,
135
+ "status": null,
136
+ "duration": {
137
+ "seconds": 0,
138
+ "formatted": "00:00:00"
139
+ },
140
+ "thumbnail": {
141
+ "small": "http://cdn.crystal.dev/assets/thumbnails/default-small.png",
142
+ "medium": "http://cdn.crystal.dev/assets/thumbnails/default-medium.png",
143
+ "large": "http://cdn.crystal.dev/assets/thumbnails/default-large.png"
144
+ },
145
+ "is_drm_enabled": false,
146
+ "subtitles_count": 0,
147
+ "files_count": 0,
148
+ "created_at": "2014-07-14T15:19:03Z",
149
+ "updated_at": "2015-05-04T19:36:50Z",
150
+ "position": 1
151
+ }
152
+ ],
153
+ "trailer": null
154
+ },
155
+ "id": 1025,
156
+ "title": "testing123",
157
+ "description": "test",
158
+ "sku": "testing123",
159
+ "price": {
160
+ "cents": 200,
161
+ "currency": "USD",
162
+ "formatted": "$2",
163
+ "US": {
164
+ "cents": 200,
165
+ "currency": "USD",
166
+ "formatted": "$2"
167
+ }
168
+ },
169
+ "rental": null,
170
+ "trailer_url": null,
171
+ "trailer_embed_code": "<iframe src=\"http://embed.crystal.dev/packages/1025\" width=\"640\" height=\"360\" frameborder=\"0\" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>",
172
+ "thumbnail": {
173
+ "small": "http://cdn.crystal.dev/assets/thumbnails/default-small.png",
174
+ "medium": "http://cdn.crystal.dev/assets/thumbnails/default-medium.png",
175
+ "large": "http://cdn.crystal.dev/assets/thumbnails/default-large.png",
176
+ "blurred": null
177
+ },
178
+ "is_active": false,
179
+ "is_preorder": false,
180
+ "release_date": null,
181
+ "videos_count": 2,
182
+ "extras_count": 0,
183
+ "created_at": "2014-02-06T18:00:39Z",
184
+ "updated_at": "2015-02-18T16:21:15Z"
185
+ }