Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds method to move assets #16

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
.rspec_status

Gemfile.lock
ci.yml
98 changes: 92 additions & 6 deletions lib/sony_ci_api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ module SonyCiApi
class Client
BASE_URL = "https://api.cimediacloud.com"
BASE_UPLOAD_URL = "https://io.cimediacloud.com"

# Some API endpoints require recursion, like fetching nested folder contents.
# MAX_RECURSION sets the max recursion depth to avoid accidental API abuse.
MAX_RECURSION = 5

# API endpoints that returns a list from the 'items' property tend to also have pagination params 'limit' and 'offset'.
MAX_ITEMS_PER_PAGE = 100
MAX_PAGES = 1000
MAX_ITEMS = MAX_ITEMS_PER_PAGE * MAX_PAGES

attr_reader :config, # stores the config for the connection, including credentials.
:response # stores the most recent response; default nil
Expand All @@ -36,8 +45,12 @@ def load_config!(config = {})
elsif config.is_a? Hash
config_hash = config
else
raise InvalidConfigError, "config is expected to be a valid YAML file or " \
"a Hash, but #{config.class} was given. "
msg = if config.is_a? String
"config file '#{config}' does not exist."
else
"config is expected to be a valid YAML file or a Hash, but #{config.class} was given. "
end
raise InvalidConfigError, msg
end
@config = config_hash.with_indifferent_access
rescue Psych::SyntaxError, Psych::DisallowedClass => e
Expand Down Expand Up @@ -97,16 +110,63 @@ def access_token
end
end


# Returns the 'items' property of the response fetching multiple pages as necessary.
def get_items(path, params: {}, headers: {}, post: false)
all_items = []
paginate_params(params).map do |paginated_params|
method = post ? :post : :get
results = send(method, path, params: paginated_params, headers: headers).fetch('items', [])
all_items += results
# If the results are fewer than the page count that means we asked for more pages than
# we actually have, so break early to avoid extraneous API requests.
break if results.count < paginated_params[:limit]
end
all_items
end


def paginate_params(params={})
params = params.with_indifferent_access
limit = params.delete(:limit) || MAX_ITEMS_PER_PAGE
offset = params.delete(:offset) || 0
# Calculate the number of pages we need to request to get the full limit
pages = (((limit - 1) / MAX_ITEMS_PER_PAGE) + 1)

# Page the number of pages to a list of param hashes with pagination
# values for limit and offset
pages.times.map do |page|
this_page_size = [limit - (page * MAX_ITEMS_PER_PAGE), MAX_ITEMS_PER_PAGE].min
this_offest = offset + (page * MAX_ITEMS_PER_PAGE)
params.merge(limit: this_page_size, offset: this_offest)
end
end

def workspaces(**params)
get('/workspaces', params: params)['items']
get_items('/workspaces', params: params)
end

def workspace_search(workspace_id = self.workspace_id, **params)
get("/workspaces/#{workspace_id}/search", params: params)['items']
get_items("/workspaces/#{workspace_id}/search", params: params)
end

def faceted_search(**params)
# Set workspaceIds to the current workspace as a default.
params['workspaceIds'] ||= [workspace_id]
get_items('/faceted-search', params: params, post: true)
end

# Returns an item whose name matches the `name` parameter.
def find_by_name(name, **params)
raise ArgumentError, "Expected first argument to be a string, but got #{name.class}" unless name.is_a? String
params.merge!(query: name, limit: 10)
items = faceted_search(**params).select { |item| item['name'] == name }
raise "#{items.count} items found with name '#{name}'" if items.count > 1
items.first
end

def webhooks(**params)
get("/networks/#{workspace['network']['id']}/webhooks", params: params)['items']
get_items("/networks/#{workspace['network']['id']}/webhooks", params: params)
end

def workspace_id=(wid)
Expand Down Expand Up @@ -148,6 +208,10 @@ def asset_download(asset_id)
get "/assets/#{asset_id}/download"
end

def move_assets(asset_ids: [], folder_id:)
post "/assets/move", params: { assetIds: asset_ids, folderId: folder_id }
end

def asset_stream_url(asset_id, type: "hls")
type = type.downcase
raise ArgumentError, "Invalid value for parameter type. Expected one of hls, video-3g, or video-sd, but '#{type}' was given" unless %w[hls video-3g video-sd].include?(type)
Expand All @@ -159,7 +223,29 @@ def asset_stream_url(asset_id, type: "hls")
end

def workspace_contents(workspace_id = self.workspace_id, **params)
get("/workspaces/#{workspace_id}/contents", params: params)['items']
get_items("/workspaces/#{workspace_id}/contents", params: params.merge(limit: MAX_ITEMS))
end

def folder(folder_id)
get "/folders/#{folder_id}"
end

def folder_contents(folder_id, depth: MAX_RECURSION, current_depth: 0, **params)
raise MaxRecursionError, "MAX_RECURSIION level #{MAX_RECURSION} exceeded" if current_depth >= MAX_RECURSION
contents = get_items("/folders/#{folder_id}/contents", params: params.merge(limit: MAX_ITEMS))

if current_depth < depth
contents.each do |item|
if item['kind'].downcase == 'folder'
item['contents'] = folder_contents(
item['id'],
depth: depth,
current_depth: (current_depth + 1),
**params
)
end
end
end
end

private
Expand Down
1 change: 1 addition & 0 deletions lib/sony_ci_api/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,5 @@ class SSLError < ServerError; end

# Other errors not associated with HTTP.
class InvalidConfigError < Error; end
class MaxRecursionError < Error; end
end
66 changes: 65 additions & 1 deletion spec/sony_ci_api/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def camelize_params(**params)

describe '#get' do
# Run shared spec to simply test the
it_behaves_like 'HTTP request method', http_method: :delete
it_behaves_like 'HTTP request method', http_method: :get

let(:response_status) { 200 }
let(:response_body) { { "fooBarResponse" => randstr } }
Expand Down Expand Up @@ -443,6 +443,42 @@ def camelize_params(**params)
end
end

describe '#asset_move' do
let(:asset_ids) { 5.times.map { randhex } }
let(:folder_id) { randhex}
# Pared down response body. In reality it's much bigger.
let(:response_body) {
{
"completeCount" => asset_ids.count,
"errorCount" => 0
}
}

let(:asset_download_info) {
stub_request_and_call_block(
:post,
"#{base_url}/assets/move",
with: {
body: {
"assetIds": asset_ids,
"folderId": folder_id
}
},
stub_response: {
body: response_body.to_json,
status: 200
}
) do
# Call the method under test
client.move_assets(asset_ids: asset_ids, folder_id: folder_id)
end
}

it 'returns download information for an asset' do
expect(asset_download_info).to eq response_body
end
end

describe '#asset_streams' do
let(:asset_id) { randhex }
let(:streaming_url) { "http://io.api.cimediacloud.com/assets/#{asset_id}/streams/smil_md5hash.m3u8" }
Expand Down Expand Up @@ -511,6 +547,34 @@ def camelize_params(**params)
end
end
end

describe '#folder_contents' do
let(:folder_id) { randhex}
# Pared down response body. In reality it's much bigger.
let(:response_body) {
{
"items" => [{ "id" => randhex }],
}
}

let(:call_to_folder_contents) {
stub_request_and_call_block(
:get,
"#{base_url}/folders/#{folder_id}/contents",
stub_response: {
body: response_body.to_json,
status: 200
}
) do
# Call the method under test
client.folder_contents(folder_id)
end
}

it 'returns download information for an asset' do
expect(call_to_folder_contents).to eq response_body['items']
end
end
end

describe '#load_config!' do
Expand Down
Loading