From b863d3a49729bed7e0e4208c131b3cba9cae5db9 Mon Sep 17 00:00:00 2001 From: benhao Date: Sun, 8 Jun 2025 19:55:28 +0800 Subject: [PATCH 1/4] feat: init favorite add favorite queries --- python/constants/__init__.py | 1 + python/constants/favorite_query.py | 44 ++++++++++++++++++++++++++++ python/lc_libs/__init__.py | 1 + python/lc_libs/favorite.py | 47 ++++++++++++++++++++++++++++++ python/scripts/leetcode.py | 4 ++- 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 python/constants/favorite_query.py create mode 100644 python/lc_libs/favorite.py diff --git a/python/constants/__init__.py b/python/constants/__init__.py index b14b8a3ac..61ee19475 100644 --- a/python/constants/__init__.py +++ b/python/constants/__init__.py @@ -14,3 +14,4 @@ CARGO_TOML_TEMPLATE_SOLUTION, CONTEST_TEMPLATE_PYTHON) from .display import (SUBMIT_BASIC_RESULT, SUBMIT_SUCCESS_RESULT, SUBMIT_FAIL_RESULT) from .contest_query import CONTEST_HISTORY_QUERY +from .favorite_query import ADD_QUESTION_TO_FAVORITE_QUERY, MY_FAVORITE_QUERY diff --git a/python/constants/favorite_query.py b/python/constants/favorite_query.py new file mode 100644 index 000000000..4bff4ac08 --- /dev/null +++ b/python/constants/favorite_query.py @@ -0,0 +1,44 @@ +ADD_QUESTION_TO_FAVORITE_QUERY = """ + mutation batchAddQuestionsToFavorite($favoriteSlug: String!, $questionSlugs: [String]!) { + batchAddQuestionsToFavorite( + favoriteSlug: $favoriteSlug + questionSlugs: $questionSlugs + ) { + ok + error + } +}""" + +MY_FAVORITE_QUERY = """ + query myFavoriteList { + myCreatedFavoriteList { + favorites { + coverUrl + coverEmoji + coverBackgroundColor + hasCurrentQuestion + isPublicFavorite + lastQuestionAddedAt + name + slug + favoriteType + } + hasMore + totalLength + } + myCollectedFavoriteList { + hasMore + totalLength + favorites { + coverUrl + coverEmoji + coverBackgroundColor + hasCurrentQuestion + isPublicFavorite + name + slug + lastQuestionAddedAt + favoriteType + } + } +}""" diff --git a/python/lc_libs/__init__.py b/python/lc_libs/__init__.py index 818d59e9b..ad3f3280c 100644 --- a/python/lc_libs/__init__.py +++ b/python/lc_libs/__init__.py @@ -15,3 +15,4 @@ from .study_plan import get_user_study_plans, get_user_study_plan_progress from .rating import get_rating from .answers import get_answer_san_ye +from .favorite import query_my_favorites, batch_add_questions_to_favorite diff --git a/python/lc_libs/favorite.py b/python/lc_libs/favorite.py new file mode 100644 index 000000000..4495a8b9a --- /dev/null +++ b/python/lc_libs/favorite.py @@ -0,0 +1,47 @@ +import logging +from typing import Optional + +import requests + +from python.constants import LEET_CODE_BACKEND, ADD_QUESTION_TO_FAVORITE_QUERY, MY_FAVORITE_QUERY +from python.utils import general_request + + +def batch_add_questions_to_favorite(favorite_slug: str, questions: list, cookie: str) -> Optional[dict]: + def handle_response(response: requests.Response): + resp = response.json() + if resp.get("data", {}).get("batchAddQuestionsToFavorite", {}).get("ok"): + return {"status": "success"} + else: + error = resp.get("data", {}).get("batchAddQuestionsToFavorite", {}).get("error", "Unknown error") + logging.error(f"Failed to add questions to favorite: {error}") + return {"status": "error", "message": error} + + return general_request( + LEET_CODE_BACKEND, + handle_response, + json={"query": ADD_QUESTION_TO_FAVORITE_QUERY, + "variables": {"favoriteSlug": favorite_slug, "questionSlugs": questions}, + "operationName": "batchAddQuestionsToFavorite"}, + cookies={"cookie": cookie} + ) + + +def query_my_favorites(cookie: str) -> Optional[dict]: + def handle_response(response: requests.Response) -> Optional[list]: + resp = response.json() + my_created_favorites = resp.get("data", {}).get("myCreatedFavoriteList", {}).get("favorites", []) + return [ + { + "name": favorite.get("name"), + "slug": favorite.get("slug"), + } + for favorite in my_created_favorites + ] + + return general_request( + LEET_CODE_BACKEND, + handle_response, + json={"query": MY_FAVORITE_QUERY, "operationName": "myFavoriteList", "variables": {}}, + cookies={"cookie": cookie} + ) diff --git a/python/scripts/leetcode.py b/python/scripts/leetcode.py index 42ffec642..f99100638 100644 --- a/python/scripts/leetcode.py +++ b/python/scripts/leetcode.py @@ -17,7 +17,8 @@ sys.path.insert(0, root_path.as_posix()) from python.constants import constant -from python.lc_libs import get_daily_question, contest as contest_lib +from python.lc_libs import get_daily_question, query_my_favorites, batch_add_questions_to_favorite, \ + contest as contest_lib import python.lc_libs as lc_libs from python.scripts.submit import main as submit_main_async from python.utils import back_question_id, format_question_id, check_cookie_expired @@ -39,6 +40,7 @@ 4. Contest 5. Clean empty java 6. Clean error rust +7. Favorite management """ __user_input_get_problem = """Please select the get problem method [0-5, default: 0]: 0. Back From 4bfeda8c642a7833dbd19c8c3d26015692257b5d Mon Sep 17 00:00:00 2001 From: benhao Date: Sun, 8 Jun 2025 20:53:14 +0800 Subject: [PATCH 2/4] feat: add query_favorite_questions and update favorite handling favorite methods --- python/constants/__init__.py | 2 +- python/constants/favorite_query.py | 36 ++++++++ python/lc_libs/__init__.py | 2 +- python/lc_libs/favorite.py | 106 +++++++++++++++++++++--- python/scripts/get_problem.py | 35 ++++---- python/scripts/leetcode.py | 127 ++++++++++++++++++++++++++++- 6 files changed, 280 insertions(+), 28 deletions(-) diff --git a/python/constants/__init__.py b/python/constants/__init__.py index 61ee19475..75ea748c7 100644 --- a/python/constants/__init__.py +++ b/python/constants/__init__.py @@ -14,4 +14,4 @@ CARGO_TOML_TEMPLATE_SOLUTION, CONTEST_TEMPLATE_PYTHON) from .display import (SUBMIT_BASIC_RESULT, SUBMIT_SUCCESS_RESULT, SUBMIT_FAIL_RESULT) from .contest_query import CONTEST_HISTORY_QUERY -from .favorite_query import ADD_QUESTION_TO_FAVORITE_QUERY, MY_FAVORITE_QUERY +from .favorite_query import ADD_QUESTION_TO_FAVORITE_QUERY, MY_FAVORITE_QUERY, FAVORITE_QUESTION_QUERY diff --git a/python/constants/favorite_query.py b/python/constants/favorite_query.py index 4bff4ac08..269473b56 100644 --- a/python/constants/favorite_query.py +++ b/python/constants/favorite_query.py @@ -42,3 +42,39 @@ } } }""" + +FAVORITE_QUESTION_QUERY = """ + query favoriteQuestionList($favoriteSlug: String!, $filter: FavoriteQuestionFilterInput, $searchKeyword: String, $filtersV2: QuestionFilterInput, $sortBy: QuestionSortByInput, $limit: Int, $skip: Int, $version: String = "v2") { + favoriteQuestionList( + favoriteSlug: $favoriteSlug + filter: $filter + filtersV2: $filtersV2 + searchKeyword: $searchKeyword + sortBy: $sortBy + limit: $limit + skip: $skip + version: $version + ) { + questions { + difficulty + id + paidOnly + questionFrontendId + status + title + titleSlug + translatedTitle + isInMyFavorites + frequency + acRate + topicTags { + name + nameTranslated + slug + } + } + totalLength + hasMore + } +} +""" \ No newline at end of file diff --git a/python/lc_libs/__init__.py b/python/lc_libs/__init__.py index ad3f3280c..31d405fc4 100644 --- a/python/lc_libs/__init__.py +++ b/python/lc_libs/__init__.py @@ -15,4 +15,4 @@ from .study_plan import get_user_study_plans, get_user_study_plan_progress from .rating import get_rating from .answers import get_answer_san_ye -from .favorite import query_my_favorites, batch_add_questions_to_favorite +from .favorite import query_my_favorites, batch_add_questions_to_favorite, query_favorite_questions diff --git a/python/lc_libs/favorite.py b/python/lc_libs/favorite.py index 4495a8b9a..7f05aa7ee 100644 --- a/python/lc_libs/favorite.py +++ b/python/lc_libs/favorite.py @@ -3,7 +3,8 @@ import requests -from python.constants import LEET_CODE_BACKEND, ADD_QUESTION_TO_FAVORITE_QUERY, MY_FAVORITE_QUERY +from python.constants import LEET_CODE_BACKEND, ADD_QUESTION_TO_FAVORITE_QUERY, MY_FAVORITE_QUERY, \ + FAVORITE_QUESTION_QUERY from python.utils import general_request @@ -28,16 +29,23 @@ def handle_response(response: requests.Response): def query_my_favorites(cookie: str) -> Optional[dict]: - def handle_response(response: requests.Response) -> Optional[list]: + def handle_response(response: requests.Response) -> Optional[dict]: resp = response.json() - my_created_favorites = resp.get("data", {}).get("myCreatedFavoriteList", {}).get("favorites", []) - return [ - { - "name": favorite.get("name"), - "slug": favorite.get("slug"), - } - for favorite in my_created_favorites - ] + + my_created_favorites = resp.get("data", {}).get("myCreatedFavoriteList", {}) + total_length = my_created_favorites.get("totalLength", 0) + favorites = my_created_favorites.get("favorites", []) + return { + "total": total_length, + "favorites": [ + { + "name": favorite.get("name"), + "slug": favorite.get("slug"), + } + for favorite in favorites + ], + "has_more": my_created_favorites.get("hasMore", False) + } return general_request( LEET_CODE_BACKEND, @@ -45,3 +53,81 @@ def handle_response(response: requests.Response) -> Optional[list]: json={"query": MY_FAVORITE_QUERY, "operationName": "myFavoriteList", "variables": {}}, cookies={"cookie": cookie} ) + + +def query_favorite_questions(favorite_slug: str, cookie: str, limit: int = 100, skip: int = 0) -> Optional[dict]: + def handle_response(response: requests.Response) -> Optional[dict]: + data = response.json().get("data", {}).get("favoriteQuestionList", {}) + total = data.get("totalLength", 0) + questions = data.get("questions", []) + return { + "total": total, + "questions": [ + { + "title": question.get("title"), + "title_slug": question.get("titleSlug"), + "translated_title": question.get("translatedTitle"), + "difficulty": question.get("difficulty"), + "id": question.get("id"), + "question_frontend_id": question.get("questionFrontendId"), + } + for question in questions + ], + "has_more": data.get("hasMore", False) + } + + return general_request( + LEET_CODE_BACKEND, + handle_response, + json={ + "query": FAVORITE_QUESTION_QUERY, + "variables": { + "skip": skip, + "limit": limit, + "favoriteSlug": favorite_slug, + "filtersV2": { + "filterCombineType": "ALL", + "statusFilter": { + "questionStatuses": [], + "operator": "IS" + }, + "difficultyFilter": { + "difficulties": [], + "operator": "IS" + }, + "languageFilter": { + "languageSlugs": [], + "operator": "IS" + }, + "topicFilter": { + "topicSlugs": [], + "operator": "IS" + }, + "acceptanceFilter": {}, + "frequencyFilter": {}, + "frontendIdFilter": {}, + "lastSubmittedFilter": {}, + "publishedFilter": {}, + "companyFilter": { + "companySlugs": [], + "operator": "IS" + }, + "positionFilter": { + "positionSlugs": [], + "operator": "IS" + }, + "premiumFilter": { + "premiumStatus": [], + "operator": "IS" + } + }, + "searchKeyword": "", + "sortBy": { + "sortField": "CUSTOM", + "sortOrder": "ASCENDING" + } + }, + "operationName": "favoriteQuestionList" + }, + cookies={"cookie": cookie} + ) diff --git a/python/scripts/get_problem.py b/python/scripts/get_problem.py index 82e753392..7d0f42c05 100644 --- a/python/scripts/get_problem.py +++ b/python/scripts/get_problem.py @@ -144,6 +144,25 @@ def process_single_database_problem(problem_folder: str, problem_id: str, proble logging.info(f"Add question: [{problem_id}]{problem_slug}") +def get_question_slug_by_id( + problem_id: str, + problem_category: Optional[str] = None, + cookie: Optional[str] = None) -> Optional[str]: + questions = get_questions_by_key_word(problem_id, problem_category) if problem_category \ + else get_questions_by_key_word(problem_id) + if not questions: + logging.error(f"Unable to find any questions with problem_id {problem_id}") + return None + for question in questions: + if question["paidOnly"] and not cookie: + continue + if question["frontendQuestionId"] == problem_id: + return question["titleSlug"] + logging.error(f"Unable to find any questions with problem_id {problem_id}" + f", possible questions: {questions}") + return None + + def main(origin_problem_id: Optional[str] = None, problem_slug: Optional[str] = None, problem_category: Optional[str] = None, force: bool = False, cookie: Optional[str] = None, fetch_all: bool = False, premium_only: bool = False, replace_problem_id: bool = False, @@ -153,20 +172,8 @@ def main(origin_problem_id: Optional[str] = None, problem_slug: Optional[str] = logging.critical("Requires at least one of problem_id or problem_slug to fetch in single mode.") return 1 if not problem_slug: - questions = get_questions_by_key_word(origin_problem_id, problem_category) if problem_category \ - else get_questions_by_key_word(origin_problem_id) - if not questions: - logging.error(f"Unable to find any questions with problem_id {origin_problem_id}") - return 1 - for question in questions: - if question["paidOnly"] and not cookie: - continue - if question["frontendQuestionId"] == origin_problem_id: - problem_slug = question["titleSlug"] - break - if not problem_slug: - logging.error(f"Unable to find any questions with problem_id {origin_problem_id}" - f", possible questions: {questions}") + question_slug = get_question_slug_by_id(origin_problem_id, problem_category, cookie) + if not question_slug: return 1 question_info = get_question_info(problem_slug, cookie) if not question_info: diff --git a/python/scripts/leetcode.py b/python/scripts/leetcode.py index f99100638..46a723d5f 100644 --- a/python/scripts/leetcode.py +++ b/python/scripts/leetcode.py @@ -18,12 +18,12 @@ from python.constants import constant from python.lc_libs import get_daily_question, query_my_favorites, batch_add_questions_to_favorite, \ - contest as contest_lib + query_favorite_questions, contest as contest_lib import python.lc_libs as lc_libs from python.scripts.submit import main as submit_main_async from python.utils import back_question_id, format_question_id, check_cookie_expired from python.scripts.daily_auto import main as daily_auto_main -from python.scripts.get_problem import main as get_problem_main +from python.scripts.get_problem import main as get_problem_main, get_question_slug_by_id from python.scripts.tools import lucky_main, remain_main, clean_empty_java_main, clean_error_rust_main __separate_line = "-" * 50 @@ -71,6 +71,11 @@ b. last page n. next page """ +__user_input_favorite_method = """Please select the favorite method [0-2, default: 0]: +0. Back +1. List problems in the favorite +2. Add problems to the favorite +""" __supported_languages = ["python3", "java", "golang", "cpp", "typescript", "rust"] __user_input_language = f"""Select multiple languages you want to use, separated by comma [0-{len(__supported_languages) - 1}, default: 0]: @@ -441,6 +446,121 @@ def contest_list(): return None +def favorite_main(languages, problem_folder, cookie): + def favorite_list(): + while True: + my_favorites = query_my_favorites(cookie) + total, data, has_more = my_favorites["total"], my_favorites["favorites"], my_favorites["has_more"] + if not data: + print("No favorites found.") + break + content = "\n".join( + [f"{_i}. {f['name']}" for _i, f in enumerate(data, start=1)], + ) + user_input_select = input_until_valid( + __user_input_page.format(total, content), + __allow_all + ) + pick = None + match user_input_select: + case v if v.isdigit() and 1 <= int(v) <= 10: + pick = int(v) + case _: + break + print(__separate_line) + if not pick: + continue + return data[pick - 1] + return None + + def question_list(favorite_slug): + cur_page = 1 + while True: + questions = query_favorite_questions(favorite_slug, cookie, limit=20, skip=(cur_page-1)*20) + total, data, has_more = questions["total"], questions["questions"], questions["has_more"] + max_page = math.ceil(total / 10) + if not data: + print("No questions found in this favorite.") + break + content = "\n".join( + [f"{_i}. [{q['question_frontend_id']}] {q['translated_title']}" for _i, q in enumerate(data, start=1)], + ) + user_input_select = input_until_valid( + __user_input_page.format(total, content), + __allow_all + ) + pick = None + match user_input_select: + case "b": + cur_page = max(1, cur_page - 1) + case "n": + cur_page = min(max_page, cur_page + 1) + case v if v.isdigit() and 1 <= int(v) <= 10: + pick = int(v) + case _: + break + print(__separate_line) + if not pick: + continue + return data[pick - 1] + return None + + if check_cookie_expired(cookie): + print("Cookie expired, please update it to continue.") + return + while True: + favorite = favorite_list() + if not favorite: + return + slug = favorite["slug"] + while True: + favorite_method = input_until_valid( + __user_input_favorite_method, + __allow_all + ) + print(__separate_line) + match favorite_method: + case "1": + question = question_list(slug) + if not question: + break + code = get_problem_main(problem_slug=question["title_slug"], cookie=cookie, replace_problem_id=True, + languages=languages, problem_folder=problem_folder) + if code == 0: + print(f"Problem [{question['question_frontend_id']}]" + f" {question['translated_title']} fetched successfully.") + else: + print(f"Failed to fetch the problem [{question['question_frontend_id']}]" + f" {question['translated_title']}.") + case "2": + input_questions = input_until_valid( + "Enter the problem ids to add to favorite, separated by comma: ", + __allow_all_not_empty, + "Problem ids cannot be empty." + ) + question_ids = [q.strip() for q in input_questions.split(",")] + if not question_ids: + print("No questions to add.") + continue + questions = [] + for question_id in question_ids: + question_slug = get_question_slug_by_id(question_id) + if not question_slug: + print(f"Invalid question ID: {question_id}. Skipping.") + continue + questions.append(question_slug) + if not questions: + print("No valid questions to add.") + continue + result = batch_add_questions_to_favorite(slug, questions, cookie) + if result.get("status") == "success": + print(f"Added {len(questions)} questions to favorite [{favorite['name']}] successfully.") + else: + print(f"Failed to add questions to favorite [{favorite['name']}]: {result.get('message')}") + case _: + break + + def main(): try: languages, problem_folder, cookie, contest_folder = configure() @@ -467,6 +587,9 @@ def main(): clean_error_rust_main(root_path, problem_folder) print("Done cleaning error Rust files.") print(__separate_line) + case "7": + favorite_main(languages, problem_folder, cookie) + print(__separate_line) case _: print("Exiting...") break From 92d0b5c30a14c3c9e4073ff09ac1447c8209a0f6 Mon Sep 17 00:00:00 2001 From: benhao Date: Sun, 8 Jun 2025 21:08:45 +0800 Subject: [PATCH 3/4] feat: improve error logging and optimize question retrieval in favorite handling multithread slug query --- python/scripts/get_problem.py | 7 +++---- python/scripts/leetcode.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/python/scripts/get_problem.py b/python/scripts/get_problem.py index 7d0f42c05..a827e57a0 100644 --- a/python/scripts/get_problem.py +++ b/python/scripts/get_problem.py @@ -158,8 +158,7 @@ def get_question_slug_by_id( continue if question["frontendQuestionId"] == problem_id: return question["titleSlug"] - logging.error(f"Unable to find any questions with problem_id {problem_id}" - f", possible questions: {questions}") + logging.error(f"Unable to find any questions with problem_id {problem_id}, possible questions: {questions}") return None @@ -172,8 +171,8 @@ def main(origin_problem_id: Optional[str] = None, problem_slug: Optional[str] = logging.critical("Requires at least one of problem_id or problem_slug to fetch in single mode.") return 1 if not problem_slug: - question_slug = get_question_slug_by_id(origin_problem_id, problem_category, cookie) - if not question_slug: + problem_slug = get_question_slug_by_id(origin_problem_id, problem_category, cookie) + if not problem_slug: return 1 question_info = get_question_info(problem_slug, cookie) if not question_info: diff --git a/python/scripts/leetcode.py b/python/scripts/leetcode.py index 46a723d5f..5a35dfe70 100644 --- a/python/scripts/leetcode.py +++ b/python/scripts/leetcode.py @@ -8,6 +8,7 @@ import re import sys import time +from concurrent.futures import ThreadPoolExecutor from pathlib import Path from dotenv import load_dotenv @@ -475,9 +476,10 @@ def favorite_list(): def question_list(favorite_slug): cur_page = 1 + page_size = 20 while True: - questions = query_favorite_questions(favorite_slug, cookie, limit=20, skip=(cur_page-1)*20) - total, data, has_more = questions["total"], questions["questions"], questions["has_more"] + _questions = query_favorite_questions(favorite_slug, cookie, limit=page_size, skip=(cur_page-1)*page_size) + total, data, has_more = _questions["total"], _questions["questions"], _questions["has_more"] max_page = math.ceil(total / 10) if not data: print("No questions found in this favorite.") @@ -542,9 +544,11 @@ def question_list(favorite_slug): if not question_ids: print("No questions to add.") continue + with ThreadPoolExecutor() as executor: + slugs = list(executor.map(get_question_slug_by_id, question_ids)) + questions = [] - for question_id in question_ids: - question_slug = get_question_slug_by_id(question_id) + for question_id, question_slug in zip(question_ids, slugs): if not question_slug: print(f"Invalid question ID: {question_id}. Skipping.") continue From afab0aba297547fa96447c0c8177df52dc134ba5 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 8 Jun 2025 21:10:02 +0800 Subject: [PATCH 4/4] fix: bug Update python/scripts/leetcode.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- python/scripts/leetcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/scripts/leetcode.py b/python/scripts/leetcode.py index 5a35dfe70..ace00de84 100644 --- a/python/scripts/leetcode.py +++ b/python/scripts/leetcode.py @@ -480,7 +480,7 @@ def question_list(favorite_slug): while True: _questions = query_favorite_questions(favorite_slug, cookie, limit=page_size, skip=(cur_page-1)*page_size) total, data, has_more = _questions["total"], _questions["questions"], _questions["has_more"] - max_page = math.ceil(total / 10) + max_page = math.ceil(total / page_size) if not data: print("No questions found in this favorite.") break