dm_cloud 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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dm_cloud.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jeremy Mortelette
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,64 @@
1
+ # DmCloud
2
+
3
+ I created this gem to simplify request and responses from DailyMotion Cloud API.
4
+ With this gem, you can :
5
+ - get generated embed code as a string
6
+ - get direct access url to your files (I used this to provide video flux to TV-connected application)
7
+ - (I'm working on ) video creation, delete, paginated lists and video informations (a/v encodings, bitrate, video lenght...)
8
+ - (I'm working on ) CRUD on videos' meta-data
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'dm_cloud'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install dm_cloud
23
+
24
+ ## Usage
25
+
26
+ First, your will need to specify your :user_id, :api_key and your security level.
27
+ I used a file in `APP_ROOT/config/initializers/conf.rb`.
28
+ You can note the securitylevel, for more information about it, take a look at ``
29
+
30
+ # DAILYMOTION CLOUD SETTINGS
31
+ require 'dm_cloud'
32
+ DMC_USER_ID = 'your user id'
33
+ DMC_SECRET = 'your api key'
34
+ DMC_SECURITY_LEVEL = :none
35
+
36
+ DMCloud.configure( {
37
+ :user_key => DMC_USER_ID,
38
+ :secret_key => DMC_SECRET,
39
+ :security_level => DMC_SECURITY_LEVEL
40
+ })
41
+
42
+
43
+
44
+ Second part, how to get you embed url :
45
+
46
+ DMCloud::Streaming.embed('your video id looks like a secret key')
47
+
48
+ Or how to get your direct url :
49
+
50
+ DMCloud::Streaming.url('your video id', ['asset_name'], {options})
51
+
52
+ The next parts will come soon, just need some time to finish its and create corresponding tests.
53
+
54
+ ## Contributing
55
+
56
+ Your welcome to share and enhance this gem.
57
+ This is my first one (and not the last one) but I know some mistakes might be done by myself.
58
+ I do my best and I'm open to all ideas or comments about my work.
59
+
60
+ 1. Fork it
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/dm_cloud.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dm_cloud/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dm_cloud"
8
+ gem.version = DMCloud::VERSION
9
+ gem.authors = ["Jeremy Mortelette"]
10
+ gem.email = ["mortelette.jeremy@gmail.com"]
11
+ gem.description = 'This gem will simplify usage of DailyMotion Cloud API, it represent api in ruby style, with automated handler for search and upload files'
12
+ gem.summary = 'Simplify DailyMotion Cloud API usage'
13
+ gem.homepage = ""
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
+ end
@@ -0,0 +1,72 @@
1
+ module DMCloud
2
+ class Media
3
+ # Creates a new media object.
4
+ # This method can either create an empty media object
5
+ # or also download a media with the url paramater
6
+ # and use it as the source to encode the ASSET_NAME listed in assets_names
7
+ # Params :
8
+ # args:
9
+ # url: SCHEME://USER:PASSWORD@HOSTNAME/MY/PATH/FILENAME.EXTENSION (could be ftp or http)
10
+ # author: an author name
11
+ # title: a title for the film
12
+ # assets_names: (Array) – (optional) the list of ASSET_NAME you want to transcode,
13
+ # when you set this parameter you must also set the url parameter
14
+ # Return :
15
+ # media_id: return the media id of the object
16
+ def self.create(media_id)
17
+ call = "media.create"
18
+
19
+ params = {
20
+ call: call,
21
+ args: DMCloud::Builder::Media.create(args)
22
+ }
23
+ DMCloud::Request.execute(call, params)
24
+ end
25
+
26
+ # Delete a media object with all its associated assets.
27
+ #
28
+ # Parameters:
29
+ # id (media ID) – (required) the id of the media object you want to delete.
30
+ # Return :
31
+ # Nothing
32
+ def self.delete
33
+ call = "media.delete"
34
+
35
+ params = {
36
+ call: call,
37
+ args: { id: media_id}
38
+ }
39
+ DMCloud::Request.execute(call, params)
40
+ end
41
+
42
+ def self.info(fields = [])
43
+ call = "media.info"
44
+
45
+ params = {
46
+ call: call,
47
+ args: DMCloud::Builder::Media.info(fields)
48
+ }
49
+ DMCloud::Request.execute(call, params)
50
+ end
51
+
52
+ # Gives information about a given media object.
53
+ #
54
+ # Params :
55
+ # media_id: (media ID) – (required) the id of the new media object.
56
+ # fields (Array) – (required) the list of fields to retrieve.
57
+ # Returns: a multi-level structure containing about the media related to the requested fields.
58
+ Return type: Object
59
+ def self.list(options = {})
60
+ call = "media.list"
61
+ page = options[:page].present? ? options[:page] : 1
62
+ per_page = options[:per_page].present? ? options[:per_page] : 10
63
+
64
+ params = {
65
+ call: call,
66
+ args: DMCloud::Builder::Media.list(options)
67
+ }
68
+ DMCloud::Request.execute(call, params)
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,13 @@
1
+ module DMCloud
2
+ class Request
3
+
4
+
5
+ def self.execute(call, params = {})
6
+ request = DMCloud.identify(params)
7
+ params.merge!({'auth' => request})
8
+ result = DMCloud::Request.new(params)
9
+ DMCloud::Response.parse(call, result)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,146 @@
1
+ module DMCloud
2
+ class Signing
3
+ # To sign a URL, the client needs a secret shared with Dailymotion Cloud.
4
+ # This secret is call client secret and is available in the back-office interface.
5
+ # Params:
6
+ # expires: An expiration timestamp.
7
+ # sec-level: A security level mask.
8
+ # url-no-query: The URL without the query-string.
9
+ # nonce: A 8 characters-long random alphanumeric lowercase string to make the signature unique.
10
+ # secret: The client secret.
11
+ # sec-data: If sec-level doesn’t have the DELEGATED bit activated,
12
+ # this component contains concatenated informations
13
+ # for all activated sec levels.
14
+ # pub-sec-data: Some sec level data have to be passed in clear in the signature.
15
+ # To generate this component the parameters are serialized using x-www-form-urlencoded, compressed with gzip and encoded in base64.
16
+ # Result :
17
+ # return a string which contain the signed url like
18
+ # <url>?auth=<expires>-<sec>-<nonce>-<md5sum>[-<pub-sec-data>]
19
+ def self.sign(stream)
20
+ raise StandardError, "missing :stream in params" unless stream
21
+ security = security(DMCloud.config[:security_level])
22
+ sec_data = security_data(DMCloud.config[:security_level])
23
+
24
+ base = {
25
+ :sec_level => security(DMCloud.config[:security_level]),
26
+ :url_no_query => stream,
27
+ :expires => (Time.now + 1.hour).to_i,
28
+ :nonce => SecureRandom.hex(16)[0,8],
29
+ :secret => DMCloud.config[:secret_key]
30
+ }
31
+ base.merge!(:sec_data => sec_data, :pub_sec_data => sec_data) unless sec_data.nil?
32
+ puts base
33
+ digest_struct = build_digest_struct(base)
34
+
35
+ check_sum = Digest::MD5.hexdigest(digest_struct)
36
+
37
+ signed_url = [base[:expires], base[:sec_level], base[:nonce], check_sum].compact
38
+ signed_url.merge!(:pub_sec_data => sec_data) unless sec_data.nil?
39
+
40
+ puts signed_url
41
+
42
+ signed_url = signed_url.join('-')
43
+ signed_url
44
+ end
45
+
46
+ # Prepare datas for signing
47
+ # Params :
48
+ # base : contains media id and others for url signing
49
+ def self.build_digest_struct(base)
50
+ result = []
51
+ base.each_pair { |key, value| result << value }
52
+ result.join('')
53
+ end
54
+
55
+ # The client must choose a security level for the signature.
56
+ # Security level defines the mechanism used by Dailymotion Cloud architecture
57
+ # to ensure the signed URL will be used by a single end-user.
58
+ # Params :
59
+ # type :
60
+ # None: The signed URL will be valid for everyone
61
+ # ASNUM: The signed URL will only be valid for the AS of the end-user.
62
+ # The ASNUM (for Autonomous System Number) stands for the network identification,
63
+ # each ISP have a different ASNUM for instance.
64
+ # IP: The signed URL will only be valid for the IP of the end-user.
65
+ # This security level may wrongly block some users
66
+ # which have their internet access load-balanced between several proxies.
67
+ # This is the case in some office network or some ISPs.
68
+ # User-Agent: Used in addition to one of the two former levels,
69
+ # this level a limit on the exact user-agent of the end-user.
70
+ # This is more secure but in some specific condition may lead to wrongly blocked users.
71
+ # Use Once: The signed URL will only be usable once.
72
+ # Note: should not be used with stream URLs.
73
+ # Country: The URL can only be queried from specified countrie(s).
74
+ # The rule can be reversed to allow all countries except some.
75
+ # Referer: The URL can only be queried
76
+ # if the Referer HTTP header contains a specified value.
77
+ # If the URL contains a Referer header with a different value,
78
+ # the request is refused. If the Referer header is missing,
79
+ # the request is accepted in order to prevent from false positives as some browsers,
80
+ # anti-virus or enterprise proxies may remove this header.
81
+ # Delegate: This option instructs the signing algorithm
82
+ # that security level information won’t be embeded into the signature
83
+ # but gathered and lock at the first use.
84
+ # Result :
85
+ # Return a string which contain the signed url like
86
+ # http://cdn.dmcloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
87
+ def self.security(type = nil)
88
+ type = :none unless type
89
+ type = type.to_sym if type.class == String
90
+
91
+ result = case type
92
+ when :none
93
+ 0 # None
94
+ when :delegate
95
+ 1 << 0 # None
96
+ when :asnum
97
+ 1 << 1 # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
98
+ when :ip
99
+ 1 << 2 # The end-user quad dotted IP address (ie: ip=195.8.215.138)
100
+ when :user_agent
101
+ 1 << 3 # The end-user browser user-agent (parameter name is ua)
102
+ when :use_once
103
+ 1 << 4 # None
104
+ when :country
105
+ 1 << 5 # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
106
+ when :referer
107
+ 1 << 6 # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
108
+ end
109
+ result
110
+ end
111
+
112
+ def self.security_data(type, value = nil)
113
+ type = type.to_sym if type.class == String
114
+
115
+ result = case type
116
+ when :asnum
117
+ "as=#{value}" # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
118
+ when :ip
119
+ "ip=#{value}" # The end-user quad dotted IP address (ie: ip=195.8.215.138)
120
+ when :user_agent
121
+ "ua=#{value}" # The end-user browser user-agent (parameter name is ua)
122
+ when :country
123
+ "cc=#{value}" # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
124
+ when :referer
125
+ "rf=#{value}" # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
126
+ else
127
+ nil
128
+ end
129
+ result
130
+ end
131
+
132
+ def self.security_pub_sec_data(type, value)
133
+ type = type.to_sym if type.class == String
134
+
135
+ result = case type
136
+ when :country
137
+ "cc=#{value}" # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
138
+ when :referer
139
+ "rf=#{value}" # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
140
+ else
141
+ nil
142
+ end
143
+ result
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,67 @@
1
+ require "time"
2
+ require "openssl"
3
+ require "base64"
4
+ require 'digest/md5'
5
+
6
+ module DMCloud
7
+ class Streaming
8
+ # Default URL to get embed content ou direct url
9
+ DIRECT_STREAM = '[PROTOCOL]://cdn.dmcloud.net/route/[USER_ID]/[MEDIA_ID]/[ASSET_NAME].[ASSET_EXTENSION]'
10
+ EMBED_STREAM = '[PROTOCOL]://api.dmcloud.net/embed/[USER_ID]/[MEDIA_ID]?auth=[AUTH_TOKEN]&skin=[SKIN_ID]'
11
+ EMBED_IFRAME = '<iframe width=[WIDTH] height=[HEIGHT] frameborder="0" scrolling="no" src="[EMBED_URL]"></iframe>'
12
+ # Get embeded player
13
+ # Params :
14
+ # media_id: this is the id of the media (eg: 4c922386dede830447000009)
15
+ # options:
16
+ # skin_id: (optional) the id of the custom skin for the video player
17
+ # width: (optional) the width for the video player frame
18
+ # height: (optional) the height for the video player frame
19
+ # Result :
20
+ # return a string which contain the signed url like
21
+ # <iframe width="848" height="480" frameborder="0" scrolling="no" src="http://api.dmcloud.net/embed/<user_id>/<media_id>?auth=<auth_token>&skin=<skin_id>"></iframe>
22
+ def self.embed(media_id, options = {})
23
+ raise StandardError, "missing :media_id in params" unless media_id
24
+
25
+ skin_id = options[:skin_id].present? ? options[:skin_id] : 'modern1'
26
+ width = options[:width].present? ? options[:width] : '848'
27
+ height = options[:height].present? ? options[:height] : '480'
28
+
29
+ stream = EMBED_STREAM
30
+ stream.gsub!('[PROTOCOL]', DMCloud.config[:protocol])
31
+ stream.gsub!('[USER_ID]', DMCloud.config[:user_key])
32
+ stream.gsub!('[MEDIA_ID]', media_id)
33
+ stream.gsub!('[SKIN_ID]', skin_id)
34
+ stream += '?auth=[AUTH_TOKEN]'.gsub!('[AUTH_TOKEN]', DMCloud::Signing.sign(stream))
35
+
36
+ frame = EMBED_IFRAME
37
+ frame.gsub!('[WIDTH]', width)
38
+ frame.gsub!('[HEIGHT]', height)
39
+ frame.gsub!('[EMBED_URL]', stream)
40
+ frame
41
+ end
42
+
43
+ # Get media url for direct link to the file on DailyMotion Cloud
44
+ # Params :
45
+ # media_id: this is the id of the media (eg: 4c922386dede830447000009)
46
+ # asset_name: the name of the asset you want to stream (eg: mp4_h264_aac)
47
+ # asset_extension: the extension of the asset, most of the time it is the first part of the asset name (eg: mp4)
48
+ # Result :
49
+ # return a string which contain the signed url like
50
+ # http://cdn.dmcloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
51
+ def self.url(media_id, asset_name, asset_extension = nil)
52
+ asset_extension = asset_name.split('_').first unless asset_extension
53
+
54
+ raise StandardError, "missing :media_id in params" unless media_id
55
+ raise StandardError, "missing :asset_name in params" unless asset_name
56
+
57
+ stream = DIRECT_STREAM
58
+ stream.gsub!('[PROTOCOL]', DMCloud.config[:protocol])
59
+ stream.gsub!('[USER_ID]', DMCloud.config[:user_key])
60
+ stream.gsub!('[MEDIA_ID]', media_id)
61
+ stream.gsub!('[ASSET_NAME]', asset_name)
62
+ stream.gsub!('[ASSET_EXTENSION]', asset_extension)
63
+ stream += '?auth=[AUTH_TOKEN]'.gsub!('[AUTH_TOKEN]', DMCloud::Signing.sign(stream))
64
+ stream
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module DMCloud
2
+ VERSION = "0.0.1"
3
+ end
data/lib/dm_cloud.rb ADDED
@@ -0,0 +1,83 @@
1
+ require "dm_cloud/version"
2
+ require 'yaml'
3
+
4
+ module DMCloud
5
+
6
+ # Configuration defaults
7
+ @@config = {
8
+ :security_level => 'none',
9
+ :protocol => 'http'
10
+ }
11
+
12
+ YAML_INITIALIZER_PATH = File.dirname(__FILE__)
13
+ @valid_config_keys = @@config.keys
14
+
15
+ # Configure through hash
16
+ def self.configure(opts = {})
17
+ opts.each {|k,v| @@config[k.to_sym] = v } # if @valid_config_keys.include? k.to_sym}
18
+ end
19
+
20
+ # Configure through yaml file
21
+ def self.configure_with(yaml_file_path = nil)
22
+ yaml_file_path = YAML_INITIALIZER_PATH unless yaml_file_path
23
+ begin
24
+ config = YAML::load(IO.read(path_to_yaml_file))
25
+ rescue Errno::ENOENT
26
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
27
+ rescue Psych::SyntaxError
28
+ log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
29
+ end
30
+
31
+ configure(config)
32
+ end
33
+
34
+ def self.config
35
+ @@config = configure unless @@config
36
+ @@config
37
+ end
38
+
39
+ def self.identify(request)
40
+ user_id = @@config[:user_id]
41
+ api_key = @@config[:api_key]
42
+ checksum = md5(user_id + normalize(request) + api_key)
43
+
44
+ auth_token = user_id + ':' + checksum
45
+ end
46
+
47
+ def self.create_has_library(library)
48
+ define_singleton_method("has_#{library}?") do
49
+ cv="@@#{library}"
50
+ if !class_variable_defined? cv
51
+ begin
52
+ require library.to_s
53
+ class_variable_set(cv,true)
54
+ rescue LoadError
55
+ class_variable_set(cv,false)
56
+ end
57
+ end
58
+ class_variable_get(cv)
59
+ end
60
+ end
61
+
62
+ create_has_library :treaming
63
+
64
+
65
+ class << self
66
+ # Load a object saved on a file.
67
+ def load(filename)
68
+ if File.exists? filename
69
+ o=false
70
+ File.open(filename,"r") {|fp| o=Marshal.load(fp) }
71
+ o
72
+ else
73
+ false
74
+ end
75
+ end
76
+ end
77
+
78
+ autoload(:Streaming, 'dm_cloud/streaming')
79
+
80
+
81
+ end
82
+
83
+ Dir.glob('dm_cloud/**/*.rb').each{ |m| require File.dirname(__FILE__) + '/dm_cloud/' + m }
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm_cloud
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeremy Mortelette
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-25 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This gem will simplify usage of DailyMotion Cloud API, it represent api
15
+ in ruby style, with automated handler for search and upload files
16
+ email:
17
+ - mortelette.jeremy@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - dm_cloud.gemspec
28
+ - lib/dm_cloud.rb
29
+ - lib/dm_cloud/media.rb
30
+ - lib/dm_cloud/request.rb
31
+ - lib/dm_cloud/signing.rb
32
+ - lib/dm_cloud/streaming.rb
33
+ - lib/dm_cloud/version.rb
34
+ homepage: ''
35
+ licenses: []
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.24
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: Simplify DailyMotion Cloud API usage
58
+ test_files: []