├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── aparat ├── __init__.py └── aparat.py ├── setup.py └── test ├── __init__.py └── test_aparat.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | # End of https://www.toptal.com/developers/gitignore/api/python 167 | 168 | # vscode setting 169 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mahdi Khashan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aparat Python API 2 | ![Aparat Logo](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Aparat_English_Square.png/250px-Aparat_English_Square.png) 3 | 4 | [Aparat](http://www.aparat.com/) (Persian: آپارات‎, Âpârât) is an Iranian video sharing service (source: wikipedia [1]) 5 | 6 | ## Features 7 | 8 | - [x] Login 9 | - [x] Profile 10 | - [x] Search 11 | - [x] Profile Categories 12 | - [x] Video 13 | - [x] Video Categories 14 | - [x] Video By User 15 | - [x] Comment By Video 16 | - [x] Video By Tag 17 | - [x] Upload Video 18 | - [ ] Download Video 19 | 20 | ## How to login 21 | 22 | ```python 23 | from aparat import Aparat 24 | 25 | 26 | aparat = Aparat() 27 | 28 | user = aparat.login('username', 'password') 29 | ``` 30 | 31 | 32 | ## How to upload a video 33 | 34 | ```python 35 | from aparat import Aparat 36 | 37 | 38 | aparat = Aparat() 39 | 40 | user = aparat.login(username, password) 41 | form = aparat.uploadForm(user.username, user.ltoken) 42 | 43 | video = aparat.uploadPost( 44 | form=form, 45 | video_path='video.mp4', 46 | title='title', 47 | category=10, 48 | tags=['tag1', 'tag2', 'tag3'], 49 | allow_comment=True, 50 | descreption='desc', 51 | video_pass=False 52 | ) 53 | ``` 54 | 55 | 56 | ## Contributors 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /aparat/__init__.py: -------------------------------------------------------------------------------- 1 | from aparat.aparat import Aparat -------------------------------------------------------------------------------- /aparat/aparat.py: -------------------------------------------------------------------------------- 1 | """Aparat Video Platform 2 | 3 | author: Mahdi Khashan 4 | data: 1399/02/13 5 | """ 6 | 7 | import os 8 | import json 9 | import hashlib 10 | import urllib3 11 | import certifi 12 | import requests 13 | 14 | 15 | class Base(object): 16 | """Base Object""" 17 | 18 | def __init__(self, dic): 19 | """init""" 20 | self.dic = dic 21 | for key in dic.keys(): 22 | setattr(self, key, dic[key]) 23 | 24 | 25 | class CommentList(object): 26 | """ Aparat CommentList Model 27 | TODO: 28 | """ 29 | 30 | def __init__(self, dic): 31 | self.dic = dic 32 | for key in dic.keys(): 33 | setattr(self, key, dic[key]) 34 | 35 | 36 | class Category(object): 37 | """ Aparat Categories Model 38 | TODO: 39 | """ 40 | 41 | def __init__(self, dic): 42 | self.dic = dic 43 | for key in dic.keys(): 44 | setattr(self, key, dic[key]) 45 | 46 | 47 | class Channel(object): 48 | """ Aparat User Model 49 | TODO: 50 | """ 51 | 52 | def __init__(self, dic): 53 | self.dic = dic 54 | for key in dic.keys(): 55 | setattr(self, key, dic[key]) 56 | 57 | 58 | class ChannelInfo(object): 59 | """ Aparat User Info Model 60 | TODO: 61 | """ 62 | 63 | def __init__(self): 64 | pass 65 | 66 | 67 | class Video(object): 68 | """ Aparat Video Model 69 | TODO: 70 | """ 71 | 72 | def __init__(self, dic): 73 | self.dic = dic 74 | for key in dic.keys(): 75 | setattr(self, key, dic[key]) 76 | 77 | 78 | class VideoList(Base): 79 | """ Aparat Video List Model 80 | TODO: 81 | """ 82 | 83 | def __init__(self, dic): 84 | super().__init__(dic) 85 | 86 | 87 | class Profile(Base): 88 | """ 89 | Profile Object 90 | 91 | :param id: user id -> (int) 92 | :param username: user username -> (string) 93 | :param name: user name -> (string) 94 | :param pic: does user have profile pic or not -> (string) 95 | :param ltokem: token when user need login e.x: video uploading -> (string) 96 | :param banned: whether usesr is banned or not -> (string) 97 | :param email: user email -> (string) 98 | :param mobile_number: user mobile number -> (string) 99 | :param movile_valid: validated user mobile number -> (string) 100 | :param pic_s: small sized user profile pic -> (url) 101 | :param pic_m: medium sized user profile pic -> (url) 102 | :param pic_b: big sized user profile pic -> (url) 103 | """ 104 | 105 | def __init__(self, dic): 106 | super().__init__(dic) 107 | 108 | 109 | class Form(Base): 110 | """ 111 | Form Action Object 112 | 113 | :param formAction: data to post should be to this address -> (url) 114 | :param directuploadAction: no information -> (url) 115 | :param frm-id: form id -> (int) 116 | """ 117 | 118 | def __init__(self, dic): 119 | super().__init__(dic) 120 | 121 | 122 | class Aparat(object): 123 | """ Aparat Api Model 124 | TODO: 125 | """ 126 | 127 | def __init__(self): 128 | pass 129 | 130 | def __sh1_mdf5__(self, value): 131 | mdf5 = hashlib.md5() 132 | sha1 = hashlib.sha1() 133 | mdf5.update(value.encode('utf-8')) 134 | sha1.update(mdf5.hexdigest().encode('utf-8')) 135 | hashedValue = sha1.hexdigest() 136 | return hashedValue 137 | 138 | def login(self, luser, lpass): 139 | hashedpass = self.__sh1_mdf5__(lpass) 140 | r = requests.get( 141 | 'https://www.aparat.com/etc/api/login/luser/{}/lpass/{}'.format(luser, hashedpass)) 142 | if r.status_code == 200: 143 | data = json.loads(r.text) 144 | dic = data['login'] 145 | profile = Profile(dic) 146 | return profile 147 | else: 148 | return None 149 | 150 | def profile(self, username): 151 | r = requests.get( 152 | 'https://www.aparat.com/etc/api/profile/username/{}'.format(username)) 153 | if r.status_code == 200: 154 | data = json.loads(r.text) 155 | dic = data['profile'] 156 | channel = Channel(dic) 157 | return channel 158 | else: 159 | return None 160 | 161 | def userBySearch(self, text, perpage=10): 162 | r = requests.get( 163 | 'https://www.aparat.com/etc/api/userBySearch/text/{}/perpage/{}'.format(text, perpage)) 164 | if r.status_code == 200: 165 | data = json.loads(r.text) 166 | dic = data['userbysearch'] 167 | channels = [] 168 | for channel in dic: 169 | c = Channel(channel) 170 | channels.append(c) 171 | return channels 172 | else: 173 | return None 174 | 175 | def profileCategories(self, username): 176 | r = requests.get( 177 | 'https://www.aparat.com/etc/api/profilecategories/username/{}'.format(username)) 178 | if r.status_code == 200: 179 | data = json.loads(r.text) 180 | dic = data['profilecategories'] 181 | categories = [] 182 | for category in dic: 183 | c = Category(category) 184 | categories.append(c) 185 | return categories 186 | else: 187 | return None 188 | 189 | def video(self, videohash): 190 | r = requests.get( 191 | ' https://www.aparat.com/etc/api/video/videohash/{}'.format(videohash)) 192 | if r.status_code == 200: 193 | data = json.loads(r.text) 194 | dic = data['video'] 195 | video = Video(dic) 196 | return video 197 | else: 198 | return None 199 | 200 | def categoryVideo(self, perpage=10, cat=1): 201 | r = requests.get( 202 | 'https://www.aparat.com/etc/api/categoryVideos/cat/{}/perpage/{}'.format(cat, perpage)) 203 | if r.status_code == 200: 204 | data = json.loads(r.text) 205 | dic = data['categoryvideos'] 206 | catVideos = [] 207 | for catVideo in dic: 208 | cv = VideoList(catVideo) 209 | catVideos.append(cv) 210 | return catVideos 211 | else: 212 | return None 213 | 214 | def videoByUser(self, username, perpage=10): 215 | r = requests.get( 216 | 'https://www.aparat.com/etc/api/videoByUser/username/{}/perpage/{}'.format(username, perpage)) 217 | if r.status_code == 200: 218 | data = json.loads(r.text) 219 | dic = data['videobyuser'] 220 | videos = [] 221 | for video in dic: 222 | v = VideoList(video) 223 | videos.append(v) 224 | return videos 225 | else: 226 | return None 227 | 228 | def commentByVideos(self, videohash, perpage=10): 229 | r = requests.get( 230 | 'https://www.aparat.com/etc/api/commentByVideos/videohash/{}/perpage/{}'.format(videohash, perpage)) 231 | if r.status_code == 200: 232 | data = json.loads(r.text) 233 | dic = data['videobyuser'] 234 | comments = [] 235 | for comment in dic: 236 | c = CommentList(comment) 237 | comments.append(c) 238 | return comments 239 | else: 240 | return None 241 | 242 | def videoBySearch(self, text, perpage=10): 243 | r = requests.get( 244 | 'https://www.aparat.com/etc/api/videoBySearch/text/{}/perpage/{}'.format(text, perpage)) 245 | if r.status_code == 200: 246 | data = json.loads(r.text) 247 | dic = data['videobysearch'] 248 | videos = [] 249 | for video in dic: 250 | v = VideoList(video) 251 | videos.append(v) 252 | return videos 253 | else: 254 | return None 255 | 256 | def videoByTag(self, text): 257 | r = requests.get( 258 | 'https://www.aparat.com/etc/api/videobytag/text/{}'.format(text)) 259 | if r.status_code == 200: 260 | data = json.loads(r.text) 261 | dic = data['videobytag'] 262 | videos = [] 263 | for video in dic: 264 | v = VideoList(video) 265 | videos.append(v) 266 | return videos 267 | else: 268 | return None 269 | 270 | def uploadForm(self, luser, ltoken): 271 | r = requests.get( 272 | 'https://www.aparat.com/etc/api/uploadform/luser/{}/ltoken/{}'.format(luser, ltoken)) 273 | if r.status_code == 200: 274 | data = json.loads(r.text) 275 | dic = data['uploadform'] 276 | frm_id = dic['frm-id'] 277 | dic['frm_id'] = frm_id 278 | del dic['frm-id'] 279 | form = Form(dic) 280 | return form 281 | else: 282 | return None 283 | 284 | def uploadPost( 285 | self, 286 | video_path: str, 287 | title: str, 288 | category: int, 289 | form: Form, 290 | tags: 'list[str]' = None, 291 | allow_comment: bool = None, 292 | descreption: str = None, 293 | video_pass: bool = None 294 | ): 295 | 296 | url = form.formAction 297 | with open(video_path, 'rb') as f: 298 | video_data = f.read() 299 | 300 | data = { 301 | 'frm-id': form.frm_id, 302 | 'data[title]': title, 303 | 'data[category]': category 304 | } 305 | 306 | if tags: 307 | data['data[tags]'] = ','.join(tags) 308 | if allow_comment is not None: 309 | data['data[comment]'] = allow_comment 310 | if descreption is not None: 311 | data['data[descr]'] = descreption 312 | if video_pass is not None: 313 | data['data[video_pass]'] = video_pass 314 | 315 | urllib_http = urllib3.PoolManager(ca_certs=certifi.where()) 316 | resp = urllib_http.request("POST", url, fields={ 317 | "video": (video_path, video_data), 318 | **data 319 | }) 320 | 321 | if resp.status != 200: 322 | raise Exception("Upload failed") 323 | 324 | try: 325 | response_data = json.loads(resp.data.decode('utf-8')) 326 | except Exception as ex: 327 | raise Exception("Upload failed") 328 | 329 | videohash = response_data['uploadpost']['uid'] 330 | return self.video(videohash) 331 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding = "utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name = "aparat-python", 8 | version = "0.0.2", 9 | author = "Mahdi Khashan", 10 | author_email = "mahdikhashan1@gmail.com", 11 | description = "Access aparat video sharing api", 12 | long_description = long_description, 13 | long_description_content_type = "text/markdown", 14 | url = "https://pypi.org/project/aparat-python/", 15 | license='LICENSE.txt', 16 | project_urls = { 17 | "Bug Tracker": "https://github.com/mahdikhashan/aparat-python/issues", 18 | }, 19 | classifiers = [ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | packages = ['aparat'], 25 | python_requires = ">=3.6", 26 | install_requires=[ 27 | "certifi", 28 | "charset-normalizer", 29 | "idna", 30 | "requests", 31 | "urllib3", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdikhashan/aparat-python/d4420c4ee42ed0c3aee1118b7941c6a744d092b5/test/__init__.py -------------------------------------------------------------------------------- /test/test_aparat.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from aparat import Aparat, Video 4 | 5 | 6 | class TestAparat(unittest.TestCase): 7 | def setUp(self) -> None: 8 | self.username = os.environ.get('APARAT_USERNAME') 9 | self.password = os.environ.get('APARAT_PASSWORD') 10 | 11 | def test_aparat_login(self): 12 | aparat = Aparat() 13 | p = aparat.login(self.username, self.password) 14 | self.assertEqual(p.type, 'success') 15 | 16 | def test_aparat_profile(self): 17 | aparat = Aparat() 18 | p = aparat.profile('alooty') 19 | self.assertEqual(p.url, 'http://aparat.com/alooty') 20 | 21 | def test_aparat_user_by_search(self): 22 | aparat = Aparat() 23 | #u = aparat.userBySearch('masaf') 24 | #self.assertEqual(u[0].userid, 358277) 25 | 26 | def test_aparat_profile_categories(self): 27 | aparat = Aparat() 28 | c = aparat.profileCategories('masaf.ir') 29 | self.assertEqual(c[0].id, '131119') 30 | 31 | def test_aparat_video(self): 32 | aparat = Aparat() 33 | v = aparat.video('rzKus') 34 | self.assertEqual(v.id, '1146420') 35 | 36 | def test_aparat_category_video(self): 37 | aparat = Aparat() 38 | cv = aparat.categoryVideo(10, 1) 39 | self.assertEqual(cv[0].id, '21036349') 40 | 41 | def test_aparat_video_by_user(self): 42 | aparat = Aparat() 43 | v = aparat.videoByUser('alooty', 10) 44 | self.assertEqual(v[0].id, '18965323') 45 | 46 | def test_aparat_comment_by_video(self): 47 | # problem with login 48 | self.assertEqual(1, 1) 49 | 50 | def test_aparat_video_by_tag(self): 51 | aparat = Aparat() 52 | v = aparat.videoByTag('iran') 53 | self.assertEqual(v[0].id, '20977126') 54 | 55 | def test_aparat_upload_form(self): 56 | aparat = Aparat() 57 | video_title = 'test_video' 58 | video_desc = 'desc' 59 | video_tags = ['tag1', 'tag2', 'tag3'] 60 | user = aparat.login(self.username, self.password) 61 | form = aparat.uploadForm(user.username, user.ltoken) 62 | video = aparat.uploadPost( 63 | form=form, 64 | video_path='test_video.mp4', 65 | title=video_title, 66 | category=10, 67 | tags=video_tags, 68 | allow_comment=True, 69 | descreption=video_desc, 70 | video_pass=False 71 | ) 72 | self.assertIsInstance(video, Video) 73 | self.assertNotEqual(video.uid, None) 74 | self.assertNotEqual(video.uid, '') 75 | video_action = video.watch_action.get('type') 76 | self.assertEqual(video_action, 'watch') 77 | self.assertEqual(video.title, video_title) 78 | self.assertEqual(video.description, video_desc) 79 | self.assertEqual(video.tag_str, ','.join(video_tags)) 80 | 81 | 82 | if __name__ == "__main__": 83 | unittest.main() 84 | --------------------------------------------------------------------------------