commit af17d6fb227bbd7c717c0ab15b0e5ee1f68a0291 Author: simon Date: Fri Dec 24 16:33:34 2021 +0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8097a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# editor +.vscode/ + +# unrelated +notes.md + +# builds +dist/ +ryd_client.egg-info/ + +# cache +__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cb6678 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# RYD Client +Python client library for the **Return YouTube Dislike API**: + +- [https://returnyoutubedislike.com/](https://returnyoutubedislike.com/) +- [https://github.com/Anarios/return-youtube-dislike/](https://github.com/Anarios/return-youtube-dislike/) + + +## Functionality +- Get votes from a list of YouTube video IDs. +- Register your user ID by solving the challenge. +- Cast your vote for a list of YouTube video IDs. + + +## Usage +Some command example + +### Get Votes +Pass a list of YouTube video IDs and get a list of votes. + +```python +import ryd_client + +ratings = ryd_client.get_votes(["kxOuG8jMIgI", "CaaJyRvvaq8"]) + +# Returns a list of dictionaries with ratings for every video ID +[{'id': 'kxOuG8jMIgI', + 'likes': 27569, + 'dislikes': 503144, + 'rating': 1.2117898772151874, + 'viewCount': 3177346, + 'deleted': False, + 'status': 200}, + {'id': 'CaaJyRvvaq8', + 'likes': 502489, + 'dislikes': 13270, + 'rating': 4.900305046067389, + 'viewCount': 3575816, + 'deleted': False, + 'status': 200}] +``` + +### Register +To cast a vote, you need to be registered in the API with your user id. Generate a random user id, one per user, store it in your application and reuse for all future votes from this user. + +```python +import ryd_client + +user_id = ryd_client.generate_user_id() + +# Returns a random 36 char string of ascii_letters and digits +'5v3X3mxQOm3fkez8aWsGsEgjpFe0pJNPWIJi' + +``` + +Register your user_id in the api: + +```python +import ryd_client + +success = ryd_client.register(user_id) + +# Returns True on success, False on fail +True + +``` + +### Post Votes +Once your `user_id` is registered, you are allowed to vote. Vote on a list of video IDs. Pass a list of tuples where the first value is the video ID and second value is the vote either as `string` or `int`: +- like: 1 +- dislike: -1 +- neutral: 0 (aka *undo* your previous vote) + +Strings automatically get converted to the matching number, both are valid: + +```python +import ryd_client + +votes = [ + ("kxOuG8jMIgI", "dislike"), + ("CaaJyRvvaq8", 1), + ("CEp5SLT-DJg", 0), +] + +response = ryd_client.post_votes(votes, user_id=user_id) + +# Returns a list of dictionaries for every vote cast +[{'id': 'kxOuG8jMIgI', 'status': True, 'vote': -1}, + {'id': 'CaaJyRvvaq8', 'status': True, 'vote': 1}, + {'id': 'CEp5SLT-DJg', 'status': True, 'vote': 0}] + +``` + + +## Acknowledgement +If you find this API usefull, please consider donating to the [project](https://returnyoutubedislike.com/donate). diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..896cb97 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# build package + +python -m build + +## +exit 0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5a3c46 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/ryd-client/__init__.py b/ryd-client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ryd-client/ryd_client.py b/ryd-client/ryd_client.py new file mode 100644 index 0000000..040b433 --- /dev/null +++ b/ryd-client/ryd_client.py @@ -0,0 +1,218 @@ +"""post votes for YouTube video""" + +import random +import string +import base64 +import hashlib + +import requests + +API_URL = "https://returnyoutubedislikeapi.com" + + +class Login: + """handle user registation""" + + def __init__(self, user_id=False): + self.user_id = user_id + + def generate_user_id(self): + """get random 36 int user id""" + choice = string.ascii_letters + string.digits + new_user_id = str() + for _ in range(36): + letter = random.SystemRandom().choice(choice) + new_user_id = new_user_id + letter + + self.user_id = new_user_id + + return new_user_id + + def get_puzzle(self): + """get puzzle""" + user_id = self.user_id or self.generate_user_id() + base_url = f"{API_URL}/puzzle/registration" + puzzle = requests.get(f"{base_url}?userId={user_id}").json() + puzzle["user_id"] = user_id + + return puzzle + + def post_puzzle(self, solution): + """post solved puzzle to confirm registration""" + url = f"{API_URL}/puzzle/registration?userId={self.user_id}" + response = requests.post(url, json=solution) + if response.ok: + print(f"successfully registered with user id {self.user_id}") + return response.text == "true" + + return False + + +class Puzzle: + """solve your puzzle""" + + def __init__(self, puzzle): + self.puzzle = puzzle + + @staticmethod + def count_leading_zeros(to_check): + """return leading binary zeroes""" + zeros = 0 + for i in to_check: + if i == 0: + zeros = zeros + 8 + else: + zeros = zeros + f"{i:08b}".index("1") + break + + return zeros + + def solve(self): + """get puzzle solution""" + challenge = list(base64.b64decode(self.puzzle['challenge'])) + max_count = 2 ** self.puzzle["difficulty"] * 5 + # fill buffer + buffer = bytearray(20) + for i in range(4, 20): + buffer[i] = challenge[i - 4] + # keep hashing until leading zeros are matched + for i in range(max_count): + new_buffer = (i).to_bytes(4, byteorder="little") + buffer[4:20] + to_check = list(hashlib.sha512(new_buffer).digest()) + zeros = self.count_leading_zeros(to_check) + if zeros >= self.puzzle["difficulty"]: + solution = base64.b64encode(new_buffer[0:4]).decode() + return {"solution": solution} + + return False + + +class Vote: + """cast your vote""" + + def __init__(self, user_id, vote): + self.user_id = user_id + self.video_id = vote[0] + self.vote = self.validate_vote(vote[1]) + + def post(self): + """post vote to API""" + puzzle = self._initial_vote() + solution = Puzzle(puzzle).solve() + response = self._confirm_vote(solution) + if not response: + print(f"failed to cast vote for: {self.user_id}, {self.video_id}") + raise ValueError + + message = { + "id": self.video_id, + "status": response, + "vote": self.vote + } + + return message + + @staticmethod + def validate_vote(vote): + """convert vote""" + vote_map = { + "like": 1, + "dislike": -1, + "neutral": 0 + } + if isinstance(vote, str): + try: + return vote_map[vote] + except KeyError: + print(f"invalid vote: {vote}") + raise + elif isinstance(vote, int): + if vote in vote_map.values(): + return vote + raise ValueError(f"invalid vote cast: {vote}") + + return False + + def _initial_vote(self): + """send initial vote to recieve puzzle""" + data = { + "userId": self.user_id, + "videoId": self.video_id, + "value": self.vote, + } + response = requests.post(f"{API_URL}/interact/vote", json=data) + if not response.ok: + print("failed") + raise ValueError + puzzle = response.json() + + return puzzle + + def _confirm_vote(self, solution): + """send second confirmation with solved puzzle""" + data = { + "userId": self.user_id, + "videoId": self.video_id, + "solution": solution["solution"] + } + response = requests.post(f"{API_URL}/interact/confirmVote", json=data) + if response.ok: + return response.text == "true" + + return False + + +def generate_user_id(): + """short hand to generate user id""" + user_id = Login().generate_user_id() + return user_id + + +def register(user_id): + """register your user id""" + login_handler = Login(user_id) + puzzle = login_handler.get_puzzle() + solution = Puzzle(puzzle).solve() + response = login_handler.post_puzzle(solution) + if not response: + print(f"failed to register with user id {user_id}") + return False + + return True + + +def get_votes(youtube_ids): + """get votes from list of youtube_ids""" + + all_votes = [] + + for youtube_id in youtube_ids: + votes = requests.get(f"{API_URL}/votes?videoId={youtube_id}") + + if votes.ok: + parsed = votes.json() + parsed["status"] = votes.status_code + del parsed["dateCreated"] + elif votes.status_code in [400, 404]: + parsed = { + "id": youtube_id, + "status": votes.status_code, + } + elif votes.status_code == 429: + print("ratelimiting reached, cancle") + break + + all_votes.append(parsed) + + return all_votes + + +def post_votes(votes, user_id): + """post votes""" + all_votes = [] + for vote_pair in votes: + vote_handler = Vote(user_id, vote_pair) + message = vote_handler.post() + all_votes.append(message) + + return all_votes diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..99e316a --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setuptools.setup( + name="ryd-client", + version="0.0.1", + author="Simon", + author_email="simobilleter@gmail.com", + description="api client for returnyoutubedislike.com", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/bbilly1/ryd-client", + project_urls={ + "Bug Tracker": "https://github.com/bbilly1/ryd-client/issues", + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + package_dir={"ryd-client": "ryd-client"}, + packages=setuptools.find_packages(where="ryd_client"), + python_requires=">=3.6", +)