├── .github └── renovate.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TestDailymotion.py ├── config.py ├── dailymotion.py ├── examples └── video.mp4 ├── requirements.txt ├── setup.cfg ├── setup.py └── xupload.py /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>dailymotion/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # PyInstaller 25 | # Usually these files are written by a python script from a template 26 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 27 | *.manifest 28 | *.spec 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .coverage 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | 42 | # Translations 43 | *.mo 44 | *.pot 45 | 46 | # Django stuff: 47 | *.log 48 | 49 | # Sphinx documentation 50 | docs/_build/ 51 | 52 | # PyBuilder 53 | target/ 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - '2.7' 5 | - '3.5' 6 | 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install pytest > /dev/null 10 | - pip install pytest-cov > /dev/null 11 | - pip install coveralls > /dev/null 12 | 13 | script: py.test --cov dailymotion --cov-report term-missing TestDailymotion.py 14 | 15 | after_success: 16 | - coveralls 17 | 18 | deploy: 19 | provider: pypi 20 | user: dailymotion 21 | password: 22 | secure: LgLIHRH7OWruOTx0tAiYK/QMH5ecfk+WSEk4k/ifvh2EGR10dEI9hNC/AGSP+i2sGjHkeStFyMKu4rpjc9We/xfhCr27LVrhrWRIk0/1YAzGjQOYHlscX2mr/eRjGjZgWshQrH+CFXYHARYFnPt7riHfD9/X2znlD6VUaWHuXSk= 23 | on: 24 | branch: master 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Samir AMZANI 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dailymotion API SDK for Python 2 | ================== 3 | [![Build Status](https://travis-ci.org/dailymotion/dailymotion-sdk-python.svg?branch=master)](https://travis-ci.org/dailymotion/dailymotion-sdk-python) [![Coverage Status](https://coveralls.io/repos/dailymotion/dailymotion-sdk-python/badge.svg?branch=master&service=github)](https://coveralls.io/github/dailymotion/dailymotion-sdk-python?branch=master) 4 | 5 | 6 | Installation 7 | ------------ 8 | 9 | ``` 10 | $ pip install dailymotion 11 | ``` 12 | 13 | Or 14 | 15 | ``` 16 | $ git clone git@github.com:dailymotion/dailymotion-sdk-python.git 17 | $ cd dailymotion-sdk-python 18 | $ python setup.py install 19 | ``` 20 | 21 | 22 | Examples 23 | -------- 24 | 25 | Public API call: 26 | 27 | ```python 28 | d = dailymotion.Dailymotion() 29 | d.get('/videos') 30 | ``` 31 | 32 | Authenticated call: 33 | 34 | ```python 35 | d = dailymotion.Dailymotion() 36 | d.set_grant_type('password', api_key=API_KEY, api_secret=API_SECRET, 37 | scope=['userinfo'], info={'username': USERNAME, 'password': PASSWORD}) 38 | d.get('/me', {'fields': 'id,fullname'}) 39 | ``` 40 | 41 | Video upload: 42 | 43 | ```python 44 | d = dailymotion.Dailymotion() 45 | d.set_grant_type('password', api_key=API_KEY, api_secret=API_SECRET, 46 | scope=['manage_videos'], info={'username': USERNAME, 'password': PASSWORD}) 47 | url = d.upload('./video.mp4') 48 | d.post('/user//videos', 49 | {'url': url, 'title': 'MyTitle', 'published': 'true', 'channel': 'news'}) 50 | ``` 51 | 52 | Set your own access_token (assuming your access_token is valide): 53 | 54 | ```python 55 | d = dailymotion.Dailymotion 56 | d.set_access_token(YOUR_ACCESS_TOKEN) 57 | d.get('/me') 58 | ``` 59 | 60 | Authentication: 61 | --------------- 62 | 63 | The Dailymotion API requires OAuth 2.0 authentication in order to access protected resources. 64 | 65 | Contrary to most SDKs, the Dailymotion Python SDK implements lazy authentication, which means that no authentication request is sent as long as no data is requested from the API. At which point, two requests are sent back-to-back during the first request for information, one to authenticate and one to fetch the data. 66 | 67 | Please note that the Dailymotion Python SDK also takes care of abstracting the entire OAuth flow, from retrieving, storing and using access tokens, to using refresh tokens to gather new access tokens automatically. You shouldn't have to deal with access tokens manually. 68 | 69 | The session storage is enabled by default, you can disabled it by passing `session_store_enabled=false` to the constructor. 70 | 71 | Access tokens are stored in memory by default, storing them in your OS files is recommended : 72 | 73 | ```python 74 | import dailymotion 75 | 76 | # The ./data directory 77 | file_session = dailymotion.FileSessionStore('./data') 78 | d = dailymotion.Dailymotion(session_storage=file_session) 79 | .... 80 | ``` 81 | 82 | 83 | 84 | Tests 85 | ----- 86 | 87 | 1. Install dependencies: 88 | 89 | ``` 90 | $ pip install -r requirements.txt 91 | ``` 92 | 93 | 2. Update the file named _config.py_ or set environment variables with the following content: 94 | 95 | ```python 96 | CLIENT_ID = '[YOUR API KEY]' 97 | CLIENT_SECRET = '[YOUR API SECRET]' 98 | USERNAME = '[YOUR USERNAME]' 99 | PASSWORD = '[YOUR PASSWORD]' 100 | REDIRECT_URI = '[YOUR REDIRECT URI]' 101 | BASE_URL = 'https://api.dailymotion.com' 102 | OAUTH_AUTHORIZE_URL = 'https://www.dailymotion.com/oauth/authorize' 103 | OAUTH_TOKEN_URL = 'https://api.dailymotion.com/oauth/token' 104 | ``` 105 | 106 | 3. Run tests: 107 | 108 | ``` 109 | $ py.test TestDailymotion.py 110 | ``` 111 | -------------------------------------------------------------------------------- /TestDailymotion.py: -------------------------------------------------------------------------------- 1 | import dailymotion 2 | import unittest 3 | import config 4 | import re 5 | import time 6 | import os 7 | import pytest 8 | import sys 9 | 10 | class TestA(unittest.TestCase): 11 | 12 | @classmethod 13 | def setUpClass(self): 14 | self.api_base_url = config.BASE_URL or 'http://api.dailymotion.com' 15 | self.api_key = config.CLIENT_ID 16 | self.api_secret = config.CLIENT_SECRET 17 | self.username = config.USERNAME 18 | self.password = config.PASSWORD 19 | self.scope = ['manage_videos', 'manage_playlists', 'userinfo'] 20 | self.redirect_uri = config.REDIRECT_URI 21 | self.file_path = config.VIDEO_PATH or './examples/video.mp4' 22 | self.oauth_authorize_endpoint_url = config.OAUTH_AUTHORIZE_URL or 'https://api.dailymotion.com/oauth/authorize' 23 | self.oauth_token_endpoint_url = config.OAUTH_TOKEN_URL or 'https://api.dailymotion.com/oauth/token' 24 | self.session_file_directory = './data' 25 | if not os.path.exists(self.session_file_directory): 26 | os.makedirs(self.session_file_directory) 27 | 28 | @classmethod 29 | def tearDownClass(self): 30 | if os.path.exists(self.session_file_directory): 31 | os.rmdir(self.session_file_directory) 32 | 33 | def test_init(self): 34 | d = dailymotion.Dailymotion() 35 | self.assertEqual(d.api_base_url, 'https://api.dailymotion.com') 36 | 37 | d = dailymotion.Dailymotion(api_base_url='http://api.stage.dailymotion.com', timeout=10, debug=True) 38 | self.assertEqual(d.api_base_url, 'http://api.stage.dailymotion.com') 39 | self.assertEqual(d.timeout, 10) 40 | self.assertEqual(d.debug, True) 41 | 42 | def test_get(self): 43 | d = dailymotion.Dailymotion() 44 | videos = d.get('/videos') 45 | self.assertEqual('has_more' in videos, True) 46 | self.assertEqual(videos['has_more'], True) 47 | self.assertEqual('list' in videos, True) 48 | self.assertEqual(len(videos['list']) > 0, True) 49 | 50 | def test_set_grant_type(self): 51 | d = dailymotion.Dailymotion() 52 | self.assertRaises(dailymotion.DailymotionClientError, d.set_grant_type, 'password', api_secret=self.api_secret, scope=self.scope, 53 | info={'username': self.username, 'password': self.password}) 54 | self.assertRaises(dailymotion.DailymotionClientError, d.set_grant_type, 'password', api_secret=self.api_secret, scope=self.scope) 55 | self.assertRaises(dailymotion.DailymotionClientError, d.set_grant_type, 'password', api_secret=self.api_secret, scope=None) 56 | 57 | def test_get_authorization_url(self): 58 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url) 59 | d.set_grant_type('authorization', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'redirect_uri' : self.redirect_uri}) 60 | authorization_url = d.get_authorization_url(redirect_uri=self.redirect_uri, scope=self.scope) 61 | self.assertEqual(re.match('https?://(?:www)?(?:[\w-]{2,255}(?:\.\w{2,6}){1,2})(?:/[\w&%?#-]{1,300})?',authorization_url) == None, False) 62 | 63 | def test_get_access_token(self): 64 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, 65 | oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url, 66 | oauth_token_endpoint_url=self.oauth_token_endpoint_url) 67 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 68 | access_token = d.get_access_token() 69 | self.assertEqual(isinstance (access_token, str) or isinstance(access_token, unicode), True) 70 | d.logout() 71 | 72 | def test_set_access_token(self): 73 | d = dailymotion.Dailymotion() 74 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 75 | d.set_access_token(d.get_access_token()) 76 | response = d.get('/me/?fields=fullname') 77 | self.assertEqual(isinstance (response.get('fullname'), str) or isinstance(response.get('fullname'), unicode), True) 78 | d.logout() 79 | 80 | def test_auth_call(self): 81 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, 82 | oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url, 83 | oauth_token_endpoint_url=self.oauth_token_endpoint_url, 84 | session_store_enabled=True) 85 | 86 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 87 | response = d.get('/me/?fields=fullname') 88 | self.assertEqual(isinstance (response.get('fullname'), str) or isinstance(response.get('fullname'), unicode), True) 89 | d.logout() 90 | 91 | def test_upload(self): 92 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, 93 | oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url, 94 | oauth_token_endpoint_url=self.oauth_token_endpoint_url, 95 | session_store_enabled=True) 96 | 97 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 98 | url = d.upload(self.file_path) 99 | self.assertEqual(re.match('https?://(?:www)?(?:[\w-]{2,255}(?:\.\w{2,6}){1,2})(?:/[\w&%?#-]{1,300})?',url) == None, False) 100 | video = d.post('/videos', {'url' : url, 101 | 'title' : 'my_test_upload_%s' % time.strftime("%c"), 102 | 'published' : 'true', 103 | 'channel' : 'news' 104 | }) 105 | self.assertEqual('id' in video, True) 106 | d.delete('/video/%s' % video['id']) 107 | d.logout() 108 | 109 | @pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python3.5 or higher") 110 | def test_xupload(self): 111 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, 112 | oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url, 113 | oauth_token_endpoint_url=self.oauth_token_endpoint_url, 114 | session_store_enabled=True) 115 | 116 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 117 | url = d.upload(self.file_path, workers=5) 118 | self.assertEqual(re.match('https?://(?:www)?(?:[\w-]{2,255}(?:\.\w{2,6}){1,2})(?:/[\w&%?#-]{1,300})?',url) == None, False) 119 | video = d.post('/videos', {'url' : url, 120 | 'title' : 'my_test_upload_%s' % time.strftime("%c"), 121 | 'published' : 'true', 122 | 'channel' : 'news' 123 | }) 124 | self.assertEqual('id' in video, True) 125 | d.delete('/video/%s' % video['id']) 126 | d.logout() 127 | 128 | 129 | def test_session_store_option(self): 130 | d = dailymotion.Dailymotion(session_store_enabled=False) 131 | self.assertFalse(d._session_store_enabled) 132 | 133 | d = dailymotion.Dailymotion(session_store_enabled=True) 134 | self.assertTrue(d._session_store_enabled) 135 | 136 | d = dailymotion.Dailymotion(session_store_enabled=None) 137 | self.assertEqual(d.DEFAULT_SESSION_STORE, d._session_store_enabled) 138 | 139 | def test_in_memory_session(self): 140 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, 141 | oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url, 142 | oauth_token_endpoint_url=self.oauth_token_endpoint_url, 143 | session_store_enabled=True) 144 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 145 | access_token = d.get_access_token() 146 | self.assertEqual(isinstance (access_token, str) or isinstance(access_token, unicode), True) 147 | second_access_token = d.get_access_token() 148 | self.assertEqual(isinstance (second_access_token, str) or isinstance(second_access_token, unicode), True) 149 | self.assertEqual(second_access_token, access_token) 150 | d.logout() 151 | 152 | def test_file_storage_session(self): 153 | fs = dailymotion.FileSessionStore(self.session_file_directory) 154 | d = dailymotion.Dailymotion(api_base_url=self.api_base_url, 155 | oauth_authorize_endpoint_url=self.oauth_authorize_endpoint_url, 156 | oauth_token_endpoint_url=self.oauth_token_endpoint_url, 157 | session_store_enabled=True, 158 | session_store=fs) 159 | d.set_grant_type('password', api_key=self.api_key, api_secret=self.api_secret, scope=self.scope, info={'username': self.username, 'password': self.password}) 160 | access_token = d.get_access_token() 161 | self.assertEqual(isinstance (access_token, str) or isinstance(access_token, unicode), True) 162 | second_access_token = d.get_access_token() 163 | self.assertEqual(isinstance (second_access_token, str) or isinstance(second_access_token, unicode), True) 164 | self.assertEqual(second_access_token, access_token) 165 | d.logout() 166 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CLIENT_ID = os.getenv('DM_CLIENT_ID', '[YOUR API KEY]') 4 | CLIENT_SECRET = os.getenv('DM_CLIENT_SECRET', '[YOUR API SECRET]') 5 | USERNAME = os.getenv('DM_USERNAME', '[YOUR USERNAME]') 6 | PASSWORD = os.getenv('DM_PASSWORD', '[YOUR PASSWORD]') 7 | REDIRECT_URI = os.getenv('DM_REDIRECT_URI', '[YOUR REDIRECT URI]') 8 | VIDEO_PATH = os.getenv('DM_VIDEO_PATH') 9 | BASE_URL = 'https://api.dailymotion.com' 10 | OAUTH_AUTHORIZE_URL = 'https://www.dailymotion.com/oauth/authorize' 11 | OAUTH_TOKEN_URL = 'https://api.dailymotion.com/oauth/token' 12 | -------------------------------------------------------------------------------- /dailymotion.py: -------------------------------------------------------------------------------- 1 | """ Dailymotion SDK """ 2 | import requests 3 | from requests_toolbelt import MultipartEncoder 4 | import time 5 | import os 6 | import sys 7 | import re 8 | import json 9 | from collections import defaultdict 10 | 11 | __author__ = 'Samir AMZANI ' 12 | __version__ = '0.2.5' 13 | __python_version__ = '.'.join([str(i) for i in sys.version_info[:3]]) 14 | 15 | try: 16 | from urllib.parse import urlencode 17 | except ImportError: # Python 2 18 | from urllib import urlencode 19 | 20 | try: 21 | from urllib.parse import parse_qsl 22 | except ImportError: # Python 2 23 | try: 24 | from urlparse import parse_qsl 25 | except ImportError: # Python < 2.6 26 | from cgi import parse_qsl 27 | 28 | if sys.version_info > (3, 5): 29 | import xupload 30 | else: # Python < 3.5 31 | xupload = None 32 | 33 | class DailymotionClientError(Exception): 34 | def __init__(self, message, error_type=None): 35 | self.type = error_type 36 | 37 | self.message = message 38 | if error_type is not None: 39 | self.message = '%s: %s' % (error_type, message) 40 | 41 | super(DailymotionClientError, self).__init__(self.message) 42 | class DailymotionApiError(DailymotionClientError): pass 43 | class DailymotionAuthError(DailymotionClientError): pass 44 | class DailymotionTokenExpired(DailymotionClientError): pass 45 | class DailymotionUploadTransportError(DailymotionClientError): pass 46 | class DailymotionUploadInvalidResponse(DailymotionClientError): pass 47 | class DailymotionUploadError(DailymotionClientError): pass 48 | 49 | 50 | 51 | class SessionStore(object): 52 | 53 | def __init__(self): 54 | self._sessions = defaultdict(dict) 55 | self._user = 'default' 56 | 57 | def set_user(self, user=None): 58 | self._user = user if user else 'default' 59 | 60 | def set(self, session): 61 | self.current.update(session) 62 | 63 | def set_value(self, key, value): 64 | self.current[key] = value 65 | 66 | def get_value(self, value, default=None): 67 | return self.current.get(value, default) 68 | 69 | def clear(self): 70 | del self.current 71 | 72 | @property 73 | def current(self): 74 | return self._sessions[self._user] 75 | 76 | @current.setter 77 | def current(self, value): 78 | self._sessions[self._user] = value 79 | 80 | @current.deleter 81 | def current(self): 82 | del self._sessions[self._user] 83 | 84 | 85 | class FileSessionStore(object): 86 | 87 | def __init__(self, directory): 88 | self._directory = directory 89 | self._sessions = defaultdict(dict) 90 | self._user = 'default' 91 | 92 | def set_user(self, user=None): 93 | self._user = user if user else 'default' 94 | 95 | def set(self, session): 96 | self.current.update(session) 97 | self.save() 98 | 99 | def get_value(self, value, default=None): 100 | return self.current.get(value, default) 101 | 102 | def _get_storage_file(self): 103 | return '%s/%s.json' % (self._directory, self._user) 104 | 105 | def _remove(self): 106 | try: 107 | os.remove(self._get_storage_file()) 108 | except (IOError, OSError): 109 | pass 110 | 111 | def _load(self): 112 | try: 113 | with open(self._get_storage_file()) as f: 114 | self.current = json.loads(f.read()) 115 | except (ValueError, IOError): 116 | pass 117 | 118 | def reload(self): 119 | self._load() 120 | 121 | def save(self): 122 | with open(self._get_storage_file(), 'w') as f: 123 | f.write(json.dumps(self.current)) 124 | 125 | def clear(self): 126 | del self.current 127 | 128 | @property 129 | def current(self): 130 | if self._user not in self._sessions: 131 | self._load() 132 | return self._sessions[self._user] 133 | 134 | @current.setter 135 | def current(self, value): 136 | self._sessions[self._user] = value 137 | 138 | @current.deleter 139 | def current(self): 140 | del self._sessions[self._user] 141 | self._remove() 142 | 143 | 144 | class Dailymotion(object): 145 | 146 | DEFAULT_DEBUG = False 147 | DEFAULT_TIMEOUT = 5 148 | DEFAULT_API_BASE_URL = 'https://api.dailymotion.com' 149 | DEFAULT_AUTHORIZE_URL = 'https://www.dailymotion.com/oauth/authorize' 150 | DEFAULT_TOKEN_URL = 'https://api.dailymotion.com/oauth/token' 151 | DEFAULT_SESSION_STORE = True 152 | 153 | def __init__(self, api_base_url=None, debug=None, timeout=None, oauth_authorize_endpoint_url=None, oauth_token_endpoint_url=None, session_store_enabled=None, session_store=None): 154 | 155 | self.api_base_url = api_base_url or self.DEFAULT_API_BASE_URL 156 | self.debug = debug or self.DEFAULT_DEBUG 157 | self.timeout = timeout or self.DEFAULT_TIMEOUT 158 | self.oauth_authorize_endpoint_url = oauth_authorize_endpoint_url or self.DEFAULT_AUTHORIZE_URL 159 | self.oauth_token_endpoint_url = oauth_token_endpoint_url or self.DEFAULT_TOKEN_URL 160 | self._grant_type = None 161 | self._grant_info = {} 162 | self._headers = {'Accept' : 'application/json', 163 | 'User-Agent' : 'Dailymotion-Python/%s (Python %s)' % (__version__, __python_version__)} 164 | self._session_store_enabled = self.DEFAULT_SESSION_STORE if session_store_enabled is None else session_store_enabled 165 | self._session_store = SessionStore() if session_store is None else session_store 166 | 167 | 168 | def set_grant_type(self, grant_type = 'client_credentials', api_key=None, api_secret=None, scope=None, info=None): 169 | 170 | """ 171 | Grant types: 172 | - token: 173 | An authorization is requested to the end-user by redirecting it to an authorization page hosted 174 | on Dailymotion. Once authorized, a refresh token is requested by the API client to the token 175 | server and stored in the end-user's cookie (or other storage technique implemented by subclasses). 176 | The refresh token is then used to request time limited access token to the token server. 177 | 178 | - none / client_credentials: 179 | This grant type is a 2 legs authentication: it doesn't allow to act on behalf of another user. 180 | With this grant type, all API requests will be performed with the user identity of the API key owner. 181 | 182 | - password: 183 | This grant type allows to authenticate end-user by directly providing its credentials. 184 | This profile is highly discouraged for web-server workflows. If used, the username and password 185 | MUST NOT be stored by the client. 186 | """ 187 | 188 | if api_key and api_secret: 189 | self._grant_info['key'] = api_key 190 | self._grant_info['secret'] = api_secret 191 | else: 192 | raise DailymotionClientError('Missing API key/secret') 193 | 194 | if isinstance(info, dict): 195 | self._grant_info.update(info) 196 | else: 197 | info = {} 198 | 199 | if self._session_store_enabled and isinstance(info, dict) and info.get('username') is not None: 200 | self._session_store.set_user(info.get('username')) 201 | 202 | if grant_type in ('authorization', 'token'): 203 | grant_type = 'authorization' 204 | if 'redirect_uri' not in info: 205 | raise DailymotionClientError('Missing redirect_uri in grant info for token grant type.') 206 | elif grant_type in ('client_credentials', 'none'): 207 | grant_type = 'client_credentials' 208 | elif grant_type == 'password': 209 | if 'username' not in info or 'password' not in info: 210 | raise DailymotionClientError('Missing username or password in grant info for password grant type.') 211 | 212 | self._grant_type = grant_type 213 | 214 | if scope: 215 | if not isinstance(scope, (list, tuple)): 216 | raise DailymotionClientError('Invalid scope type: must be a list of valid scopes') 217 | self._grant_info['scope'] = scope 218 | 219 | def get_authorization_url(self, redirect_uri=None, scope=None, display='page'): 220 | if self._grant_type != 'authorization': 221 | raise DailymotionClientError('This method can only be used with TOKEN grant type.') 222 | 223 | qs = { 224 | 'response_type': 'code', 225 | 'client_id': self._grant_info['key'], 226 | 'redirect_uri': redirect_uri, 227 | 'display': display, 228 | } 229 | if scope and type(scope) in (list, tuple): 230 | qs['scope'] = ' '.join(scope) 231 | 232 | return '%s?%s' % (self.oauth_authorize_endpoint_url, urlencode(qs)) 233 | 234 | def oauth_token_request(self, params): 235 | try: 236 | result = self.request(self.oauth_token_endpoint_url, 'POST', params) 237 | except DailymotionApiError as e: 238 | raise DailymotionAuthError(str(e)) 239 | 240 | if 'error' in result: 241 | raise DailymotionAuthError(result.get('error_description','')) 242 | 243 | if 'access_token' not in result: 244 | raise DailymotionAuthError("Invalid token server response : ", str(result)) 245 | 246 | result = { 247 | 'access_token': result['access_token'], 248 | 'expires': int(time.time() + int(result['expires_in']) * 0.85), # refresh at 85% of expiration time for safety 249 | 'refresh_token': result['refresh_token'] if 'refresh_token' in result else None, 250 | 'scope': result['scope'] if 'scope' in result else [], 251 | } 252 | 253 | if self._session_store_enabled and self._session_store != None: 254 | self._session_store.set(result) 255 | return result 256 | 257 | def set_access_token(self, access_token): 258 | self._session_store.set_value('access_token', access_token) 259 | 260 | def get_access_token(self, force_refresh=False, request_args=None): 261 | params = {} 262 | access_token = self._session_store.get_value('access_token') 263 | 264 | if access_token is None and self._grant_type is None: 265 | return None 266 | 267 | if self._session_store_enabled and access_token is not None: 268 | if access_token and not force_refresh and time.time() < self._session_store.get_value('expires', 0): 269 | return access_token 270 | 271 | refresh_token = self._session_store.get_value('refresh_token') 272 | if self._session_store_enabled and refresh_token is not None: 273 | if refresh_token: 274 | params = { 275 | 'grant_type': 'refresh_token', 276 | 'client_id': self._grant_info['key'], 277 | 'client_secret': self._grant_info['secret'], 278 | 'scope': ' '.join(self._grant_info['scope']) if 'scope' in self._grant_info and self._grant_info['scope'] else '', 279 | 'refresh_token': refresh_token, 280 | } 281 | response = self.oauth_token_request(params) 282 | return response.get('access_token') 283 | 284 | if self._grant_type == 'authorization': 285 | if request_args and 'code' in request_args: 286 | params = { 287 | 'grant_type': 'authorization_code', 288 | 'client_id': self._grant_info['key'], 289 | 'client_secret': self._grant_info['secret'], 290 | 'redirect_uri': self._grant_info['redirect_uri'], 291 | 'scope': ' '.join(self._grant_info['scope']) if 'scope' in self._grant_info and self._grant_info['scope'] else '', 292 | 'code': request_args['code'], 293 | } 294 | 295 | elif request_args and 'error' in request_args: 296 | error_msg = request_args.get('error_description') 297 | if request_args['error'] == 'error_description': 298 | raise DailymotionAuthError(error_msg) 299 | else: 300 | raise DailymotionAuthError(error_msg) 301 | else: 302 | params = { 303 | 'grant_type': self._grant_type, 304 | 'client_id': self._grant_info['key'], 305 | 'username': self._grant_info['username'], 306 | 'client_secret': self._grant_info['secret'], 307 | 'scope': ' '.join(self._grant_info['scope']) if 'scope' in self._grant_info and self._grant_info['scope'] else '', 308 | } 309 | if self._grant_type == 'password': 310 | params['password'] = self._grant_info['password'] 311 | 312 | response = self.oauth_token_request(params) 313 | return response.get('access_token') 314 | 315 | def logout(self): 316 | self.call('/logout') 317 | self._session_store.clear() 318 | 319 | def get(self, endpoint, params=None): 320 | return self.call(endpoint, params=params) 321 | 322 | def post(self, endpoint, params=None, files=None): 323 | return self.call(endpoint, method='POST', params=params) 324 | 325 | def delete(self, endpoint, params=None): 326 | return self.call(endpoint, method='DELETE', params=params) 327 | 328 | def call(self, endpoint, method='GET', params=None, files=None): 329 | try: 330 | access_token = self.get_access_token() 331 | if access_token: 332 | self._headers['Authorization'] = 'Bearer %s' % access_token 333 | return self.request(endpoint, method, params, files) 334 | except DailymotionTokenExpired: 335 | access_token = 'Bearer %s' % self.get_access_token(True) 336 | if access_token: 337 | self._headers['Authorization'] = 'Bearer %s' % access_token 338 | 339 | return self.request(endpoint, method, params, files) 340 | 341 | def upload(self, file_path, progress=None, workers=0): 342 | if not os.path.exists(file_path): 343 | raise IOError("[Errno 2] No such file or directory: '%s'" % file_path) 344 | 345 | if sys.version[0] == 2 and isinstance(file_path, unicode): 346 | file_path = file_path.encode('utf8') 347 | 348 | file_path = os.path.abspath(os.path.expanduser(file_path)) 349 | 350 | result = self.get('/file/upload') 351 | headers = { 352 | 'User-Agent': 'Dailymotion-Python/%s (Python %s)' % (__version__, __python_version__) 353 | } 354 | 355 | if workers > 0 and xupload: 356 | x = xupload.Xupload( 357 | result['upload_url'], 358 | file_path, 359 | workers=workers, 360 | headers=headers, 361 | progress=progress 362 | ) 363 | response = x.start() 364 | else: 365 | m = MultipartEncoder(fields={'file': (os.path.basename(file_path), open(file_path, 'rb'))}) 366 | headers['Content-Type'] = m.content_type 367 | 368 | r = requests.post(result['upload_url'], data=m, headers=headers, timeout=self.timeout) 369 | 370 | try: 371 | response = json.loads(r.text) 372 | except ValueError as e: 373 | raise DailymotionUploadInvalidResponse('Invalid API server response.\n%s' % str(e)) 374 | 375 | if 'error' in response: 376 | raise DailymotionUploadError(response['error']) 377 | 378 | return response['url'] 379 | 380 | def request(self, endpoint, method='GET', params=None, files=None): 381 | params = params or {} 382 | 383 | if endpoint.find('http') == 0: 384 | url = endpoint 385 | else: 386 | if endpoint.find('/') != 0: 387 | raise DailymotionClientError('Endpoint must start with / (eg:/me/video)') 388 | url = '%s%s' % (self.api_base_url, endpoint) 389 | 390 | method = method.lower() 391 | 392 | if not method in ('get', 'post', 'delete'): 393 | raise DailymotionClientError('Method must be of GET, POST or DELETE') 394 | 395 | func = getattr(requests, method) 396 | try: 397 | if method == 'get': 398 | response = func(url, params=params, headers=self._headers, timeout=self.timeout) 399 | else: 400 | response = func(url, 401 | data=params, 402 | files=files, 403 | headers=self._headers, 404 | timeout=self.timeout) 405 | 406 | except requests.exceptions.ConnectionError: 407 | raise DailymotionClientError('Network problem (DNS failure, refused connection...).') 408 | except requests.exceptions.HTTPError: 409 | raise DailymotionClientError('Invalid HTTP response') 410 | except requests.exceptions.Timeout: 411 | raise DailymotionApiError('The request times out, current timeout is = %s' % self.timeout) 412 | except requests.exceptions.TooManyRedirects: 413 | raise DailymotionApiError('The request exceeds the configured number of maximum redirections') 414 | except requests.exceptions.RequestException: 415 | raise DailymotionClientError('An unknown error occurred.') 416 | 417 | try: 418 | content = response.json if isinstance(response.json, dict) else response.json() 419 | except ValueError: 420 | raise DailymotionApiError('Unable to parse response, invalid JSON.') 421 | 422 | 423 | if response.status_code != 200: 424 | if content.get('error') is not None: 425 | if response.status_code in (400, 401, 403): 426 | authenticate_header = response.headers.get('www-authenticate') 427 | if authenticate_header: 428 | m = re.match('.*error="(.*?)"(?:, error_description="(.*?)")?', authenticate_header) 429 | if m: 430 | error = m.group(1) 431 | msg = m.group(2) 432 | if error == 'invalid_token': 433 | raise DailymotionTokenExpired(msg, error_type=error) 434 | raise DailymotionAuthError(msg, error_type='auth_error') 435 | 436 | error = content['error'] 437 | error_type = error.get('type', '') 438 | error_message = error.get('message', '') 439 | 440 | raise DailymotionApiError(error_message, error_type=error_type) 441 | 442 | return content 443 | -------------------------------------------------------------------------------- /examples/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dailymotion/dailymotion-sdk-python/7c7ba8e1663648b663ee86ce252b4498e68ed1f4/examples/video.mp4 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | requests_toolbelt 3 | pytest 4 | aiohttp!=4.0.0a1;python_version>"3.4" 5 | aiofiles;python_version>"3.4" 6 | asyncio;python_version>"3.4" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os, sys 3 | 4 | setup(name='dailymotion', 5 | version='0.2.5', 6 | description='Dailymotion API SDK', 7 | long_description='Dailymotion API SDK', 8 | classifiers=[ 9 | "Programming Language :: Python", 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: Apache Software License", 13 | "Operating System :: MacOS :: MacOS X", 14 | "Operating System :: Microsoft :: Windows", 15 | "Operating System :: POSIX", 16 | "Programming Language :: Python :: 3" 17 | ], 18 | keywords=['dailymotion', 'api', 'sdk', 'graph'], 19 | author='Samir AMZANI', 20 | author_email='samir.amzani@gmail.com', 21 | url='http://github.com/dailymotion/dailymotion-sdk-python', 22 | license='Apache License, Version 2.0', 23 | include_package_data=True, 24 | zip_safe=False, 25 | py_modules = ['dailymotion','xupload'], 26 | setup_requires=["wheel"], 27 | install_requires=[ 28 | 'requests', 29 | 'requests_toolbelt', 30 | 'aiohttp!=4.0.0a1;python_version>"3.4"', 31 | 'aiofiles;python_version>"3.4"', 32 | 'asyncio;python_version>"3.4"' 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /xupload.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import aiofiles 3 | import asyncio 4 | import os 5 | 6 | 7 | class Xupload(object): 8 | 9 | PROXY_SOCKET = "" 10 | _QUERY_TIMEOUT = 60 * 60 11 | _CHUNK_SIZE = 4 << 20 12 | 13 | def __init__(self, upload_url, file_path, workers=1, headers=None, progress=None): 14 | if not os.path.exists(file_path): 15 | raise IOError("[Errno 2] No such file or directory: '%s'" % file_path) 16 | 17 | self._url = upload_url 18 | self._file_path = file_path 19 | self._file_size = os.stat(self._file_path).st_size 20 | self._workers = int(max(1, min(workers, self._file_size / self._CHUNK_SIZE, 8))) 21 | self._headers = headers if isinstance(headers,dict) else {} 22 | self._progress = progress 23 | self._chunk_size = self._file_size / self._workers 24 | self._chunk_size = ( 25 | int(self._chunk_size / int(self._chunk_size / self._CHUNK_SIZE)) 26 | if int(self._chunk_size / self._CHUNK_SIZE) > 0 27 | else int(self._chunk_size) 28 | ) 29 | self._session = None 30 | self._clients = [] 31 | self._tasks = [] 32 | chunks = int(round(self._file_size / self._chunk_size / self._workers)) 33 | 34 | for index in range(self._workers): 35 | start = index * self._chunk_size * chunks 36 | end = ( 37 | self._file_size - 1 38 | if index == self._workers - 1 39 | else ((index + 1) * self._chunk_size * chunks) - 1 40 | ) 41 | self._clients.append({ 42 | 'start': start, 43 | 'offset': start, 44 | 'end': end, 45 | 'size': 0, 46 | 'sent': 0 47 | }) 48 | 49 | def start(self): 50 | loop = asyncio.get_event_loop() 51 | result = loop.run_until_complete(self._run()) 52 | loop.run_until_complete(asyncio.sleep(0.250)) 53 | loop.close() 54 | return result 55 | 56 | async def _prepare_handle(self, client): 57 | async with aiofiles.open(self._file_path, "rb") as file: 58 | client['size'] = min(self._chunk_size, client['end'] - client['offset'] + 1) 59 | client['data'] = await self._get_file_chunk(file, client['size'], client['offset']) 60 | client['headers'] = { 61 | **self._headers, 62 | **{ 63 | 'Accept': '*/*', 64 | 'Content-Type': 'application/octet-stream', 65 | 'Content-Disposition': 'attachment; filename="{}"'.format( 66 | os.path.basename(self._file_path) 67 | ), 68 | 'Content-Range': 'bytes {}-{}/{}'.format( 69 | client['offset'], 70 | client['offset'] + client['size'] - 1, 71 | self._file_size 72 | ) 73 | } 74 | } 75 | self._tasks.append( 76 | asyncio.ensure_future(self._post_chunk(self._url, client)) 77 | ) 78 | 79 | async def _run(self): 80 | self._session = aiohttp.ClientSession( 81 | timeout=aiohttp.ClientTimeout(total=self._QUERY_TIMEOUT), 82 | connector=aiohttp.TCPConnector(limit=self._workers) 83 | ) 84 | 85 | if self._progress: 86 | self._progress(0, self._file_size) 87 | 88 | async with self._session: 89 | for client in self._clients: 90 | await self._prepare_handle(client) 91 | 92 | while len(self._tasks) > 0: 93 | await asyncio.sleep(0.3) 94 | for task in self._tasks: 95 | if task.done(): 96 | self._tasks.remove(task) 97 | result = task.result() 98 | exception = task.exception() 99 | 100 | if result['status'] == 200: 101 | if self._progress: 102 | self._progress(self._file_size, self._file_size) 103 | return result["content"] 104 | if result['status'] in (202, 416): 105 | client = self._get_client_from_request(result['request_info']) 106 | client['sent'] += client['size'] 107 | 108 | if self._progress: 109 | sent = 0 110 | for c in self._clients: 111 | sent += c['sent'] 112 | self._progress(min(sent,self._file_size), self._file_size) 113 | 114 | ranges = [] 115 | range_header = result['headers'].get('Range').split('/') 116 | if len(range_header) > 0: 117 | ranges = [r.split('-') for r in range_header[0].split(',')] 118 | 119 | for r_start, r_end in [[int(i), int(j)] for i,j in ranges]: 120 | if client['start'] >= r_start and client['start'] < r_end: 121 | if client['end'] <= r_end: 122 | break 123 | if (r_end - client['start'] + 1) % self._chunk_size == 0: 124 | client['offset'] = r_end + 1 125 | await self._prepare_handle(client) 126 | break 127 | elif 'content' in result and 'error' in result['content']: 128 | return result['content'] 129 | 130 | if exception: 131 | raise DailymotionXuploadError(str(exception)) 132 | 133 | def _get_client_from_request(self, request_info): 134 | for client in self._clients: 135 | if client['headers']['Content-Range'] == request_info.headers['Content-Range']: 136 | return client 137 | 138 | @staticmethod 139 | def print_progress(current, total): 140 | """ 141 | Example of function which prints the percentage of progression 142 | :param current: current bytes sent 143 | :param total: total bytes 144 | :return: None 145 | """ 146 | percent = int(min((current * 100) / total, 100)) 147 | 148 | print( 149 | "[{}{}] {}%\r".format( 150 | "*" * int(percent), " " * (100 - int(percent)), percent 151 | ), 152 | flush=True, 153 | end="", 154 | ) 155 | 156 | async def _post_chunk(self, url, client): 157 | async with self._session.post( 158 | url, 159 | data=client["data"], 160 | headers=client["headers"], 161 | proxy=self.PROXY_SOCKET, 162 | expect100=True, 163 | ) as resp: 164 | return { 165 | "status": resp.status, 166 | "headers": resp.headers, 167 | "content": await resp.json(), 168 | "request_info": resp.request_info, 169 | } 170 | 171 | async def _get_file_chunk(self, file, chunk_length, chunk_start=0): 172 | await file.seek(chunk_start) 173 | return await file.read(chunk_length) 174 | 175 | 176 | class DailymotionXuploadError(Exception): 177 | pass 178 | --------------------------------------------------------------------------------