conversant 1.0.16
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.
- checksums.yaml +7 -0
- data/.env.example +39 -0
- data/.gitignore +52 -0
- data/.gitlab-ci.yml +108 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +487 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +860 -0
- data/RELEASE.md +726 -0
- data/Rakefile +21 -0
- data/conversant.gemspec +49 -0
- data/examples/inheritance_integration.rb +348 -0
- data/examples/rails_initializer.rb +69 -0
- data/lib/conversant/configuration.rb +132 -0
- data/lib/conversant/v3/base.rb +47 -0
- data/lib/conversant/v3/http_client.rb +456 -0
- data/lib/conversant/v3/mixins/authentication.rb +221 -0
- data/lib/conversant/v3/services/authorization.rb +194 -0
- data/lib/conversant/v3/services/cdn/analytics.rb +483 -0
- data/lib/conversant/v3/services/cdn/audit.rb +71 -0
- data/lib/conversant/v3/services/cdn/business.rb +122 -0
- data/lib/conversant/v3/services/cdn/certificate.rb +180 -0
- data/lib/conversant/v3/services/cdn/dashboard.rb +109 -0
- data/lib/conversant/v3/services/cdn/domain.rb +223 -0
- data/lib/conversant/v3/services/cdn/monitoring.rb +65 -0
- data/lib/conversant/v3/services/cdn/partner/analytics.rb +233 -0
- data/lib/conversant/v3/services/cdn/partner.rb +60 -0
- data/lib/conversant/v3/services/cdn.rb +221 -0
- data/lib/conversant/v3/services/lms/dashboard.rb +99 -0
- data/lib/conversant/v3/services/lms/domain.rb +108 -0
- data/lib/conversant/v3/services/lms/job.rb +211 -0
- data/lib/conversant/v3/services/lms/partner/analytics.rb +266 -0
- data/lib/conversant/v3/services/lms/partner/business.rb +151 -0
- data/lib/conversant/v3/services/lms/partner/report.rb +170 -0
- data/lib/conversant/v3/services/lms/partner.rb +58 -0
- data/lib/conversant/v3/services/lms/preset.rb +57 -0
- data/lib/conversant/v3/services/lms.rb +173 -0
- data/lib/conversant/v3/services/oss/partner/analytics.rb +105 -0
- data/lib/conversant/v3/services/oss/partner.rb +48 -0
- data/lib/conversant/v3/services/oss.rb +128 -0
- data/lib/conversant/v3/services/portal/dashboard.rb +114 -0
- data/lib/conversant/v3/services/portal.rb +219 -0
- data/lib/conversant/v3/services/vms/analytics.rb +114 -0
- data/lib/conversant/v3/services/vms/business.rb +190 -0
- data/lib/conversant/v3/services/vms/partner/analytics.rb +133 -0
- data/lib/conversant/v3/services/vms/partner/business.rb +90 -0
- data/lib/conversant/v3/services/vms/partner.rb +57 -0
- data/lib/conversant/v3/services/vms/transcoding.rb +184 -0
- data/lib/conversant/v3/services/vms.rb +166 -0
- data/lib/conversant/v3.rb +36 -0
- data/lib/conversant/version.rb +5 -0
- data/lib/conversant.rb +108 -0
- data/publish.sh +107 -0
- data/sig/conversant/v3/services/authorization.rbs +34 -0
- data/sig/conversant/v3/services/cdn.rbs +123 -0
- data/sig/conversant/v3/services/lms.rbs +80 -0
- data/sig/conversant/v3/services/portal.rbs +22 -0
- data/sig/conversant/v3/services/vms.rbs +64 -0
- data/sig/conversant/v3.rbs +85 -0
- data/sig/conversant.rbs +37 -0
- metadata +267 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'authorization'
|
|
4
|
+
|
|
5
|
+
module Conversant
|
|
6
|
+
module V3
|
|
7
|
+
module Services
|
|
8
|
+
# CDN service client for Conversant Content Delivery Network
|
|
9
|
+
#
|
|
10
|
+
# Provides comprehensive access to CDN functionality including:
|
|
11
|
+
# - Analytics and reporting (bandwidth, volume, viewers, RPS)
|
|
12
|
+
# - Domain management and configuration
|
|
13
|
+
# - SSL certificate management
|
|
14
|
+
# - Business metrics and monitoring
|
|
15
|
+
# - Audit logging and compliance
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage
|
|
18
|
+
# cdn = Conversant::V3.cdn(customer_id)
|
|
19
|
+
#
|
|
20
|
+
# # Get bandwidth analytics
|
|
21
|
+
# bandwidth = cdn.analytics.bandwidths({
|
|
22
|
+
# domain: "All",
|
|
23
|
+
# startTime: "2025-01-01T00:00:00Z",
|
|
24
|
+
# endTime: "2025-01-31T23:59:59Z",
|
|
25
|
+
# interval: "hour"
|
|
26
|
+
# })
|
|
27
|
+
#
|
|
28
|
+
# # Manage domains
|
|
29
|
+
# domains = cdn.domain.all
|
|
30
|
+
# new_domain = cdn.domain.create(domain_config)
|
|
31
|
+
#
|
|
32
|
+
# # Business metrics
|
|
33
|
+
# traffic = cdn.business.bandwidth(time_range)
|
|
34
|
+
#
|
|
35
|
+
# @since 1.0.0
|
|
36
|
+
class CDN < ::Conversant::V3::Base
|
|
37
|
+
include Authorization
|
|
38
|
+
|
|
39
|
+
# Get analytics service instance
|
|
40
|
+
#
|
|
41
|
+
# @return [Analytics] analytics service for CDN metrics and reporting
|
|
42
|
+
# @since 1.0.0
|
|
43
|
+
def analytics
|
|
44
|
+
@analytics ||= Analytics.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get monitoring service instance
|
|
48
|
+
#
|
|
49
|
+
# @return [Monitoring] monitoring service for real-time CDN performance
|
|
50
|
+
# @since 1.0.0
|
|
51
|
+
def monitoring
|
|
52
|
+
@monitoring ||= Monitoring.new(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get audit service instance
|
|
56
|
+
#
|
|
57
|
+
# @return [Audit] audit service for activity logging and compliance
|
|
58
|
+
# @since 1.0.0
|
|
59
|
+
def audit
|
|
60
|
+
@audit ||= Audit.new(self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get dashboard service instance
|
|
64
|
+
#
|
|
65
|
+
# @return [Dashboard] dashboard service for quick daily metrics
|
|
66
|
+
# @since 1.0.8
|
|
67
|
+
def dashboard
|
|
68
|
+
@dashboard ||= Dashboard.new(self)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get domain management service instance
|
|
72
|
+
#
|
|
73
|
+
# @return [Domain] domain service for CDN domain management
|
|
74
|
+
# @since 1.0.1
|
|
75
|
+
def domain
|
|
76
|
+
@domain ||= Domain.new(self)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get business metrics service instance
|
|
80
|
+
#
|
|
81
|
+
# @return [Business] business service for billing and usage metrics
|
|
82
|
+
# @since 1.0.1
|
|
83
|
+
def business
|
|
84
|
+
@business ||= Business.new(self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get certificate management service instance
|
|
88
|
+
#
|
|
89
|
+
# @return [Certificate] certificate service for SSL/TLS management
|
|
90
|
+
# @since 1.0.1
|
|
91
|
+
def certificate
|
|
92
|
+
@certificate ||= Certificate.new(self)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get partner service instance
|
|
96
|
+
#
|
|
97
|
+
# Provides access to partner-level analytics and reporting that aggregate
|
|
98
|
+
# data across multiple customer accounts.
|
|
99
|
+
#
|
|
100
|
+
# @return [Partner] partner service for CDN and OSS analytics
|
|
101
|
+
# @since 1.0.12
|
|
102
|
+
#
|
|
103
|
+
# @example Access partner analytics
|
|
104
|
+
# cdn = Conversant::V3.cdn(12345)
|
|
105
|
+
# bandwidth = cdn.partner.analytics.bandwidth(payload)
|
|
106
|
+
# storage = cdn.partner.oss.storage_usage(payload)
|
|
107
|
+
def partner
|
|
108
|
+
@partner ||= Partner.new(self)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @deprecated Use {#partner} instead
|
|
112
|
+
# Get partner analytics service instance (deprecated)
|
|
113
|
+
#
|
|
114
|
+
# @return [CDN::Partner::Analytics] partner analytics for CDN reporting
|
|
115
|
+
# @since 1.0.8
|
|
116
|
+
# @deprecated Use `cdn.partner.analytics` instead of `cdn.partner_analytics`
|
|
117
|
+
def partner_analytics
|
|
118
|
+
@partner_analytics ||= Partner::Analytics.new(self)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @deprecated Use {Conversant::V3.oss} instead
|
|
122
|
+
# Get partner OSS analytics service instance (deprecated)
|
|
123
|
+
#
|
|
124
|
+
# @return [Conversant::V3::Services::OSS::Partner::Analytics] partner analytics for OSS storage reporting
|
|
125
|
+
# @since 1.0.8
|
|
126
|
+
# @deprecated Use `oss = Conversant::V3.oss(customer_id); oss.partner.analytics` instead of `cdn.partner_oss`
|
|
127
|
+
def partner_oss
|
|
128
|
+
@partner_oss ||= Conversant::V3::Services::OSS::Partner::Analytics.new(self)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
protected
|
|
132
|
+
|
|
133
|
+
def call(method, uri, payload = nil)
|
|
134
|
+
url = "#{configuration.private_cdn_endpoint}#{uri}"
|
|
135
|
+
|
|
136
|
+
headers = authorized_headers
|
|
137
|
+
|
|
138
|
+
# Check for SESSION in Cookie header
|
|
139
|
+
if headers['Cookie'].nil? || !headers['Cookie'].include?('SESSION=')
|
|
140
|
+
logger.error "#{identifier}.METHOD:call.NO_SESSION"
|
|
141
|
+
raise AuthenticationError, 'Missing SESSION for CDN'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
code, response = request(method, url, payload, headers)
|
|
145
|
+
|
|
146
|
+
if code >= 400
|
|
147
|
+
logger.error "#{identifier}.METHOD:call.HTTP_ERROR:#{code}"
|
|
148
|
+
raise ApiError, "CDN API error: #{code}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
response.body
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def fetch_new_session
|
|
157
|
+
sessions = authenticate
|
|
158
|
+
return nil unless sessions && sessions[:session] && sessions[:sso_gw_session2]
|
|
159
|
+
|
|
160
|
+
signature_url = "#{configuration.private_cdn_endpoint}/partner-dashboard?customerId=#{customer_id}"
|
|
161
|
+
logger.debug "#{identifier}.METHOD:authorize.REQUESTING_SESSION"
|
|
162
|
+
|
|
163
|
+
response = RestClient.get(
|
|
164
|
+
signature_url,
|
|
165
|
+
{
|
|
166
|
+
authority: URI.parse(configuration.private_cdn_endpoint).hostname,
|
|
167
|
+
referer: portal_endpoint,
|
|
168
|
+
user_agent: configuration.default_ua,
|
|
169
|
+
cookies: {
|
|
170
|
+
'SESSION': sessions[:session],
|
|
171
|
+
'SSO_GW_SESSION2': sessions[:sso_gw_session2]
|
|
172
|
+
},
|
|
173
|
+
timeout: 30,
|
|
174
|
+
open_timeout: 30
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if response.cookies['SESSION']
|
|
179
|
+
session_cookie = response.cookies['SESSION']
|
|
180
|
+
redis.set(session_cache_key, session_cookie, ex: configuration.cache_ttl)
|
|
181
|
+
logger.debug "#{identifier}.METHOD:authorize.SESSION_OBTAINED"
|
|
182
|
+
session_cookie
|
|
183
|
+
else
|
|
184
|
+
logger.error "#{identifier}.METHOD:authorize.NO_SESSION_IN_RESPONSE"
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
rescue RestClient::Unauthorized, RestClient::Forbidden => e
|
|
188
|
+
logger.error "#{identifier}.METHOD:authorize.AUTH_ERROR:#{e.message}"
|
|
189
|
+
nil
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
logger.error "#{identifier}.METHOD:authorize.ERROR:#{e.message}"
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
protected
|
|
196
|
+
|
|
197
|
+
def service_endpoint
|
|
198
|
+
configuration.private_cdn_endpoint
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def session_cookie_name
|
|
202
|
+
'SESSION'
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def requires_session?
|
|
206
|
+
true
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Load nested service classes after CDN is defined
|
|
214
|
+
require_relative 'cdn/analytics'
|
|
215
|
+
require_relative 'cdn/monitoring'
|
|
216
|
+
require_relative 'cdn/audit'
|
|
217
|
+
require_relative 'cdn/dashboard'
|
|
218
|
+
require_relative 'cdn/domain'
|
|
219
|
+
require_relative 'cdn/business'
|
|
220
|
+
require_relative 'cdn/certificate'
|
|
221
|
+
require_relative 'cdn/partner'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conversant
|
|
4
|
+
module V3
|
|
5
|
+
module Services
|
|
6
|
+
class LMS
|
|
7
|
+
class Dashboard
|
|
8
|
+
attr_reader :parent
|
|
9
|
+
|
|
10
|
+
def initialize(parent)
|
|
11
|
+
@parent = parent
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def live
|
|
15
|
+
payload = { _: (Time.now.to_f * 1000).to_i }
|
|
16
|
+
response = @parent.send(:call, 'GET', "/v4/live/duration?#{payload.to_query}")
|
|
17
|
+
return nil if response.nil?
|
|
18
|
+
|
|
19
|
+
JSON.parse(response)
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def vod
|
|
26
|
+
payload = { _: (Time.now.to_f * 1000).to_i }
|
|
27
|
+
response = @parent.send(:call, 'GET', "/v4/live/duration?#{payload.to_query}")
|
|
28
|
+
return nil if response.nil?
|
|
29
|
+
|
|
30
|
+
JSON.parse(response)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dvr
|
|
37
|
+
payload = { _: (Time.now.to_f * 1000).to_i }
|
|
38
|
+
response = @parent.send(:call, 'GET', "/v4/dvr/duration?#{payload.to_query}")
|
|
39
|
+
return nil if response.nil?
|
|
40
|
+
|
|
41
|
+
JSON.parse(response)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def recent_jobs
|
|
48
|
+
response = @parent.send(:call, 'GET', '/dashboard/dashboard')
|
|
49
|
+
return [] if response.nil?
|
|
50
|
+
|
|
51
|
+
# Parse HTML response to extract job data
|
|
52
|
+
require 'nokogiri' if defined?(Nokogiri)
|
|
53
|
+
return [] unless defined?(Nokogiri)
|
|
54
|
+
|
|
55
|
+
doc = Nokogiri::HTML.parse(response)
|
|
56
|
+
table = doc.at('table')
|
|
57
|
+
return [] if table.nil?
|
|
58
|
+
|
|
59
|
+
jobs = table.search('tr').map do |row|
|
|
60
|
+
cells = row.search('td')
|
|
61
|
+
next [] if cells.length.zero?
|
|
62
|
+
|
|
63
|
+
app_name, stream_name, type, created, started, recording, status = cells
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
app_name: app_name&.text&.strip,
|
|
67
|
+
stream_name: stream_name&.text&.strip,
|
|
68
|
+
type: type&.text&.strip,
|
|
69
|
+
created: parse_timestamp(created&.text),
|
|
70
|
+
started: parse_timestamp(started&.text),
|
|
71
|
+
recording: recording&.text&.strip,
|
|
72
|
+
status: status&.text&.strip
|
|
73
|
+
}
|
|
74
|
+
end.flatten.compact
|
|
75
|
+
|
|
76
|
+
jobs
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def parse_timestamp(timestamp_text)
|
|
85
|
+
return nil unless timestamp_text
|
|
86
|
+
|
|
87
|
+
Time.at(timestamp_text.to_i / 1000)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def logger
|
|
93
|
+
@parent.send(:logger)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conversant
|
|
4
|
+
module V3
|
|
5
|
+
module Services
|
|
6
|
+
class LMS
|
|
7
|
+
class Domain
|
|
8
|
+
attr_reader :parent
|
|
9
|
+
|
|
10
|
+
def initialize(parent)
|
|
11
|
+
@parent = parent
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all(params = {})
|
|
15
|
+
default_payload = {
|
|
16
|
+
sEcho: 1,
|
|
17
|
+
iColumns: 1,
|
|
18
|
+
sColumns: nil,
|
|
19
|
+
iDisplayStart: 0,
|
|
20
|
+
iDisplayLength: 20,
|
|
21
|
+
mDataProp_0: 'domain',
|
|
22
|
+
mDataProp_1: 'app',
|
|
23
|
+
mDataProp_2: 'stream',
|
|
24
|
+
mDataProp_3: 'created',
|
|
25
|
+
mDataProp_4: 'id',
|
|
26
|
+
iSortingCols: 0,
|
|
27
|
+
bSortable_0: 'false',
|
|
28
|
+
bSortable_1: 'false',
|
|
29
|
+
bSortable_2: 'false',
|
|
30
|
+
bSortable_3: 'false',
|
|
31
|
+
bSortable_4: 'false',
|
|
32
|
+
page_number: 1,
|
|
33
|
+
page_size: 20,
|
|
34
|
+
sort_field: nil,
|
|
35
|
+
sort_type: nil,
|
|
36
|
+
_: (Time.now.to_f * 1000).to_i
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
merged_payload = default_payload.merge(params)
|
|
40
|
+
response = @parent.send(:call, 'GET', "/live/domain?#{merged_payload.to_query}")
|
|
41
|
+
return nil if response.nil?
|
|
42
|
+
|
|
43
|
+
response = JSON.parse(response)
|
|
44
|
+
return nil unless response&.[]('list')
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
current_page_number: response['current_page_number'],
|
|
48
|
+
has_next: response['has_next'],
|
|
49
|
+
has_pre: response['has_pre'],
|
|
50
|
+
total_count: response['total_count'],
|
|
51
|
+
total_page_number: response['total_page_number'],
|
|
52
|
+
data: response['list']
|
|
53
|
+
}
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def show(domain_id)
|
|
60
|
+
response = @parent.send(:call, 'GET', "/live/domain/#{domain_id}")
|
|
61
|
+
return nil if response.nil?
|
|
62
|
+
|
|
63
|
+
JSON.parse(response)
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create(payload)
|
|
70
|
+
response = @parent.send(:call, 'POST', '/live/domain', payload)
|
|
71
|
+
return false if response.nil?
|
|
72
|
+
|
|
73
|
+
JSON.parse(response)&.[]('status') == 'ok'
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update(payload)
|
|
80
|
+
response = @parent.send(:call, 'PUT', '/live/domain', payload)
|
|
81
|
+
return false if response.nil?
|
|
82
|
+
|
|
83
|
+
JSON.parse(response)&.[]('status') == 'ok'
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def delete(domain_id)
|
|
90
|
+
response = @parent.send(:call, 'DELETE', "/live/domain/#{domain_id}")
|
|
91
|
+
return false if response.nil?
|
|
92
|
+
|
|
93
|
+
JSON.parse(response)&.[]('status') == 'ok'
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
logger.error "#{@parent.send(:identifier)}.METHOD:#{__method__}.EXCEPTION:#{e.message}"
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def logger
|
|
102
|
+
@parent.send(:logger)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conversant
|
|
4
|
+
module V3
|
|
5
|
+
module Services
|
|
6
|
+
class LMS
|
|
7
|
+
# Job management service for live streaming operations
|
|
8
|
+
#
|
|
9
|
+
# Provides comprehensive job management functionality for live streaming including:
|
|
10
|
+
# - Job listing and filtering
|
|
11
|
+
# - Job status tracking
|
|
12
|
+
# - Stream profile information
|
|
13
|
+
# - Manifest URL generation
|
|
14
|
+
#
|
|
15
|
+
# @example Query streaming jobs
|
|
16
|
+
# lms = Conversant::V3.lms(12345)
|
|
17
|
+
#
|
|
18
|
+
# # Get all streaming jobs
|
|
19
|
+
# jobs = lms.job.where(status: 'streaming')
|
|
20
|
+
#
|
|
21
|
+
# jobs.each do |job|
|
|
22
|
+
# puts "Job #{job[:id]}: #{job[:name]} - #{job[:status]}"
|
|
23
|
+
# puts " Manifest: #{job[:manifest]}"
|
|
24
|
+
# puts " Profiles: #{job[:profiles].length}"
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @since 1.0.0
|
|
28
|
+
class Job
|
|
29
|
+
# @return [LMS] the parent LMS service instance
|
|
30
|
+
attr_reader :parent
|
|
31
|
+
|
|
32
|
+
# Initialize job service
|
|
33
|
+
#
|
|
34
|
+
# @param parent [LMS] the parent LMS service instance
|
|
35
|
+
def initialize(parent)
|
|
36
|
+
@parent = parent
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Query streaming jobs with filters
|
|
40
|
+
#
|
|
41
|
+
# Retrieves a list of streaming jobs matching the specified filters.
|
|
42
|
+
# Returns detailed information about each job including status, profiles,
|
|
43
|
+
# and manifest URLs.
|
|
44
|
+
#
|
|
45
|
+
# @param params [Hash] query parameters
|
|
46
|
+
# @option params [String] :status job status filter ('streaming', 'ready', 'interrupted')
|
|
47
|
+
# @option params [Integer] :limit maximum number of results
|
|
48
|
+
# @option params [Integer] :offset pagination offset
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<Hash>, nil] array of job hashes with details, or nil on error
|
|
51
|
+
# Each hash contains:
|
|
52
|
+
# - :entity [Integer] OSP entity ID
|
|
53
|
+
# - :id [Integer] job ID
|
|
54
|
+
# - :app [String] application name
|
|
55
|
+
# - :name [String] stream name
|
|
56
|
+
# - :status [String] job status ('ready', 'streaming', 'interrupted')
|
|
57
|
+
# - :created_at [DateTime] job creation timestamp
|
|
58
|
+
# - :started_at [DateTime] job start timestamp
|
|
59
|
+
# - :ended_at [DateTime] job end timestamp
|
|
60
|
+
# - :client [String] client IP address
|
|
61
|
+
# - :encoder [String] encoder IP address
|
|
62
|
+
# - :server [String] execution server
|
|
63
|
+
# - :manifest [String] manifest URL
|
|
64
|
+
# - :profiles [Array<Hash>] array of stream profiles
|
|
65
|
+
# - :transcoding [Boolean] transcoding enabled
|
|
66
|
+
# - :gpu [Boolean] GPU acceleration enabled
|
|
67
|
+
#
|
|
68
|
+
# @example Get streaming jobs
|
|
69
|
+
# jobs = lms.job.where(status: 'streaming', limit: 50)
|
|
70
|
+
# jobs.each do |job|
|
|
71
|
+
# puts "#{job[:name]} (#{job[:status]}) - #{job[:manifest]}"
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# @example Get all jobs
|
|
75
|
+
# all_jobs = lms.job.where
|
|
76
|
+
#
|
|
77
|
+
# @since 1.0.0
|
|
78
|
+
def where(**params)
|
|
79
|
+
response = JSON.parse(@parent.send(:call, 'GET', "/v4/streams?#{params.to_query}"))
|
|
80
|
+
return nil unless response
|
|
81
|
+
|
|
82
|
+
items = response['list'] || response[:list] || []
|
|
83
|
+
|
|
84
|
+
items.map do |item|
|
|
85
|
+
status = item['status']&.to_i || item[:status]&.to_i
|
|
86
|
+
inbound = item['stream_rtmp_inbound'] || item[:stream_rtmp_inbound]
|
|
87
|
+
|
|
88
|
+
profiles = (item['stream_rtmp_outbounds'] || item[:stream_rtmp_outbounds])&.map do |stream|
|
|
89
|
+
{
|
|
90
|
+
preset: stream['preset_name'] || stream[:preset_name],
|
|
91
|
+
type: stream['type'] || stream[:type],
|
|
92
|
+
profile: build_profile_url(inbound, item, stream)
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
entity: item['osp_id'] || item[:osp_id],
|
|
98
|
+
id: item['id'] || item[:id],
|
|
99
|
+
app: item['app'] || item[:app],
|
|
100
|
+
name: item['stream'] || item[:stream],
|
|
101
|
+
status: parse_status(status),
|
|
102
|
+
created_at: parse_timestamp(item['created'] || item[:created]),
|
|
103
|
+
started_at: parse_timestamp(item['started'] || item[:started]),
|
|
104
|
+
ended_at: parse_timestamp(item['ended'] || item[:ended]),
|
|
105
|
+
client: inbound && (inbound['client_ip'] || inbound[:client_ip]),
|
|
106
|
+
encoder: extract_encoder(item),
|
|
107
|
+
server: item['exec_ta'] || item[:exec_ta],
|
|
108
|
+
manifest: build_manifest_url(inbound, item, profiles),
|
|
109
|
+
profiles: profiles || [],
|
|
110
|
+
transcoding: item['transcoding'] || item[:transcoding],
|
|
111
|
+
gpu: (item['gpu'] || item[:gpu]) == 'gpu'
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
@parent.send(:logger).error "LMS::Job.where error: #{e.message}"
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Parse numeric status code to string
|
|
122
|
+
#
|
|
123
|
+
# @param status [Integer] numeric status code
|
|
124
|
+
# @return [String, Integer] status string or original code if unknown
|
|
125
|
+
def parse_status(status)
|
|
126
|
+
case status
|
|
127
|
+
when 1 then 'ready'
|
|
128
|
+
when 2 then 'streaming'
|
|
129
|
+
when 12 then 'interrupted'
|
|
130
|
+
else status
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse Unix timestamp (milliseconds) to DateTime
|
|
135
|
+
#
|
|
136
|
+
# @param timestamp [Integer] Unix timestamp in milliseconds
|
|
137
|
+
# @return [DateTime, nil] parsed DateTime or nil if invalid
|
|
138
|
+
def parse_timestamp(timestamp)
|
|
139
|
+
return nil unless timestamp
|
|
140
|
+
|
|
141
|
+
Time.at(timestamp.to_i / 1000).to_datetime
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Extract encoder IP from stream metadata
|
|
147
|
+
#
|
|
148
|
+
# @param item [Hash] job item hash
|
|
149
|
+
# @return [String] encoder IP address or '-' if not found
|
|
150
|
+
def extract_encoder(item)
|
|
151
|
+
metadata = item['stream_metadata'] || item[:stream_metadata]
|
|
152
|
+
metadata && (metadata['source_ip'] || metadata[:source_ip]) || '-'
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get file extension for stream type
|
|
156
|
+
#
|
|
157
|
+
# @param type [String] stream type ('hls', 'dash')
|
|
158
|
+
# @return [String] file extension with dot
|
|
159
|
+
def stream_extension(type)
|
|
160
|
+
case type
|
|
161
|
+
when 'hls' then '.m3u8'
|
|
162
|
+
when 'dash' then '.mpd'
|
|
163
|
+
else ''
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Build profile URL for a stream
|
|
168
|
+
#
|
|
169
|
+
# @param inbound [Hash] inbound stream configuration
|
|
170
|
+
# @param item [Hash] job item hash
|
|
171
|
+
# @param stream [Hash] stream profile configuration
|
|
172
|
+
# @return [String, nil] profile URL or nil if inbound is missing
|
|
173
|
+
def build_profile_url(inbound, item, stream)
|
|
174
|
+
return nil unless inbound
|
|
175
|
+
|
|
176
|
+
domain = inbound['domain'] || inbound[:domain]
|
|
177
|
+
app = item['app'] || item[:app]
|
|
178
|
+
stream_name = item['stream'] || item[:stream]
|
|
179
|
+
preset = stream['preset_name'] || stream[:preset_name]
|
|
180
|
+
type = stream['type'] || stream[:type]
|
|
181
|
+
|
|
182
|
+
"http://#{domain}/#{app}/#{stream_name}-#{preset}#{stream_extension(type)}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Build manifest URL for a job
|
|
186
|
+
#
|
|
187
|
+
# @param inbound [Hash] inbound stream configuration
|
|
188
|
+
# @param item [Hash] job item hash
|
|
189
|
+
# @param profiles [Array<Hash>] array of stream profiles
|
|
190
|
+
# @return [String, nil] manifest URL or nil if inbound is missing
|
|
191
|
+
def build_manifest_url(inbound, item, profiles)
|
|
192
|
+
return nil unless inbound
|
|
193
|
+
|
|
194
|
+
domain = inbound['domain'] || inbound[:domain]
|
|
195
|
+
app = item['app'] || item[:app]
|
|
196
|
+
manifest_name = item['manifest_name'] || item[:manifest_name]
|
|
197
|
+
|
|
198
|
+
ext = if profiles&.first
|
|
199
|
+
type = profiles.first[:type]
|
|
200
|
+
stream_extension(type)
|
|
201
|
+
else
|
|
202
|
+
''
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
"http://#{domain}/#{app}/#{manifest_name}#{ext}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|