From 94fdc825680db77dcaa7cf7e6ce5469de162d134 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 9 Aug 2024 11:52:12 +0330 Subject: [PATCH 01/20] Add: Put Event Controller --- soundcld/__init__.py | 24 ++++++++++++++------- soundcld/api_handler.py | 7 +++++- soundcld/request_handler.py | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index e4cbd73..1824918 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -637,20 +637,21 @@ def get_my_following_ids( } return self._get_id_list(link, **param) - def put_me_info( + def change_my_profile_info( self, - permalink: str, - username: str, - city: str = '', - country_code: str = 'EN', - description: str = '', - first_name: str = '', - last_name: str = '' + permalink: str = None, + username: str = None, + city: str = None, + country_code: str = None, + description: str = None, + first_name: str = None, + last_name: str = None ): """ Changes My {Logged-In User} Information. """ link = '/me' + last_info = self.get_user(self.my_account_id) payload = { 'city': city, 'country_code': country_code, @@ -660,4 +661,11 @@ def put_me_info( 'permalink': permalink, 'username': username } + for item, value in payload.items(): + if not value: + payload[item] = last_info[item] return self._put_payload(link, payload) + + def like_track(self, track_id:int): + link = f'/users/{self.my_account_id}/track_likes/{track_id}' + return self._put_opt_payload(link) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index ea8f8bb..64d09e1 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -15,7 +15,7 @@ GetReq, ListGetReq, CollectionGetReq, - PutReq + PutReq, PutOptReq ) from soundcld.resource import ( SearchItem, Like, RepostItem, StreamItem, @@ -184,6 +184,11 @@ def _put_payload(self, req: str, payload: dict) -> bool: return PutReq(self, req)(payload) return False + def _put_opt_payload(self, req: str) -> bool: + if self.is_logged_in(): + return PutOptReq(self, req)() + return False + def generate_client_id(self) -> None: """ Gets Client ID, App Version And User ID diff --git a/soundcld/request_handler.py b/soundcld/request_handler.py index d6440ba..694b8b8 100644 --- a/soundcld/request_handler.py +++ b/soundcld/request_handler.py @@ -167,3 +167,46 @@ def __call__(self, payload, **kwargs): print('User Information Updated.') if data is not None else ( print('User Information Not Updated.')) return bool(data) + + +@dataclass +class PutOptReq(BaseReq): + def _load_href(self, url: str, param: dict): + params = urllib.parse.urlencode( + param, + quote_via=urllib.parse.quote + ) + self.client.cookies['Content-Length'] = '0' + with requests.options( + url=url, + params=params, + timeout=20, + cookies=self.client.cookies, + headers=self.client.headers + ) as req: + if req.status_code not in [200, 201]: + print(f'Something Went Wrong. Can\'t Get Options.' + f'Error {req.status_code}') + req.raise_for_status() + + with requests.put( + url=url, + params=params, + json={}, + timeout=20, + cookies=self.client.cookies, + headers=self.client.headers + ) as req: + if req.status_code not in [200, 201]: + print(f'Something Went Wrong. Can\'t Put Request.' + f'Error {req.status_code}') + return {} + req.raise_for_status() + return {'status': 'ok'} + + def __call__(self, **kwargs): + self._call_params(**kwargs) + data = self._load_href(self.resource_url, self.params) + print('User Information Updated.') if data is not None else ( + print('User Information Not Updated.')) + return bool(data) \ No newline at end of file From d2ccb87f8a02f731b64e6d0feb8a4ff7cf1990c1 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 18:10:21 +0330 Subject: [PATCH 02/20] Edit: Put Request Handler --- soundcld/request_handler.py | 89 +++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/soundcld/request_handler.py b/soundcld/request_handler.py index 694b8b8..abc5b49 100644 --- a/soundcld/request_handler.py +++ b/soundcld/request_handler.py @@ -1,6 +1,7 @@ """ Request Handler Of SoundCld """ +import json import urllib.parse from dataclasses import dataclass from typing import Optional, Dict, Generic, TypeVar, get_origin, Union, List @@ -147,66 +148,56 @@ def _load_href( param, quote_via=urllib.parse.quote ) - with requests.put( - url=url, - params=params, - json=payload, - timeout=20, - cookies=self.client.cookies, - headers=self.client.headers - ) as req: - if req.status_code not in [200, 201]: - print(f'Something Went Wrong. Error {req.status_code}') - return {} - req.raise_for_status() - return {'status': 'ok'} - - def __call__(self, payload, **kwargs): - self._call_params(**kwargs) - data = self._load_href(self.resource_url, self.params, payload) - print('User Information Updated.') if data is not None else ( - print('User Information Not Updated.')) - return bool(data) - - -@dataclass -class PutOptReq(BaseReq): - def _load_href(self, url: str, param: dict): - params = urllib.parse.urlencode( - param, - quote_via=urllib.parse.quote - ) - self.client.cookies['Content-Length'] = '0' + put_cookies = self.client.cookies + put_headers = self.client.headers + put_headers['Content-Length'] = '0' + put_headers['x-datadome-clientid'] = self.client.cookies['datadome'] + if payload: + my_payload = json.dumps(payload) + my_payload = my_payload.replace(': "', ':"') + my_payload = my_payload.replace(', ', ',') + put_headers['Content-Length'] = f'{len(my_payload)}' with requests.options( url=url, - params=params, timeout=20, - cookies=self.client.cookies, - headers=self.client.headers + cookies=put_cookies, + headers=put_headers ) as req: if req.status_code not in [200, 201]: print(f'Something Went Wrong. Can\'t Get Options.' f'Error {req.status_code}') + return {'status': 'err'} + else: + print(f'option : {req.status_code} : {req.text}') req.raise_for_status() - - with requests.put( + req = requests.put( url=url, params=params, - json={}, + json=payload, timeout=20, - cookies=self.client.cookies, - headers=self.client.headers - ) as req: - if req.status_code not in [200, 201]: - print(f'Something Went Wrong. Can\'t Put Request.' - f'Error {req.status_code}') - return {} - req.raise_for_status() - return {'status': 'ok'} + cookies=put_cookies, + headers=put_headers + ) + if 'x-set-cookie' in req.headers.keys(): + x_set_cookie = req.headers['x-set-cookie'] + x_set_cookie = x_set_cookie.split(';') + for item in x_set_cookie: + if 'datadome' in item: + x_set_datadome_cookie = item.split('=')[1] + self.client.cookies['datadome'] = x_set_datadome_cookie + break + if req.status_code not in [200, 201]: + print(f'Something Went Wrong. Error {req.status_code}') + return {'status': 'err'} + print(f'putting : {req.status_code} : {req.text}') + req.raise_for_status() + return {'status': 'ok'} def __call__(self, **kwargs): self._call_params(**kwargs) - data = self._load_href(self.resource_url, self.params) - print('User Information Updated.') if data is not None else ( - print('User Information Not Updated.')) - return bool(data) \ No newline at end of file + data = self._load_href(self.resource_url, self.params, kwargs) + + if data['status'] == 'ok': + print('User Information Updated.') + else: + print('User Information Not Updated.') \ No newline at end of file From a59c28444e4e216ae57cb237de5a6af6c464248a Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 18:11:38 +0330 Subject: [PATCH 03/20] Edit: Put Function Updated --- soundcld/api_handler.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index 64d09e1..85a014f 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -15,7 +15,7 @@ GetReq, ListGetReq, CollectionGetReq, - PutReq, PutOptReq + PutReq ) from soundcld.resource import ( SearchItem, Like, RepostItem, StreamItem, @@ -179,14 +179,9 @@ def _get_conversation_messages(self, req: str, **param) -> Union[Iterator[Messag def _get_web_profile_list(self, req: str) -> List[WebProfile]: return ListGetReq[WebProfile](self, req, WebProfile)() - def _put_payload(self, req: str, payload: dict) -> bool: + def _put_payload(self, req: str, **payload: dict) -> bool: if self.is_logged_in(): - return PutReq(self, req)(payload) - return False - - def _put_opt_payload(self, req: str) -> bool: - if self.is_logged_in(): - return PutOptReq(self, req)() + return PutReq(self, req)(**payload) return False def generate_client_id(self) -> None: From 7a34e8b2664df708c260600fe6b107d5fc134e34 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 18:12:11 +0330 Subject: [PATCH 04/20] Add: Cookie Updater Func --- soundcld/api_handler.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index 85a014f..5541946 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -88,6 +88,16 @@ def __set_conf_last(self) -> None: with open(confDirectory, 'w', encoding='utf-8') as file: json.dump(config, file, indent=4) + def _update_cookies(self): + cookie = { + 'moe_uuid': self.cookies['moe_uuid'], + 'oauth_token': self.cookies['oauth_token'], + 'sc_anonymous_id': self.cookies['sc_anonymous_id'], + 'datadome': self.cookies['datadome'] + } + with open(cookieDirectory, 'w', encoding='utf-8') as file: + json.dump(cookie, file, indent=4) + def __get_cookies(self) -> None: if os.path.exists(cookieDirectory): with open(cookieDirectory, 'r', encoding='utf-8') as file: From 4eff1c14609f5942a8d447d1e06e2dfcc9da6ec4 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 18:12:58 +0330 Subject: [PATCH 05/20] Add: 2 Auth Api Funcs --- soundcld/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index 1824918..4ef2be6 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -664,8 +664,20 @@ def change_my_profile_info( for item, value in payload.items(): if not value: payload[item] = last_info[item] - return self._put_payload(link, payload) + return self._put_payload(link, **payload) def like_track(self, track_id:int): + """ + Likes The Track by Me {Logged-In User}. + """ link = f'/users/{self.my_account_id}/track_likes/{track_id}' - return self._put_opt_payload(link) + self._update_cookies() + return self._put_payload(link) + + def like_playlist(self, playlist_id: int): + """ + Likes The Playlist or Album by Me {Logged-In User}. + """ + link = f'/users/{self.my_account_id}/playlist_likes/{playlist_id}' + self._update_cookies() + return self._put_payload(link) From 0428851017191f8df444859edc99c08174e22779 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 19:02:51 +0330 Subject: [PATCH 06/20] Add: Delete Request Handler --- soundcld/request_handler.py | 71 ++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/soundcld/request_handler.py b/soundcld/request_handler.py index abc5b49..2d39bb2 100644 --- a/soundcld/request_handler.py +++ b/soundcld/request_handler.py @@ -200,4 +200,73 @@ def __call__(self, **kwargs): if data['status'] == 'ok': print('User Information Updated.') else: - print('User Information Not Updated.') \ No newline at end of file + print('User Information Not Updated.') + +@dataclass +class DeleteReq(BaseReq): + """ + Core Class To Send PUT Request + To Soundcloud + """ + def _load_href( + self, + url: str, + param: dict, + payload: dict + ) -> Dict: + params = urllib.parse.urlencode( + param, + quote_via=urllib.parse.quote + ) + delete_cookies = self.client.cookies + delete_headers = self.client.headers + delete_headers['Content-Length'] = '0' + delete_headers['x-datadome-clientid'] = self.client.cookies['datadome'] + if payload: + my_payload = json.dumps(payload) + my_payload = my_payload.replace(': "', ':"') + my_payload = my_payload.replace(', ', ',') + delete_headers['Content-Length'] = f'{len(my_payload)}' + with requests.options( + url=url, + timeout=20, + cookies=delete_cookies, + headers=delete_headers + ) as req: + if req.status_code not in [200, 201]: + print(f'Something Went Wrong. Can\'t Get Options.' + f'Error {req.status_code}') + return {'status': 'err'} + else: + print(f'option : {req.status_code} : {req.text}') + req.raise_for_status() + req = requests.delete( + url=url, + params=params, + json=payload, + timeout=20, + cookies=delete_cookies, + headers=delete_headers + ) + if 'x-set-cookie' in req.headers.keys(): + x_set_cookie = req.headers['x-set-cookie'] + x_set_cookie = x_set_cookie.split(';') + for item in x_set_cookie: + if 'datadome' in item: + x_set_datadome_cookie = item.split('=')[1] + self.client.cookies['datadome'] = x_set_datadome_cookie + break + if req.status_code not in [200, 201]: + print(f'Something Went Wrong. Error {req.status_code}') + return {'status': 'err'} + print(f'deleting : {req.status_code} : {req.text}') + req.raise_for_status() + return {'status': 'ok'} + + def __call__(self, **kwargs): + self._call_params(**kwargs) + data = self._load_href(self.resource_url, self.params, kwargs) + if data['status'] == 'ok': + print('User Information Updated.') + else: + print('User Information Not Updated.') From adcb75b28581527b8d1f4840607d85edc9a92d9f Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 19:03:23 +0330 Subject: [PATCH 07/20] Add: Delete Function Added --- soundcld/api_handler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index 5541946..fb1c201 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -15,7 +15,8 @@ GetReq, ListGetReq, CollectionGetReq, - PutReq + PutReq, + DeleteReq ) from soundcld.resource import ( SearchItem, Like, RepostItem, StreamItem, @@ -194,6 +195,11 @@ def _put_payload(self, req: str, **payload: dict) -> bool: return PutReq(self, req)(**payload) return False + def _delete_payload(self, req: str, **payload: dict) -> bool: + if self.is_logged_in(): + return DeleteReq(self, req)(**payload) + return False + def generate_client_id(self) -> None: """ Gets Client ID, App Version And User ID From 7bca90f2d1719663b67b88423598cdcc128956cd Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 19:03:56 +0330 Subject: [PATCH 08/20] Add: 2 Auth Api-Delete Funcs --- soundcld/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index 4ef2be6..99fc546 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -681,3 +681,19 @@ def like_playlist(self, playlist_id: int): link = f'/users/{self.my_account_id}/playlist_likes/{playlist_id}' self._update_cookies() return self._put_payload(link) + + def dislike_track(self, track_id: int): + """ + Dislikes The Track by Me {Logged-In User}. + """ + link = f'/users/{self.my_account_id}/track_likes/{track_id}' + self._update_cookies() + return self._delete_payload(link) + + def dislike_playlist(self, playlist_id: int): + """ + Dislikes The Playlist or Album by Me {Logged-In User}. + """ + link = f'/users/{self.my_account_id}/playlist_likes/{playlist_id}' + self._update_cookies() + return self._delete_payload(link) From ec37e995b734a162e6e7b4a96d27d2fa40854f78 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 20:52:08 +0330 Subject: [PATCH 09/20] Edit: Last Valid Time Moved Into data.json --- soundcld/api_handler.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index fb1c201..51bbc18 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -33,7 +33,6 @@ confDirectory = scriptDirectory + '/data.json' cookieDirectory = scriptDirectory + '/cookies.json' headerDirectory = scriptDirectory + '/headers.json' -infoDirectory = scriptDirectory + '/run_data.json' @dataclass @@ -257,7 +256,7 @@ def is_logged_in(self) -> bool: """ if self.cookies: if all(self.cookies.values()): - if os.path.exists(infoDirectory): + if os.path.exists(confDirectory): time_diff = self.__valid_time_diff() if 0 < time_diff < 60: self.__save_validate_time() @@ -277,17 +276,19 @@ def is_logged_in(self) -> bool: return True return False - @staticmethod - def __save_validate_time(): + def __save_validate_time(self): json_dict = { + 'user_id': self.data['user_id'], + 'client_id': self.data['client_id'], + 'app_version': self.data['app_version'], 'last_validate': datetime.now().isoformat() } - with open(infoDirectory, 'w', encoding='utf-8') as file: + with open(confDirectory, 'w', encoding='utf-8') as file: json.dump(json_dict, file, indent=4) @staticmethod def __valid_time_diff() -> float: - with open(infoDirectory, 'r', encoding='utf-8') as file: + with open(confDirectory, 'r', encoding='utf-8') as file: loaded_json = json.load(file) if 'last_validate' in loaded_json.keys(): try: From 952d2f3998a0afdc0de186964cac0c3c48585af4 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Sun, 6 Oct 2024 20:53:10 +0330 Subject: [PATCH 10/20] Edit: Cookie Requirements Updated --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0786d6d..14ef244 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ named cookies.json. You will also need to change your "client_id" in data.json i { "moe_uuid": "", "oauth_token": "", - "sc_anonymous_id": "" + "sc_anonymous_id": "", + "datadome": "" } ``` From 8a0c16158d7bf464a9950b157142eca89c3381e7 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Wed, 9 Oct 2024 04:42:48 +0330 Subject: [PATCH 11/20] Add: Complex Requests Handler Class --- soundcld/request_handler.py | 125 ++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 68 deletions(-) diff --git a/soundcld/request_handler.py b/soundcld/request_handler.py index 2d39bb2..9ad1989 100644 --- a/soundcld/request_handler.py +++ b/soundcld/request_handler.py @@ -133,35 +133,27 @@ def __call__(self, **kwargs): @dataclass -class PutReq(BaseReq): +class ComplexReq: """ - Core Class To Send PUT Request - To Soundcloud + Core Class To Handle Complex + Requests Common Functionality. """ - def _load_href( - self, - url: str, - param: dict, - payload: dict - ) -> Dict: - params = urllib.parse.urlencode( - param, - quote_via=urllib.parse.quote - ) - put_cookies = self.client.cookies - put_headers = self.client.headers - put_headers['Content-Length'] = '0' - put_headers['x-datadome-clientid'] = self.client.cookies['datadome'] + + def _load_option(self, client, url, payload): + self.complex_cookies = client.cookies + self.complex_headers = client.headers + self.complex_headers['Content-Length'] = '0' + self.complex_headers['x-datadome-clientid'] = client.cookies['datadome'] if payload: my_payload = json.dumps(payload) my_payload = my_payload.replace(': "', ':"') my_payload = my_payload.replace(', ', ',') - put_headers['Content-Length'] = f'{len(my_payload)}' + self.complex_headers['Content-Length'] = f'{len(my_payload)}' with requests.options( url=url, timeout=20, - cookies=put_cookies, - headers=put_headers + cookies=self.complex_cookies, + headers=self.complex_headers ) as req: if req.status_code not in [200, 201]: print(f'Something Went Wrong. Can\'t Get Options.' @@ -170,22 +162,46 @@ def _load_href( else: print(f'option : {req.status_code} : {req.text}') req.raise_for_status() - req = requests.put( - url=url, - params=params, - json=payload, - timeout=20, - cookies=put_cookies, - headers=put_headers - ) + + @staticmethod + def _update_datadome(req: requests.Response, client): if 'x-set-cookie' in req.headers.keys(): x_set_cookie = req.headers['x-set-cookie'] x_set_cookie = x_set_cookie.split(';') for item in x_set_cookie: if 'datadome' in item: x_set_datadome_cookie = item.split('=')[1] - self.client.cookies['datadome'] = x_set_datadome_cookie + client.cookies['datadome'] = x_set_datadome_cookie break + + +@dataclass +class PutReq(BaseReq, ComplexReq): + """ + Core Class To Send PUT Request + To Soundcloud + """ + + def _load_href( + self, + url: str, + param: dict, + payload: dict + ) -> Dict: + params = urllib.parse.urlencode( + param, + quote_via=urllib.parse.quote + ) + self._load_option(client=self.client, url=url, payload=payload) + req = requests.put( + url=url, + params=params, + json=payload, + timeout=20, + cookies=self.complex_cookies, + headers=self.complex_headers + ) + self._update_datadome(req=req, client=self.client) if req.status_code not in [200, 201]: print(f'Something Went Wrong. Error {req.status_code}') return {'status': 'err'} @@ -196,18 +212,19 @@ def _load_href( def __call__(self, **kwargs): self._call_params(**kwargs) data = self._load_href(self.resource_url, self.params, kwargs) - if data['status'] == 'ok': print('User Information Updated.') else: print('User Information Not Updated.') + @dataclass -class DeleteReq(BaseReq): +class DeleteReq(BaseReq, ComplexReq): """ - Core Class To Send PUT Request + Core Class To Send Delete Request To Soundcloud """ + def _load_href( self, url: str, @@ -218,44 +235,16 @@ def _load_href( param, quote_via=urllib.parse.quote ) - delete_cookies = self.client.cookies - delete_headers = self.client.headers - delete_headers['Content-Length'] = '0' - delete_headers['x-datadome-clientid'] = self.client.cookies['datadome'] - if payload: - my_payload = json.dumps(payload) - my_payload = my_payload.replace(': "', ':"') - my_payload = my_payload.replace(', ', ',') - delete_headers['Content-Length'] = f'{len(my_payload)}' - with requests.options( - url=url, - timeout=20, - cookies=delete_cookies, - headers=delete_headers - ) as req: - if req.status_code not in [200, 201]: - print(f'Something Went Wrong. Can\'t Get Options.' - f'Error {req.status_code}') - return {'status': 'err'} - else: - print(f'option : {req.status_code} : {req.text}') - req.raise_for_status() + self._load_option(client=self.client, url=url, payload=payload) req = requests.delete( - url=url, - params=params, - json=payload, - timeout=20, - cookies=delete_cookies, - headers=delete_headers + url=url, + params=params, + json=payload, + timeout=20, + cookies=self.complex_cookies, + headers=self.complex_headers ) - if 'x-set-cookie' in req.headers.keys(): - x_set_cookie = req.headers['x-set-cookie'] - x_set_cookie = x_set_cookie.split(';') - for item in x_set_cookie: - if 'datadome' in item: - x_set_datadome_cookie = item.split('=')[1] - self.client.cookies['datadome'] = x_set_datadome_cookie - break + self._update_datadome(req=req, client=self.client) if req.status_code not in [200, 201]: print(f'Something Went Wrong. Error {req.status_code}') return {'status': 'err'} From c3b98bb24472fa6eaa27753b6cfbd696d069585e Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Wed, 9 Oct 2024 04:52:08 +0330 Subject: [PATCH 12/20] Add: Decorator To Update Cookies After Some Func --- soundcld/__init__.py | 6 +----- soundcld/api_handler.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index 99fc546..dc592a8 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -666,12 +666,11 @@ def change_my_profile_info( payload[item] = last_info[item] return self._put_payload(link, **payload) - def like_track(self, track_id:int): + def like_track(self, track_id: int): """ Likes The Track by Me {Logged-In User}. """ link = f'/users/{self.my_account_id}/track_likes/{track_id}' - self._update_cookies() return self._put_payload(link) def like_playlist(self, playlist_id: int): @@ -679,7 +678,6 @@ def like_playlist(self, playlist_id: int): Likes The Playlist or Album by Me {Logged-In User}. """ link = f'/users/{self.my_account_id}/playlist_likes/{playlist_id}' - self._update_cookies() return self._put_payload(link) def dislike_track(self, track_id: int): @@ -687,7 +685,6 @@ def dislike_track(self, track_id: int): Dislikes The Track by Me {Logged-In User}. """ link = f'/users/{self.my_account_id}/track_likes/{track_id}' - self._update_cookies() return self._delete_payload(link) def dislike_playlist(self, playlist_id: int): @@ -695,5 +692,4 @@ def dislike_playlist(self, playlist_id: int): Dislikes The Playlist or Album by Me {Logged-In User}. """ link = f'/users/{self.my_account_id}/playlist_likes/{playlist_id}' - self._update_cookies() return self._delete_payload(link) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index 51bbc18..25b8bd1 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -6,6 +6,7 @@ import re from dataclasses import dataclass from datetime import datetime +from functools import wraps from typing import List, Union, Iterator import requests @@ -35,6 +36,16 @@ headerDirectory = scriptDirectory + '/headers.json' +def update_cookies_after(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + self._update_cookies() + return result + + return wrapper + + @dataclass class BaseSound: """ @@ -189,11 +200,13 @@ def _get_conversation_messages(self, req: str, **param) -> Union[Iterator[Messag def _get_web_profile_list(self, req: str) -> List[WebProfile]: return ListGetReq[WebProfile](self, req, WebProfile)() + @update_cookies_after def _put_payload(self, req: str, **payload: dict) -> bool: if self.is_logged_in(): return PutReq(self, req)(**payload) return False + @update_cookies_after def _delete_payload(self, req: str, **payload: dict) -> bool: if self.is_logged_in(): return DeleteReq(self, req)(**payload) From ed58f306b0251ef93c0b58ffadcd30a18679ddb2 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Wed, 9 Oct 2024 04:56:41 +0330 Subject: [PATCH 13/20] Add: Post Request Handler --- soundcld/request_handler.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/soundcld/request_handler.py b/soundcld/request_handler.py index 9ad1989..fdd7800 100644 --- a/soundcld/request_handler.py +++ b/soundcld/request_handler.py @@ -259,3 +259,47 @@ def __call__(self, **kwargs): print('User Information Updated.') else: print('User Information Not Updated.') + + +@dataclass +class PostReq(BaseReq, ComplexReq): + """ + Core Class To Send Post Request + To Soundcloud + """ + + def _load_href( + self, + url: str, + param: dict, + payload: dict + ) -> Dict: + params = urllib.parse.urlencode( + param, + quote_via=urllib.parse.quote + ) + self._load_option(client=self.client, url=url, payload=payload) + req = requests.post( + url=url, + params=params, + json=payload, + timeout=20, + cookies=self.complex_cookies, + headers=self.complex_headers + ) + self._update_datadome(req=req, client=self.client) + if req.status_code not in [200, 201]: + print(f'Something Went Wrong. Error {req.status_code}') + return {'status': 'err'} + print(f'posting : {req.status_code} : {req.text}') + req.raise_for_status() + return {'status': 'ok'} + + def __call__(self, **kwargs): + self._call_params(**kwargs) + data = self._load_href(self.resource_url, self.params, kwargs) + + if data['status'] == 'ok': + print('User Information Updated.') + else: + print('User Information Not Updated.') From c2b573de287bd6c75c7bf4504a36bc4e9279edb9 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Wed, 9 Oct 2024 04:57:03 +0330 Subject: [PATCH 14/20] Add: Post Function Added --- soundcld/api_handler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/soundcld/api_handler.py b/soundcld/api_handler.py index 25b8bd1..8c3c26a 100644 --- a/soundcld/api_handler.py +++ b/soundcld/api_handler.py @@ -17,7 +17,8 @@ ListGetReq, CollectionGetReq, PutReq, - DeleteReq + DeleteReq, + PostReq ) from soundcld.resource import ( SearchItem, Like, RepostItem, StreamItem, @@ -200,6 +201,12 @@ def _get_conversation_messages(self, req: str, **param) -> Union[Iterator[Messag def _get_web_profile_list(self, req: str) -> List[WebProfile]: return ListGetReq[WebProfile](self, req, WebProfile)() + @update_cookies_after + def _post_payload(self, req: str, **payload: dict) -> bool: + if self.is_logged_in(): + return PostReq(self, req)(**payload) + return False + @update_cookies_after def _put_payload(self, req: str, **payload: dict) -> bool: if self.is_logged_in(): From 1863529e27ed0eec9038f1bda0c5886c06bedd12 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 11 Oct 2024 19:13:23 +0330 Subject: [PATCH 15/20] Edit: Request Handler Response Code Checker --- soundcld/request_handler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/soundcld/request_handler.py b/soundcld/request_handler.py index fdd7800..7a1ee18 100644 --- a/soundcld/request_handler.py +++ b/soundcld/request_handler.py @@ -155,7 +155,7 @@ def _load_option(self, client, url, payload): cookies=self.complex_cookies, headers=self.complex_headers ) as req: - if req.status_code not in [200, 201]: + if not f'{req.status_code}'.startswith('2'): print(f'Something Went Wrong. Can\'t Get Options.' f'Error {req.status_code}') return {'status': 'err'} @@ -202,7 +202,7 @@ def _load_href( headers=self.complex_headers ) self._update_datadome(req=req, client=self.client) - if req.status_code not in [200, 201]: + if not f'{req.status_code}'.startswith('2'): print(f'Something Went Wrong. Error {req.status_code}') return {'status': 'err'} print(f'putting : {req.status_code} : {req.text}') @@ -245,7 +245,7 @@ def _load_href( headers=self.complex_headers ) self._update_datadome(req=req, client=self.client) - if req.status_code not in [200, 201]: + if not f'{req.status_code}'.startswith('2'): print(f'Something Went Wrong. Error {req.status_code}') return {'status': 'err'} print(f'deleting : {req.status_code} : {req.text}') @@ -288,7 +288,7 @@ def _load_href( headers=self.complex_headers ) self._update_datadome(req=req, client=self.client) - if req.status_code not in [200, 201]: + if not f'{req.status_code}'.startswith('2'): print(f'Something Went Wrong. Error {req.status_code}') return {'status': 'err'} print(f'posting : {req.status_code} : {req.text}') From 0ddcf39e06707aa776beeff4d2c8e45df7abb12d Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 11 Oct 2024 19:17:20 +0330 Subject: [PATCH 16/20] Add: Playlist Create & Delete Funcs --- soundcld/__init__.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index dc592a8..9d4480d 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -1,7 +1,7 @@ """ SoundCld Is Soundcloud-v2 api handler """ -from typing import List +from typing import List, Union import soundcld.resource from .api_handler import BaseSound @@ -693,3 +693,34 @@ def dislike_playlist(self, playlist_id: int): """ link = f'/users/{self.my_account_id}/playlist_likes/{playlist_id}' return self._delete_payload(link) + + def create_playlist( + self, + playlist_name: str, + trak_id: Union[int, List[int]], + is_public: bool = True + ): + """ + Creates The Playlist by Me {Logged-In User}. + """ + link = f'/playlists' + payload = { + 'playlist': { + 'title': playlist_name, + 'tracks': [trak_id], + '_resource_id': 'f-', + '_resource_type': 'playlist' + } + } + if is_public: + payload['sharing'] = 'public' + else: + payload['sharing'] = 'private' + return self._post_payload(link, **payload) + + def delete_playlist(self, playlist_id: int): + """ + Removes The Playlist by Me {Logged-In User}. + """ + link = f'/playlists/{playlist_id}' + return self._delete_payload(link) From a2b58080366c950c4f629c4a714f51051a80a34f Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 11 Oct 2024 19:22:55 +0330 Subject: [PATCH 17/20] Edit: Now All Objects Are Iterable --- soundcld/resource/base.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/soundcld/resource/base.py b/soundcld/resource/base.py index 138ae80..5a6c916 100644 --- a/soundcld/resource/base.py +++ b/soundcld/resource/base.py @@ -1,7 +1,7 @@ """ Base Object For SoundCloud """ -from dataclasses import dataclass +from dataclasses import dataclass, fields, is_dataclass from datetime import datetime from typing import Optional @@ -27,11 +27,48 @@ def from_dict(cls, d: dict): return from_dict(cls, d, cls.dacite_config) def __getitem__(self, item): + """ + Return Value By Key + """ try: return getattr(self, item) except AttributeError: raise KeyError(f"Key '{item}' not found.") + def __setitem__(self, item, value): + """ + Set Value To Key + """ + setattr(self, item, value) + + def items(self): + """ + Return a generator of (field_name, value) tuples, + converting dataclasses to dicts and datetime to ISO format. + """ + return ((field.name, self._convert_to_dict(getattr(self, field.name))) for field in fields(self)) + + def _convert_to_dict(self, value): + """ + Helper method to convert nested + dataclass objects to dictionaries recursively. + Also handles datetime serialization (to ISO format). + """ + if isinstance(value, datetime): + return value.isoformat() + elif is_dataclass(value): + return {f.name: self._convert_to_dict(getattr(value, f.name)) for f in fields(value)} + elif isinstance(value, (list, tuple)): + return [self._convert_to_dict(v) for v in value] + else: + return value + + def __iter__(self): + """ + Return an iterator over the field names. + """ + return (field.name for field in fields(self)) + @dataclass class BaseItem(BaseData): From 901fc53dbc70d9fab123dc34e19f1bbdddc13643 Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 11 Oct 2024 19:30:31 +0330 Subject: [PATCH 18/20] Add: Adding & Removing Tracks In Playlists --- soundcld/__init__.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index 9d4480d..7f0f35d 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -724,3 +724,52 @@ def delete_playlist(self, playlist_id: int): """ link = f'/playlists/{playlist_id}' return self._delete_payload(link) + + def add_track_to_playlist( + self, + playlist_id: int, + track_id: Union[int, List[int]] + ): + """ + Adds Track Or List Of Tracks To The Playlist by Me {Logged-In User}. + """ + link = f'/playlists/{playlist_id}' + temp_playlist = self.get_playlist(playlist_id) + temp_tracks = [] + for item in temp_playlist.tracks: + temp_tracks.append(item.id) + if isinstance(track_id, int): + temp_tracks.append(track_id) + else: + temp_tracks.extend(track_id) + payload = { + 'playlist': { + 'tracks': temp_tracks + } + } + return self._put_payload(link, **payload) + + def remove_track_from_playlist( + self, + playlist_id: int, + track_id: Union[int, List[int]] + ): + """ + Removes Track Or List Of Tracks To The Playlist by Me {Logged-In User}. + """ + link = f'/playlists/{playlist_id}' + temp_playlist = self.get_playlist(playlist_id) + temp_tracks = [] + for item in temp_playlist.tracks: + temp_tracks.append(item.id) + if isinstance(track_id, int): + temp_tracks.remove(track_id) + else: + for item in track_id: + temp_tracks.remove(item) + payload = { + 'playlist': { + 'tracks': temp_tracks + } + } + return self._put_payload(link, **payload) From 313cc6e272cd580f97fbd05db9616fe5bbb1c7ea Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 11 Oct 2024 22:02:59 +0330 Subject: [PATCH 19/20] Add: Edit Playlist Info Func --- soundcld/__init__.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/soundcld/__init__.py b/soundcld/__init__.py index 7f0f35d..a4c8393 100644 --- a/soundcld/__init__.py +++ b/soundcld/__init__.py @@ -1,6 +1,7 @@ """ SoundCld Is Soundcloud-v2 api handler """ +from datetime import datetime from typing import List, Union import soundcld.resource @@ -773,3 +774,65 @@ def remove_track_from_playlist( } } return self._put_payload(link, **payload) + + def edit_playlist_info( + self, + playlist_id: int, + title: str = None, + description: str = None, + playlist_type: str = None, + release_date: str = None, + genre: str = None, + tag: str = None, + permalink: str = None + ): + """ + Changes The Playlist Info by Me {Logged-In User}. + + :param playlist_id: The ID Of Playlist + :param title: Title Of Playlist + :param description: Description Of Playlist + :param playlist_type: Must Be One Of These [playlist, album, compilation, single, ep] + :param release_date: Must Be In "year-month-day" Format + :param genre: Genre Of Playlist + :param tag: Tag or Tags Of Playlist + :param permalink: The Link Name Of Playlist + """ + link = f'/playlists/{playlist_id}' + playlist_temp_data = self.get_playlist(playlist_id) + payload = { + 'title': title, + 'description': description, + 'kind': playlist_type, + 'release_date': release_date, + 'genre': genre, + 'tag_list': tag, + 'permalink': permalink + } + temp_dict = {} + for item, value in playlist_temp_data.items(): + temp_dict[item] = value + for item, value in payload.items(): + if value: + temp_dict[item] = value + if item == 'permalink': + temp_link = playlist_temp_data['permalink_url'].split('/') + temp_link[-1] = temp_dict[item] + temp_dict['permalink_url'] = '/'.join(temp_link) + elif item == 'kind': + if not (temp_dict['release_date'] or payload['release_date']): + print('release_date not added') + return + if value != 'playlist': + temp_dict['set_type'] = value + else: + temp_dict['set_type'] = None + some_arr = [] + for item in playlist_temp_data.tracks: + some_arr.append(item.id) + now = datetime.utcnow() + temp_dict['last_modified'] = f'{now.isoformat().split(".")[0]}Z' + temp_dict['tracks'] = some_arr + temp_dict['_resource_id'] = playlist_id + temp_dict['_resource_type'] = playlist_temp_data.kind + return self._put_payload(link, **temp_dict) From dfeb2b8a06df1e769410dda0077e012ee08858bc Mon Sep 17 00:00:00 2001 From: FaridRasidov Date: Fri, 11 Oct 2024 22:04:27 +0330 Subject: [PATCH 20/20] Edit: README Update --- README.md | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 14ef244..3338e2b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@

SoundCld

- Python Api Handler For The Internal V2 SoundCloud API. Does Not Require An API Key. + Python API handler for the SoundCloud Internal V2 API, + allowing interaction without an API key.

@@ -13,21 +14,31 @@ forks - soundcld
-**** -**Installation:** +## Table of Contents +- [Installation](#installation) +- [Usage](#usage) +- [Specifications](#specifications) +- [Authentication](#authentication) +- [License](#license) + + + +## Installation: +**Global Installation** ```shell -# For Global $ git clone https://github.com/faridrasidov/soundcld $ cd soundcld $ pip install . ``` +**Virtual Environment** ```shell -# For Venv $ git clone https://github.com/faridrasidov/soundcld $ cd soundcld $ path/to/your/venv/pip install . ``` -**Example Of Usage:** + + +## Usage ```python from soundcld import SoundCloud @@ -38,17 +49,24 @@ for item in search: print(item.permalink, item.kind) ``` -**Specifications:** + +## Specifications +- **You Can Change Your Profile Info** +- **45 Get Api Requests Has Been Handled.(Some Of Them Require Auth)** +- **7 Put Api Requests hs Been Handled. (All Of Them Require Auth)** +- **1 Post Api Requests hs Been Handled. (All Of Them Require Auth)** +- **2 Delete Api Requests hs Been Handled. (All Of Them Require Auth)** - **Last Valid Generated ID's Automatically Added To 'data.json' File To improve Api Speed.** -- **46 Get Api Requests Has Been Handled.(Some Of Them Require Auth)** -- **You Can Change Your Profile Info Too** -**** + + + +## Authentication **Notes about `auth`:** **Some methods require authentication. If you want to use them, you should get the values -written at the bottom from your cookies and put them in a package folder ("soundcloud") -named cookies.json. You will also need to change your "client_id" in data.json in that folder.** +written at the bottom from your cookies and put them in file which is in package folder ("soundcloud") +named cookies.json. You will also need to change your "client_id" in data.json file in that folder.** **Save Them Into:** @@ -71,4 +89,8 @@ named cookies.json. You will also need to change your "client_id" in data.json i "client_id": "", "app_version": "" } -``` \ No newline at end of file +``` + + +## License +`Soundcld` source code is licensed under the terms of the Boost Software License. See [LICENSE](https://github.com/faridrasidov/soundcld/blob/master/LICENCE.txt) for more information. \ No newline at end of file