1+ #!/usr/bin/env python3
2+ import sys
3+ import json
4+ import requests
5+ from typing import Dict , List , Optional
6+
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
12+
13+ headers = {
14+ "Content-Type" : "application/json" ,
15+ "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"
16+ }
17+
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
27+
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+ }
40+ """
41+
42+ 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+ )
50+
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 )
60+ except Exception as e :
61+ print (f"Error in direct problem lookup: { str (e )} " , file = sys .stderr )
62+
63+ return None
64+
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+ }
84+ """
85+
86+ variables = {
87+ "categorySlug" : "" ,
88+ "skip" : 0 ,
89+ "limit" : 10 , # Only fetch 10 problems
90+ "filters" : {
91+ "searchKeywords" : query
92+ }
93+ }
94+
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+ )
103+
104+ print (f"Response status: { response .status_code } " , file = sys .stderr )
105+ response .raise_for_status ()
106+
107+ data = response .json ()
108+ questions = data .get ("data" , {}).get ("problemsetQuestionList" , {}).get ("questions" , [])
109+ print (f"Found { len (questions )} questions" , file = sys .stderr )
110+
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 ]
117+
118+ return questions
119+
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+ })
167+
168+ return items
169+
170+ def main ():
171+ if len (sys .argv ) < 2 :
172+ print (json .dumps ({"items" : []}))
173+ return
174+
175+ query = sys .argv [1 ]
176+
177+ # Remove the immediate output that was blocking final results
178+
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 )
200+
201+ items = format_alfred_items (questions )
202+ print (json .dumps ({"items" : items }))
203+
204+ if __name__ == "__main__" :
205+ main ()
0 commit comments