11#!/usr/bin/env python3
22import sys
33import json
4- import requests
4+ import urllib .request
5+ import urllib .parse
56from typing import Dict , List , Optional
67
7- def search_leetcode (query : str ) -> List [Dict ]:
8- """Search LeetCode problems using the GraphQL API"""
9- url = "https://leetcode.com/graphql"
10- is_number = query .strip ().isdigit ()
11- query_num = query .strip () if is_number else None
8+ def search_leetcode (query : str ) -> Optional [List [Dict ]]:
9+ """
10+ Search LeetCode problems using the GraphQL API.
11+
12+ Args:
13+ query: Search query string (problem title, keywords)
1214
15+ Returns:
16+ List of problem dictionaries or None on error
17+ """
18+ # Use a consistent query structure and result limit for all search types
19+ graphql_query = {
20+ "query" : """
21+ query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
22+ problemsetQuestionList: questionList(
23+ categorySlug: $categorySlug
24+ limit: $limit
25+ skip: $skip
26+ filters: $filters
27+ ) {
28+ questions: data {
29+ difficulty
30+ frontendQuestionId: questionFrontendId
31+ isPaidOnly: isPaidOnly
32+ questionId
33+ title
34+ titleSlug
35+ }
36+ }
37+ }
38+ """ ,
39+ "variables" : {
40+ "categorySlug" : "" ,
41+ "skip" : 0 ,
42+ "limit" : 10 , # Standard limit of 10 results
43+ "filters" : {
44+ "searchKeywords" : query
45+ }
46+ }
47+ }
48+
49+ # Construct the request
50+ url = "https://leetcode.com/graphql"
1351 headers = {
1452 "Content-Type" : "application/json" ,
53+ "Referer" : "https://leetcode.com/problemset/all/" ,
1554 "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
1655 }
1756
18- # For numeric queries, try to find exact problem by ID first
19- if is_number :
20- print (f"Searching for problem #{ query_num } " , file = sys .stderr )
21- exact_problem = get_problem_by_id (url , headers , query_num )
22- if exact_problem :
23- return [exact_problem ]
24-
25- # Do standard keyword search
26- return perform_search (url , headers , query )[:10 ] # Limit to 10 results
57+ # Let exceptions propagate to caller
58+ print (f"Searching LeetCode for: '{ query } '" , file = sys .stderr )
59+ data = json .dumps (graphql_query ).encode ('utf-8' )
60+ req = urllib .request .Request (url , data = data , headers = headers , method = "POST" )
61+ with urllib .request .urlopen (req ) as response :
62+ result = json .loads (response .read ().decode ('utf-8' ))
63+ if 'data' in result and 'problemsetQuestionList' in result ['data' ]:
64+ questions = result ['data' ]['problemsetQuestionList' ]['questions' ]
65+ print (f"Found { len (questions )} results" , file = sys .stderr )
66+ return questions
67+ return []
2768
28- def get_problem_by_id (url : str , headers : Dict , problem_id : str ) -> Optional [Dict ]:
29- """Try to get a specific problem by ID"""
30- all_problems_query = """
31- query allQuestions {
32- allQuestions: allQuestionsRaw {
33- questionId
34- title
35- titleSlug
36- difficulty
37- isPaidOnly
38- }
39- }
69+ def perform_search (query ):
4070 """
71+ Performs a search on LeetCode and returns formatted results.
4172
73+ Args:
74+ query: Search query string
75+
76+ Returns:
77+ List of problem dictionaries formatted for Alfred display
78+ """
4279 try :
43- print (f"Trying direct problem lookup for #{ problem_id } " , file = sys .stderr )
44- response = requests .post (
45- url ,
46- json = {"query" : all_problems_query },
47- headers = headers ,
48- timeout = 15
49- )
80+ # Get problems from LeetCode
81+ problems = search_leetcode (query )
82+
83+ if not problems :
84+ # No results found - consistent message for all query types
85+ return [{
86+ "title" : "No LeetCode problems found" ,
87+ "subtitle" : f"No results for '{ query } '" ,
88+ "icon" : {"path" : "icon.png" },
89+ "valid" : False
90+ }]
5091
51- if response .status_code == 200 :
52- data = response .json ()
53- if "data" in data and "allQuestions" in data ["data" ]:
54- all_problems = data ["data" ]["allQuestions" ]
55- for problem in all_problems :
56- if problem ["questionId" ] == problem_id :
57- print (f"Found direct match for problem #{ problem_id } : { problem ['title' ]} " , file = sys .stderr )
58- return problem
59- print (f"No problem with ID #{ problem_id } found" , file = sys .stderr )
92+ # Format all returned problems as Alfred items
93+ return format_alfred_items (problems )
6094 except Exception as e :
61- print (f"Error in direct problem lookup: { str (e )} " , file = sys .stderr )
62-
63- return None
95+ # Get error details based on exception type
96+ error_type = type (e ).__name__
97+
98+ if isinstance (e , urllib .error .HTTPError ):
99+ title = "LeetCode API Error"
100+ message = f"HTTP Error { e .code } : { e .reason } "
101+ elif isinstance (e , urllib .error .URLError ):
102+ title = "Network Error"
103+ message = f"Could not connect to LeetCode: { str (e )} "
104+ else :
105+ title = "Error"
106+ message = f"An unexpected error occurred: { str (e )} "
107+ print (f"Error ({ error_type } ): { str (e )} " , file = sys .stderr )
108+
109+ # Return appropriate error message
110+ return [{
111+ "title" : title ,
112+ "subtitle" : message ,
113+ "icon" : {"path" : "icon.png" },
114+ "valid" : False
115+ }]
64116
65- def perform_search (url : str , headers : Dict , query : str ) -> List [Dict ]:
66- """Perform a standard search by keywords"""
67- query_string = """
68- query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
69- problemsetQuestionList: questionList(
70- categorySlug: $categorySlug
71- limit: $limit
72- skip: $skip
73- filters: $filters
74- ) {
75- questions: data {
76- title
77- titleSlug
78- difficulty
79- questionId
80- isPaidOnly
81- }
82- }
83- }
117+ def format_alfred_items (problems : List [Dict ]) -> List [Dict ]:
84118 """
119+ Format LeetCode problems as Alfred items.
85120
86- variables = {
87- "categorySlug" : "" ,
88- "skip" : 0 ,
89- "limit" : 10 , # Only fetch 10 problems
90- "filters" : {
91- "searchKeywords" : query
92- }
93- }
121+ Args:
122+ problems: List of problem dictionaries from LeetCode API
123+
124+ Returns:
125+ List of dictionaries formatted for Alfred JSON output
126+ """
127+ alfred_items = []
94128
95- try :
96- print (f"Connecting to LeetCode API..." , file = sys .stderr )
97- response = requests .post (
98- url ,
99- json = {"query" : query_string , "variables" : variables },
100- headers = headers ,
101- timeout = 10
102- )
129+ for problem in problems :
130+ # Extract problem data
131+ problem_id = problem .get ('frontendQuestionId' , 'Unknown' )
132+ title = problem .get ('title' , 'Unknown' )
133+ difficulty = problem .get ('difficulty' , 'Unknown' )
134+ is_premium = problem .get ('isPaidOnly' , False ) or problem .get ('paidOnly' , False )
135+ slug = problem .get ('titleSlug' , '' )
103136
104- print (f"Response status: { response .status_code } " , file = sys .stderr )
105- response .raise_for_status ()
137+ # Create a formatted title with difficulty indicator
138+ difficulty_icon = {
139+ 'Easy' : '🟢' ,
140+ 'Medium' : '🟡' ,
141+ 'Hard' : '🔴'
142+ }.get (difficulty , '' )
106143
107- data = response .json ()
108- questions = data .get ("data" , {}).get ("problemsetQuestionList" , {}).get ("questions" , [])
109- print (f"Found { len (questions )} questions" , file = sys .stderr )
144+ premium_icon = '🔒 ' if is_premium else ''
110145
111- # For numeric queries, check for exact match
112- if query .strip ().isdigit ():
113- for q in questions :
114- if q .get ("questionId" ) == query .strip ():
115- print (f"Found exact match for problem #{ query } : { q ['title' ]} " , file = sys .stderr )
116- return [q ]
146+ formatted_title = f"{ problem_id } . { title } "
147+ subtitle = f"{ difficulty_icon } { difficulty } { premium_icon } - Click to open in browser"
117148
118- return questions
149+ # Create Alfred item
150+ alfred_item = {
151+ "title" : formatted_title ,
152+ "subtitle" : subtitle ,
153+ "arg" : f"https://leetcode.com/problems/{ slug } /" ,
154+ "icon" : {"path" : "icon.png" },
155+ "valid" : True
156+ }
119157
120- except Exception as e :
121- print (f"Error searching LeetCode: { str (e )} " , file = sys .stderr )
122- return []
123-
124- def format_alfred_items (questions : List [Dict ]) -> List [Dict ]:
125- """Format the questions into Alfred items"""
126- items = []
127- for q in questions :
128- try :
129- difficulty_emoji = {
130- "Easy" : "🟢" ,
131- "Medium" : "🟡" ,
132- "Hard" : "🔴"
133- }.get (q .get ("difficulty" , "" ), "" )
134-
135- # Add problem number to the title
136- problem_num = q .get ("questionId" , "" )
137- title = f"{ problem_num } . { q ['title' ]} " if problem_num else q ["title" ]
138-
139- subtitle = f"{ difficulty_emoji } { q .get ('difficulty' , '' )} • { q .get ('titleSlug' , '' )} "
140-
141- if q .get ("isPaidOnly" , False ):
142- subtitle += " • 🔒 Premium"
143-
144- # Construct the full LeetCode URL
145- problem_url = f"https://leetcode.com/problems/{ q .get ('titleSlug' , '' )} /"
146-
147- # Create Alfred item with the URL as the arg (for browser opening)
148- items .append ({
149- "title" : title ,
150- "subtitle" : subtitle ,
151- "arg" : problem_url ,
152- "valid" : True ,
153- "icon" : {"path" : "icon.png" }
154- })
155- except Exception as e :
156- print (f"Error formatting question: { e } " , file = sys .stderr )
157- continue
158-
159- # If no results, add a "no results" item
160- if not items :
161- items .append ({
162- "title" : "No LeetCode problems found" ,
163- "subtitle" : "Try a different search term" ,
164- "valid" : False ,
165- "icon" : {"path" : "icon.png" }
166- })
158+ alfred_items .append (alfred_item )
167159
168- return items
160+ return alfred_items
169161
170162def main ():
163+ # Get the search query from command line arguments
171164 if len (sys .argv ) < 2 :
172- print (json . dumps ({ "items" : []}) )
165+ print ("Usage: python alfred_leetcode.py [search_query]" )
173166 return
174167
175- query = sys .argv [1 ]
168+ # Get the search query from arguments
169+ query = ' ' .join (sys .argv [1 :])
176170
177- # Remove the immediate output that was blocking final results
171+ # Perform the search and get results
172+ results = perform_search (query )
178173
179- print (f"Searching LeetCode for: '{ query } '" , file = sys .stderr )
180- questions = search_leetcode (query )
181-
182- # Print the results for debugging
183- if questions :
184- print (f"Found { len (questions )} results:" , file = sys .stderr )
185- print ("-" * 50 , file = sys .stderr )
186-
187- for i , q in enumerate (questions , 1 ):
188- title = q .get ("title" , "Unknown" )
189- difficulty = q .get ("difficulty" , "Unknown" )
190- slug = q .get ("titleSlug" , "unknown" )
191- question_id = q .get ("questionId" , "N/A" )
192- difficulty_emoji = {"Easy" : "🟢" , "Medium" : "🟡" , "Hard" : "🔴" }.get (difficulty , "" )
193-
194- paid_only = "• 🔒 Premium" if q .get ("isPaidOnly" , False ) else ""
195-
196- print (f"{ i } . { question_id } . { title } " , file = sys .stderr )
197- print (f" { difficulty_emoji } { difficulty } • { slug } { paid_only } " , file = sys .stderr )
198- print (f" https://leetcode.com/problems/{ slug } /" , file = sys .stderr )
199- print ("" , file = sys .stderr )
174+ # Output the results in Alfred's JSON format
175+ alfred_output = {
176+ "items" : results
177+ }
200178
201- items = format_alfred_items (questions )
202- print (json .dumps ({"items" : items }))
179+ print (json .dumps (alfred_output ))
203180
204181if __name__ == "__main__" :
205182 main ()
0 commit comments