WebashalarForML commited on
Commit
47df4ee
·
verified ·
1 Parent(s): 3e89456

Upload 5 files

Browse files
Files changed (4) hide show
  1. app.py +32 -8
  2. templates/batch.html +99 -13
  3. templates/index.html +8 -0
  4. templates/landing.html +7 -0
app.py CHANGED
@@ -145,25 +145,49 @@ def scrape():
145
  return jsonify({"error": str(e)}), 500
146
 
147
 
148
- @app.route('/scrape-batch', methods=['POST'])
149
- def scrape_batch():
150
  try:
151
  data = request.json
152
  query = data.get('query', '').strip()
153
- app_count = int(data.get('app_count', 5))
154
- count_type = data.get('review_count_type', 'fixed')
155
- reviews_per_app = 100000 if count_type == 'all' else int(data.get('reviews_per_app', 100))
156
 
157
- # 1. Find apps (Try direct scraping first for reliability)
158
  app_ids = scrape_store_ids(query, n_hits=app_count)
159
 
160
- # If scraper fails, try library search
161
  if not app_ids:
 
162
  hits = search(query, lang="en", country="us", n_hits=app_count)
163
  app_ids = [h['appId'] for h in hits if h.get('appId')]
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  if not app_ids:
166
- return jsonify({"error": "No apps found for this query"}), 404
167
 
168
  batch_results = []
169
  all_combined_reviews = []
 
145
  return jsonify({"error": str(e)}), 500
146
 
147
 
148
+ @app.route('/find-apps', methods=['POST'])
149
+ def find_apps():
150
  try:
151
  data = request.json
152
  query = data.get('query', '').strip()
153
+ app_count = int(data.get('app_count', 10))
 
 
154
 
155
+ # Using the robust scraper to get IDs
156
  app_ids = scrape_store_ids(query, n_hits=app_count)
157
 
 
158
  if not app_ids:
159
+ # Fallback to library
160
  hits = search(query, lang="en", country="us", n_hits=app_count)
161
  app_ids = [h['appId'] for h in hits if h.get('appId')]
162
 
163
+ results = []
164
+ for aid in app_ids:
165
+ try:
166
+ info = app_info(aid, lang='en', country='us')
167
+ results.append({
168
+ "appId": aid,
169
+ "title": info['title'],
170
+ "icon": info['icon'],
171
+ "score": info['score'],
172
+ "developer": info.get('developer', 'Unknown'),
173
+ "installs": info.get('installs', '0+')
174
+ })
175
+ except: continue
176
+
177
+ return jsonify({"results": results})
178
+ except Exception as e:
179
+ return jsonify({"error": str(e)}), 500
180
+
181
+ @app.route('/scrape-batch', methods=['POST'])
182
+ def scrape_batch():
183
+ try:
184
+ data = request.json
185
+ app_ids = data.get('app_ids', [])
186
+ count_type = data.get('review_count_type', 'fixed')
187
+ reviews_per_app = 100000 if count_type == 'all' else int(data.get('reviews_per_app', 100))
188
+
189
  if not app_ids:
190
+ return jsonify({"error": "No app IDs provided"}), 400
191
 
192
  batch_results = []
193
  all_combined_reviews = []
templates/batch.html CHANGED
@@ -21,11 +21,22 @@
21
  --muted2: #94a3b8;
22
  }
23
  * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
 
 
24
  body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
25
 
 
 
 
26
  .header { height: 60px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 20px; }
27
  .main { flex: 1; display: flex; overflow: hidden; }
28
- .sidebar { width: 320px; background: var(--surface); border-right: 1px solid var(--border); padding: 20px; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; }
29
  .content { flex: 1; background: var(--bg); position: relative; display: flex; flex-direction: column; }
30
 
31
  .mode-toggle { display: grid; grid-template-columns: 1fr 1fr; background: var(--bg); padding: 4px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 5px; }
@@ -33,9 +44,9 @@
33
  .mode-btn.active { background: var(--surface2); color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
34
 
35
  .logo { font-weight: 800; font-size: 18px; color: var(--accent); display: flex; align-items: center; gap: 8px; text-decoration: none; }
36
- .input-group { display: flex; flex-direction: column; gap: 8px; }
37
- .label { font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--muted); letter-spacing: 0.5px; }
38
- input, select { background: var(--bg); border: 1px solid var(--border); color: white; padding: 12px; border-radius: 8px; font-size: 13px; outline: none; width: 100%; }
39
  input:focus { border-color: var(--accent); }
40
 
41
  .btn-main { background: var(--accent); color: white; border: none; padding: 14px; border-radius: 10px; font-weight: 800; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: 0.2s; width: 100%; border-bottom: 3px solid rgba(0,0,0,0.2); }
@@ -100,22 +111,41 @@
100
  <div class="main">
101
  <aside class="sidebar">
102
  <div class="input-group">
103
- <div class="label">Category / Search Query</div>
104
- <input type="text" id="query" placeholder="e.g. Multiplayer Games, Fintech..." value="Multiplayer Games">
 
 
 
105
  </div>
106
 
107
  <div class="input-group">
108
- <div class="label">App Comparison Count</div>
109
- <input type="number" id="app_count" value="5" min="1" max="20">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  </div>
111
 
112
  <div class="input-group">
113
- <div class="label">Reviews Per App</div>
 
114
  <div class="mode-toggle">
115
  <div class="mode-btn active" id="btn-fixed" onclick="setMode('fixed')">Custom</div>
116
  <div class="mode-btn" id="btn-all" onclick="setMode('all')">Fetch All</div>
117
  </div>
118
- <input type="number" id="reviews_per_app" value="100" min="10" step="10">
119
  </div>
120
 
121
  <div class="input-group">
@@ -206,10 +236,67 @@
206
  document.querySelectorAll('.star-cb').forEach(cb => cb.checked = check);
207
  }
208
 
209
- async function runBatch() {
 
 
210
  const q = document.getElementById('query').value.trim();
211
  if (!q) return;
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  const stars = [...document.querySelectorAll('.star-cb:checked')].map(cb => parseInt(cb.value));
214
  if (!stars.length) return alert('Select at least one star rating');
215
 
@@ -223,8 +310,7 @@
223
  method: 'POST',
224
  headers: { 'Content-Type': 'application/json' },
225
  body: JSON.stringify({
226
- query: q,
227
- app_count: document.getElementById('app_count').value,
228
  review_count_type: currentMode,
229
  reviews_per_app: document.getElementById('reviews_per_app').value,
230
  sort_order: document.getElementById('sort').value,
 
21
  --muted2: #94a3b8;
22
  }
23
  * { box-sizing: border-box; margin: 0; padding: 0; }
24
+
25
+ /* Modern Scrollbar */
26
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
27
+ ::-webkit-scrollbar-track { background: transparent; }
28
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
29
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
30
+ * { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.1) transparent; }
31
+
32
  body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
33
 
34
+ .btn-sm { background: var(--surface2); border: 1px solid var(--border); color: white; padding: 4px 10px; border-radius: 6px; font-size: 10px; cursor: pointer; transition: 0.2s; }
35
+ .btn-sm:hover { border-color: var(--accent); }
36
+
37
  .header { height: 60px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 20px; }
38
  .main { flex: 1; display: flex; overflow: hidden; }
39
+ .sidebar { width: 300px; background: var(--surface); border-right: 1px solid var(--border); padding: 15px; display: flex; flex-direction: column; gap: 15px; overflow-y: auto; }
40
  .content { flex: 1; background: var(--bg); position: relative; display: flex; flex-direction: column; }
41
 
42
  .mode-toggle { display: grid; grid-template-columns: 1fr 1fr; background: var(--bg); padding: 4px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 5px; }
 
44
  .mode-btn.active { background: var(--surface2); color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
45
 
46
  .logo { font-weight: 800; font-size: 18px; color: var(--accent); display: flex; align-items: center; gap: 8px; text-decoration: none; }
47
+ .input-group { display: flex; flex-direction: column; gap: 6px; }
48
+ .label { font-size: 10px; font-weight: 700; text-transform: uppercase; color: var(--muted); letter-spacing: 0.5px; }
49
+ input, select { background: var(--bg); border: 1px solid var(--border); color: white; padding: 10px; border-radius: 8px; font-size: 12px; outline: none; width: 100%; }
50
  input:focus { border-color: var(--accent); }
51
 
52
  .btn-main { background: var(--accent); color: white; border: none; padding: 14px; border-radius: 10px; font-weight: 800; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: 0.2s; width: 100%; border-bottom: 3px solid rgba(0,0,0,0.2); }
 
111
  <div class="main">
112
  <aside class="sidebar">
113
  <div class="input-group">
114
+ <div class="label">Step 1: Discover Apps</div>
115
+ <div style="display:flex;gap:8px;">
116
+ <input type="text" id="query" placeholder="e.g. Multiplayer Games..." value="Multiplayer Games" style="flex:1">
117
+ <button onclick="findApps()" id="btnFind" style="background:var(--accent); border:none; color:white; padding:0 15px; border-radius:8px; cursor:pointer; font-weight:700;">Find</button>
118
+ </div>
119
  </div>
120
 
121
  <div class="input-group">
122
+ <div class="label">Discovery Limit</div>
123
+ <input type="number" id="app_count" value="10" min="1" max="50">
124
+ <div style="font-size:10px; color:var(--muted); margin-top:4px;">How many apps to search for initially.</div>
125
+ </div>
126
+
127
+ <div id="selectionArea" class="hidden" style="background:var(--surface2); border:1px solid var(--border); border-radius:12px; padding:10px; display:flex; flex-direction:column; gap:8px;">
128
+ <div class="label" style="display:flex; justify-content:space-between; align-items:center;">
129
+ <span>Select Apps</span>
130
+ <span id="selectedCount" style="color:var(--accent); font-size:9px;">0 selected</span>
131
+ </div>
132
+ <div id="appList" style="max-height:160px; overflow-y:auto; overflow-x:hidden; display:flex; flex-direction:column; gap:4px; padding-right:4px;">
133
+ <!-- Compact apps list -->
134
+ </div>
135
+ <div style="display:flex; gap:5px;">
136
+ <button onclick="toggleAllApps(true)" class="btn-sm" style="flex:1">All</button>
137
+ <button onclick="toggleAllApps(false)" class="btn-sm" style="flex:1">None</button>
138
+ </div>
139
  </div>
140
 
141
  <div class="input-group">
142
+ <div class="label">Step 2: Scrape Settings</div>
143
+ <div class="label" style="font-size:10px; margin-top:10px;">Reviews Per App</div>
144
  <div class="mode-toggle">
145
  <div class="mode-btn active" id="btn-fixed" onclick="setMode('fixed')">Custom</div>
146
  <div class="mode-btn" id="btn-all" onclick="setMode('all')">Fetch All</div>
147
  </div>
148
+ <input type="number" id="reviews_per_app" value="50" min="10" step="10">
149
  </div>
150
 
151
  <div class="input-group">
 
236
  document.querySelectorAll('.star-cb').forEach(cb => cb.checked = check);
237
  }
238
 
239
+ let foundApps = [];
240
+
241
+ async function findApps() {
242
  const q = document.getElementById('query').value.trim();
243
  if (!q) return;
244
 
245
+ const btn = document.getElementById('btnFind');
246
+ btn.disabled = true;
247
+ btn.innerText = 'Searching...';
248
+
249
+ try {
250
+ const res = await fetch('/find-apps', {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({ query: q, app_count: document.getElementById('app_count').value })
254
+ });
255
+ const data = await res.json();
256
+ if (!res.ok) throw new Error(data.error || 'Discovery failed');
257
+
258
+ foundApps = data.results;
259
+ renderAppSelection();
260
+ } catch(e) {
261
+ alert(e.message);
262
+ } finally {
263
+ btn.disabled = false;
264
+ btn.innerText = 'Find';
265
+ }
266
+ }
267
+
268
+ function renderAppSelection() {
269
+ const list = document.getElementById('appList');
270
+ document.getElementById('selectionArea').classList.remove('hidden');
271
+
272
+ list.innerHTML = foundApps.map(a => `
273
+ <label style="display:flex; align-items:center; gap:8px; padding:6px; background:var(--bg); border-radius:6px; border:1px solid var(--border); cursor:pointer; min-width:0;">
274
+ <input type="checkbox" class="app-cb" value="${a.appId}" onchange="updateSelectionCount()" checked style="width:14px; height:14px; margin:0;">
275
+ <img src="${a.icon}" style="width:20px; height:20px; border-radius:4px; flex-shrink:0;">
276
+ <div style="flex:1; min-width:0;">
277
+ <div style="font-size:10px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--text);">${a.title}</div>
278
+ <div style="font-size:9px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${a.developer}</div>
279
+ </div>
280
+ </label>
281
+ `).join('');
282
+ updateSelectionCount();
283
+ }
284
+
285
+ function toggleAllApps(check) {
286
+ document.querySelectorAll('.app-cb').forEach(cb => cb.checked = check);
287
+ updateSelectionCount();
288
+ }
289
+
290
+ function updateSelectionCount() {
291
+ const count = document.querySelectorAll('.app-cb:checked').length;
292
+ document.getElementById('selectedCount').innerText = `${count} selected`;
293
+ document.getElementById('go').disabled = count === 0;
294
+ }
295
+
296
+ async function runBatch() {
297
+ const selectedAppIds = [...document.querySelectorAll('.app-cb:checked')].map(cb => cb.value);
298
+ if (!selectedAppIds.length) return alert('Select at least one app');
299
+
300
  const stars = [...document.querySelectorAll('.star-cb:checked')].map(cb => parseInt(cb.value));
301
  if (!stars.length) return alert('Select at least one star rating');
302
 
 
310
  method: 'POST',
311
  headers: { 'Content-Type': 'application/json' },
312
  body: JSON.stringify({
313
+ app_ids: selectedAppIds,
 
314
  review_count_type: currentMode,
315
  reviews_per_app: document.getElementById('reviews_per_app').value,
316
  sort_order: document.getElementById('sort').value,
templates/index.html CHANGED
@@ -21,6 +21,14 @@
21
  --muted2: #94a3b8;
22
  }
23
  * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
 
 
24
  body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
25
 
26
  .header { height: 60px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 20px; }
 
21
  --muted2: #94a3b8;
22
  }
23
  * { box-sizing: border-box; margin: 0; padding: 0; }
24
+
25
+ /* Modern Scrollbar */
26
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
27
+ ::-webkit-scrollbar-track { background: transparent; }
28
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
29
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
30
+ * { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.1) transparent; }
31
+
32
  body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
33
 
34
  .header { height: 60px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 20px; }
templates/landing.html CHANGED
@@ -17,6 +17,13 @@
17
  }
18
 
19
  * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
 
20
 
21
  body {
22
  font-family: 'Inter', sans-serif;
 
17
  }
18
 
19
  * { box-sizing: border-box; margin: 0; padding: 0; }
20
+
21
+ /* Modern Scrollbar */
22
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
23
+ ::-webkit-scrollbar-track { background: transparent; }
24
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
25
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
26
+ * { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.1) transparent; }
27
 
28
  body {
29
  font-family: 'Inter', sans-serif;