euler314 commited on
Commit
65b12b7
·
verified ·
1 Parent(s): f4280e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +394 -48
app.py CHANGED
@@ -32,6 +32,14 @@ import googleapiclient.discovery
32
  import google.auth.transport.requests
33
  import googleapiclient.http
34
 
 
 
 
 
 
 
 
 
35
  # -------------------- Logging Setup --------------------
36
  logging.basicConfig(
37
  level=logging.INFO,
@@ -88,6 +96,77 @@ PROXY_ROTATION_CONFIG = {
88
  "proxies": [] # Will be populated from the UI if needed
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  # -------------------- Utility Functions --------------------
92
  def get_random_user_agent():
93
  return random.choice(USER_AGENTS)
@@ -857,6 +936,118 @@ class DownloadManager:
857
  logger.error(f"Error getting exam links: {e}")
858
  return []
859
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
  async def extract_downloadable_files(self, url, custom_ext_list):
861
  found_files = []
862
  try:
@@ -1176,6 +1367,17 @@ class DownloadManager:
1176
  'metadata': {}
1177
  })
1178
 
 
 
 
 
 
 
 
 
 
 
 
1179
  # Deduplicate files by URL
1180
  seen_urls = set()
1181
  unique_files = []
@@ -2725,6 +2927,9 @@ def main():
2725
  st.session_state.do_deep_search = False
2726
  st.session_state.deep_search_url = None
2727
  st.session_state.search_results = []
 
 
 
2728
 
2729
  with st.sidebar:
2730
  mode = st.radio("Select Mode", ["Manual URL", "Bing Search"], key="mode_select")
@@ -2814,6 +3019,99 @@ def main():
2814
 
2815
  if st.session_state.discovered_files:
2816
  files = st.session_state.discovered_files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2817
  col1, col2 = st.columns([1, 4])
2818
  with col1:
2819
  if st.button("Select All", key="select_all_btn"):
@@ -2821,43 +3119,19 @@ def main():
2821
  if st.button("Clear Selection", key="clear_selection_btn"):
2822
  st.session_state.selected_files = []
2823
 
2824
- # Create a formatted display of files with metadata
2825
- file_options = []
2826
- for i, file in enumerate(files):
2827
- filename = file['filename']
2828
- size = file['size']
2829
- meta = file.get('metadata', {})
2830
-
2831
- # Format display string with relevant metadata
2832
- if meta and 'Pages' in meta:
2833
- file_info = f"{filename} ({size}) - {meta.get('Pages', '')} pages"
2834
- else:
2835
- file_info = f"{filename} ({size})"
2836
-
2837
- file_options.append((i, file_info))
2838
-
2839
- selected_indices = st.multiselect(
2840
- "Select files to download",
2841
- options=[i for i, _ in file_options],
2842
- default=st.session_state.selected_files,
2843
- format_func=lambda i: next(info for idx, info in file_options if idx == i),
2844
- key="file_multiselect"
2845
- )
2846
-
2847
- st.session_state.selected_files = selected_indices
2848
-
2849
- if selected_indices:
2850
- col1, col2, col3, col4 = st.columns(4)
2851
- with col1:
2852
  download_dir = st.text_input("Download Directory", value="./downloads", key="download_dir_input")
2853
- with col2:
2854
  create_zip = st.checkbox("Create ZIP file", value=True, key="create_zip_checkbox")
2855
- with col3:
2856
  delete_after = st.checkbox("Delete after creating ZIP", key="delete_after_checkbox")
2857
- with col4:
2858
  upload_to_drive = st.checkbox("Upload to Google Drive", key="upload_drive_checkbox")
2859
 
2860
- if st.button("Download Selected", key="download_btn"):
2861
  if not os.path.exists(download_dir):
2862
  os.makedirs(download_dir)
2863
 
@@ -2871,10 +3145,10 @@ def main():
2871
  proxy=proxy,
2872
  use_stealth=use_stealth
2873
  ) as dm:
2874
- for i, idx in enumerate(selected_indices):
2875
- progress = (i + 1) / len(selected_indices)
2876
  file_info = files[idx]
2877
- status_text.text(f"Downloading {file_info['filename']}... ({i+1}/{len(selected_indices)})")
2878
  progress_bar.progress(progress)
2879
 
2880
  path = await dm.download_file(file_info, download_dir, url)
@@ -2925,23 +3199,49 @@ def main():
2925
  except Exception as e:
2926
  st.warning(f"Could not delete {path}: {e}")
2927
  st.info("Deleted original files after ZIP creation")
2928
- else:
2929
- # Provide individual file downloads
2930
- st.write("Download files individually:")
2931
- for path in downloaded:
2932
- with open(path, "rb") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2933
  file_data = f.read()
2934
 
2935
- file_name = os.path.basename(path)
2936
- mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
2937
-
2938
  st.download_button(
2939
- label=f"Download {file_name}",
2940
  data=file_data,
2941
- file_name=file_name,
2942
- mime=mime_type,
2943
- key=f"download_file_{path}"
2944
  )
 
 
 
 
 
 
 
 
 
2945
 
2946
  elif mode == "Bing Search":
2947
  st.header("Bing Search Mode")
@@ -3007,6 +3307,52 @@ def main():
3007
  st.session_state.discovered_files = files
3008
  st.session_state.current_url = url
3009
  st.success(f"Found {len(files)} files!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3010
  else:
3011
  st.warning("No files found.")
3012
 
@@ -3054,7 +3400,7 @@ def main():
3054
 
3055
  # Add footer with attribution
3056
  st.markdown('---')
3057
- st.markdown('Created by [Euler314](https://github.com/euler314)')
3058
 
3059
  if __name__ == "__main__":
3060
  main()
 
32
  import google.auth.transport.requests
33
  import googleapiclient.http
34
 
35
+ # New imports for RAG search
36
+ import nltk
37
+ from sklearn.feature_extraction.text import TfidfVectorizer
38
+ from sklearn.metrics.pairwise import cosine_similarity
39
+ import numpy as np
40
+ import docx2txt
41
+ import PyPDF2
42
+
43
  # -------------------- Logging Setup --------------------
44
  logging.basicConfig(
45
  level=logging.INFO,
 
96
  "proxies": [] # Will be populated from the UI if needed
97
  }
98
 
99
+ # -------------------- RAG Search Class --------------------
100
+ class RAGSearch:
101
+ def __init__(self):
102
+ self.file_texts = []
103
+ self.file_metadata = []
104
+ self.vectorizer = TfidfVectorizer(stop_words='english')
105
+ self.vectors = None
106
+
107
+ def add_file(self, file_data, file_info):
108
+ """Add a file to the search index"""
109
+ file_ext = os.path.splitext(file_info['filename'])[1]
110
+ text = self.extract_text(file_data, file_ext)
111
+ if text:
112
+ self.file_texts.append(text)
113
+ self.file_metadata.append(file_info)
114
+ return True
115
+ return False
116
+
117
+ def extract_text(self, file_data, file_ext):
118
+ """Extract text from different file types"""
119
+ try:
120
+ if file_ext.lower() == '.pdf':
121
+ reader = PyPDF2.PdfReader(BytesIO(file_data))
122
+ text = ""
123
+ for page in reader.pages:
124
+ text += page.extract_text() + "\n"
125
+ return text
126
+ elif file_ext.lower() in ['.docx', '.doc']:
127
+ return docx2txt.process(BytesIO(file_data))
128
+ elif file_ext.lower() in ['.txt', '.csv', '.json']:
129
+ return file_data.decode('utf-8', errors='ignore')
130
+ else:
131
+ return ""
132
+ except Exception as e:
133
+ logger.error(f"Error extracting text: {e}")
134
+ return ""
135
+
136
+ def build_index(self):
137
+ """Build the search index"""
138
+ if not self.file_texts:
139
+ return False
140
+ try:
141
+ self.vectors = self.vectorizer.fit_transform(self.file_texts)
142
+ return True
143
+ except Exception as e:
144
+ logger.error(f"Error building search index: {e}")
145
+ return False
146
+
147
+ def search(self, query, top_k=5):
148
+ """Search the index for relevant files"""
149
+ if self.vectors is None:
150
+ return []
151
+
152
+ try:
153
+ query_vector = self.vectorizer.transform([query])
154
+ similarities = cosine_similarity(query_vector, self.vectors).flatten()
155
+ top_indices = similarities.argsort()[-top_k:][::-1]
156
+
157
+ results = []
158
+ for i, idx in enumerate(top_indices):
159
+ if similarities[idx] > 0:
160
+ results.append({
161
+ 'file_info': self.file_metadata[idx],
162
+ 'score': float(similarities[idx]),
163
+ 'rank': i+1
164
+ })
165
+ return results
166
+ except Exception as e:
167
+ logger.error(f"Error during search: {e}")
168
+ return []
169
+
170
  # -------------------- Utility Functions --------------------
171
  def get_random_user_agent():
172
  return random.choice(USER_AGENTS)
 
936
  logger.error(f"Error getting exam links: {e}")
937
  return []
938
 
939
+ async def discover_hidden_links(self, page):
940
+ """Discover hidden links that might be in JavaScript, iframes, or dynamic content"""
941
+ hidden_links = set()
942
+
943
+ # Execute JavaScript to find links in script tags and data attributes
944
+ js_links = await page.evaluate("""
945
+ () => {
946
+ const links = new Set();
947
+
948
+ // Extract URLs from script tags
949
+ const scripts = document.querySelectorAll('script');
950
+ for (const script of scripts) {
951
+ const content = script.textContent || '';
952
+ const urlMatches = content.match(/["'](https?:\/\/[^"']+)["']/g) || [];
953
+ for (let match of urlMatches) {
954
+ links.add(match.replace(/["']/g, ''));
955
+ }
956
+ }
957
+
958
+ // Check for links in data attributes
959
+ const elements = document.querySelectorAll('*[data-url], *[data-href], *[data-src], *[data-link]');
960
+ for (const el of elements) {
961
+ for (const attr of ['data-url', 'data-href', 'data-src', 'data-link']) {
962
+ const val = el.getAttribute(attr);
963
+ if (val && val.match(/^https?:\/\//)) {
964
+ links.add(val);
965
+ }
966
+ }
967
+ }
968
+
969
+ // Look for URLs in inline event handlers
970
+ const clickableElements = document.querySelectorAll('*[onclick], *[onmousedown], *[onmouseup]');
971
+ for (const el of clickableElements) {
972
+ for (const attr of ['onclick', 'onmousedown', 'onmouseup']) {
973
+ const val = el.getAttribute(attr);
974
+ if (val) {
975
+ const urlMatches = val.match(/["'](https?:\/\/[^"']+)["']/g) || [];
976
+ for (let match of urlMatches) {
977
+ links.add(match.replace(/["']/g, ''));
978
+ }
979
+ }
980
+ }
981
+ }
982
+
983
+ return Array.from(links);
984
+ }
985
+ """)
986
+
987
+ for link in js_links:
988
+ hidden_links.add(link)
989
+
990
+ # Extract links from iframes
991
+ iframes = await page.query_selector_all('iframe')
992
+ for iframe in iframes:
993
+ try:
994
+ frame = await iframe.content_frame()
995
+ if frame:
996
+ iframe_links = await frame.evaluate("""
997
+ () => {
998
+ return Array.from(document.querySelectorAll('a[href]'))
999
+ .map(a => a.href)
1000
+ .filter(href => href.startsWith('http'));
1001
+ }
1002
+ """)
1003
+ for link in iframe_links:
1004
+ hidden_links.add(link)
1005
+ except Exception as e:
1006
+ logger.warning(f"Could not extract links from iframe: {e}")
1007
+
1008
+ # Look for links in shadow DOM (used in modern web components)
1009
+ shadow_links = await page.evaluate("""
1010
+ () => {
1011
+ const links = new Set();
1012
+
1013
+ // Helper function to recursively process shadow roots
1014
+ function processShadowRoot(root) {
1015
+ if (!root) return;
1016
+
1017
+ // Get links in this shadow root
1018
+ const shadowLinks = root.querySelectorAll('a[href]');
1019
+ for (const link of shadowLinks) {
1020
+ if (link.href && link.href.startsWith('http')) {
1021
+ links.add(link.href);
1022
+ }
1023
+ }
1024
+
1025
+ // Process nested shadow roots
1026
+ const elements = root.querySelectorAll('*');
1027
+ for (const el of elements) {
1028
+ if (el.shadowRoot) {
1029
+ processShadowRoot(el.shadowRoot);
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ // Find all shadow roots in the document
1035
+ const elements = document.querySelectorAll('*');
1036
+ for (const el of elements) {
1037
+ if (el.shadowRoot) {
1038
+ processShadowRoot(el.shadowRoot);
1039
+ }
1040
+ }
1041
+
1042
+ return Array.from(links);
1043
+ }
1044
+ """)
1045
+
1046
+ for link in shadow_links:
1047
+ hidden_links.add(link)
1048
+
1049
+ return hidden_links
1050
+
1051
  async def extract_downloadable_files(self, url, custom_ext_list):
1052
  found_files = []
1053
  try:
 
1367
  'metadata': {}
1368
  })
1369
 
1370
+ # Check for hidden links that might be in JavaScript, iframes, or dynamic content
1371
+ hidden_links = await self.discover_hidden_links(self.page)
1372
+ for link in hidden_links:
1373
+ if any(link.lower().endswith(ext) for ext in all_exts):
1374
+ found_files.append({
1375
+ 'url': link,
1376
+ 'filename': os.path.basename(link.split('?')[0]),
1377
+ 'size': await self.get_file_size(link),
1378
+ 'metadata': {}
1379
+ })
1380
+
1381
  # Deduplicate files by URL
1382
  seen_urls = set()
1383
  unique_files = []
 
2927
  st.session_state.do_deep_search = False
2928
  st.session_state.deep_search_url = None
2929
  st.session_state.search_results = []
2930
+ # For RAG search
2931
+ st.session_state.rag_indexed = False
2932
+ st.session_state.rag_engine = None
2933
 
2934
  with st.sidebar:
2935
  mode = st.radio("Select Mode", ["Manual URL", "Bing Search"], key="mode_select")
 
3019
 
3020
  if st.session_state.discovered_files:
3021
  files = st.session_state.discovered_files
3022
+
3023
+ # Display files with direct download buttons
3024
+ download_dir = "./downloads"
3025
+ os.makedirs(download_dir, exist_ok=True)
3026
+
3027
+ # Add RAG Search interface
3028
+ st.markdown("### Search Within Discovered Files")
3029
+ search_query = st.text_input("Enter search terms", key="rag_search_query")
3030
+
3031
+ if st.button("Search Files", key="rag_search_btn") and search_query:
3032
+ # Initialize RAG search engine
3033
+ if not st.session_state.rag_indexed:
3034
+ rag_search = RAGSearch()
3035
+
3036
+ with st.spinner("Indexing files for search..."):
3037
+ # First download files to extract text
3038
+ temp_dir = "./temp_downloads"
3039
+ os.makedirs(temp_dir, exist_ok=True)
3040
+
3041
+ async def download_for_indexing():
3042
+ downloaded = 0
3043
+ async with DownloadManager(use_proxy=use_proxy, proxy=proxy, use_stealth=use_stealth) as dm:
3044
+ for i, file_info in enumerate(files):
3045
+ # Only process common text-based file formats
3046
+ ext = os.path.splitext(file_info['filename'])[1].lower()
3047
+ if ext in ['.pdf', '.doc', '.docx', '.txt', '.csv', '.json']:
3048
+ path = await dm.download_file(file_info, temp_dir, url)
3049
+ if path:
3050
+ with open(path, 'rb') as f:
3051
+ file_data = f.read()
3052
+
3053
+ # Add to search index
3054
+ if rag_search.add_file(file_data, file_info):
3055
+ downloaded += 1
3056
+
3057
+ # Clean up
3058
+ os.remove(path)
3059
+ return downloaded
3060
+
3061
+ indexed_count = asyncio.run(download_for_indexing())
3062
+ if indexed_count > 0:
3063
+ rag_search.build_index()
3064
+ st.session_state.rag_engine = rag_search
3065
+ st.session_state.rag_indexed = True
3066
+ st.success(f"Indexed {indexed_count} files for search")
3067
+ else:
3068
+ st.warning("Could not index any files. Try with more text-based documents.")
3069
+
3070
+ # Perform the search
3071
+ if st.session_state.rag_indexed:
3072
+ search_results = st.session_state.rag_engine.search(search_query)
3073
+
3074
+ if search_results:
3075
+ st.write(f"Found {len(search_results)} relevant files:")
3076
+
3077
+ for result in search_results:
3078
+ file_info = result['file_info']
3079
+ score = result['score']
3080
+ with st.expander(f"{file_info['filename']} (Relevance: {score:.2f})"):
3081
+ st.write(f"Size: {file_info['size']}")
3082
+ if 'metadata' in file_info and file_info['metadata']:
3083
+ st.write("Metadata:")
3084
+ for k, v in file_info['metadata'].items():
3085
+ if k != 'file_id': # Skip technical details
3086
+ st.write(f"- {k}: {v}")
3087
+
3088
+ # Add direct download button
3089
+ if st.button(f"Download this file", key=f"rag_dl_{result['rank']}"):
3090
+ with st.spinner(f"Downloading {file_info['filename']}..."):
3091
+ async def download_search_result():
3092
+ async with DownloadManager(use_proxy=use_proxy, proxy=proxy, use_stealth=use_stealth) as dm:
3093
+ path = await dm.download_file(file_info, download_dir, url)
3094
+ return path
3095
+
3096
+ path = asyncio.run(download_search_result())
3097
+ if path:
3098
+ with open(path, "rb") as f:
3099
+ file_data = f.read()
3100
+
3101
+ st.download_button(
3102
+ label=f"Save {file_info['filename']}",
3103
+ data=file_data,
3104
+ file_name=file_info['filename'],
3105
+ mime=mimetypes.guess_type(path)[0] or "application/octet-stream",
3106
+ key=f"save_rag_{result['rank']}"
3107
+ )
3108
+ else:
3109
+ st.warning("No matching files found for your query.")
3110
+
3111
+ # Show all files with direct download options
3112
+ st.markdown("### All Discovered Files")
3113
+
3114
+ # Batch download options
3115
  col1, col2 = st.columns([1, 4])
3116
  with col1:
3117
  if st.button("Select All", key="select_all_btn"):
 
3119
  if st.button("Clear Selection", key="clear_selection_btn"):
3120
  st.session_state.selected_files = []
3121
 
3122
+ # Batch download settings
3123
+ if 'selected_files' in st.session_state and st.session_state.selected_files:
3124
+ batch_col1, batch_col2, batch_col3, batch_col4 = st.columns(4)
3125
+ with batch_col1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3126
  download_dir = st.text_input("Download Directory", value="./downloads", key="download_dir_input")
3127
+ with batch_col2:
3128
  create_zip = st.checkbox("Create ZIP file", value=True, key="create_zip_checkbox")
3129
+ with batch_col3:
3130
  delete_after = st.checkbox("Delete after creating ZIP", key="delete_after_checkbox")
3131
+ with batch_col4:
3132
  upload_to_drive = st.checkbox("Upload to Google Drive", key="upload_drive_checkbox")
3133
 
3134
+ if st.button("Download Selected", key="download_batch_btn"):
3135
  if not os.path.exists(download_dir):
3136
  os.makedirs(download_dir)
3137
 
 
3145
  proxy=proxy,
3146
  use_stealth=use_stealth
3147
  ) as dm:
3148
+ for i, idx in enumerate(st.session_state.selected_files):
3149
+ progress = (i + 1) / len(st.session_state.selected_files)
3150
  file_info = files[idx]
3151
+ status_text.text(f"Downloading {file_info['filename']}... ({i+1}/{len(st.session_state.selected_files)})")
3152
  progress_bar.progress(progress)
3153
 
3154
  path = await dm.download_file(file_info, download_dir, url)
 
3199
  except Exception as e:
3200
  st.warning(f"Could not delete {path}: {e}")
3201
  st.info("Deleted original files after ZIP creation")
3202
+
3203
+ # Individual file display with direct download buttons
3204
+ for i, file in enumerate(files):
3205
+ col1, col2, col3 = st.columns([3, 1, 1])
3206
+ with col1:
3207
+ filename = file['filename']
3208
+ size = file['size']
3209
+ meta = file.get('metadata', {})
3210
+ file_info = f"{filename} ({size})"
3211
+ if meta and 'Pages' in meta:
3212
+ file_info += f" - {meta.get('Pages', '')} pages"
3213
+ st.markdown(f"**{i+1}. {file_info}**")
3214
+
3215
+ with col2:
3216
+ # Add direct download button for each file
3217
+ if st.button(f"Download", key=f"direct_dl_{i}"):
3218
+ with st.spinner(f"Downloading {filename}..."):
3219
+ async def download_single_file():
3220
+ async with DownloadManager(use_proxy=use_proxy, proxy=proxy, use_stealth=use_stealth) as dm:
3221
+ path = await dm.download_file(file, download_dir, url)
3222
+ return path
3223
+
3224
+ downloaded_path = asyncio.run(download_single_file())
3225
+ if downloaded_path:
3226
+ with open(downloaded_path, "rb") as f:
3227
  file_data = f.read()
3228
 
 
 
 
3229
  st.download_button(
3230
+ label=f"Save {filename}",
3231
  data=file_data,
3232
+ file_name=filename,
3233
+ mime=mimetypes.guess_type(downloaded_path)[0] or "application/octet-stream",
3234
+ key=f"save_file_{i}"
3235
  )
3236
+
3237
+ with col3:
3238
+ # Add to selection for batch download
3239
+ if i in st.session_state.selected_files:
3240
+ if st.button("Unselect", key=f"unselect_{i}"):
3241
+ st.session_state.selected_files.remove(i)
3242
+ else:
3243
+ if st.button("Select", key=f"select_{i}"):
3244
+ st.session_state.selected_files.append(i)
3245
 
3246
  elif mode == "Bing Search":
3247
  st.header("Bing Search Mode")
 
3307
  st.session_state.discovered_files = files
3308
  st.session_state.current_url = url
3309
  st.success(f"Found {len(files)} files!")
3310
+
3311
+ # Show files with direct download options
3312
+ download_dir = "./downloads"
3313
+ os.makedirs(download_dir, exist_ok=True)
3314
+
3315
+ for i, file in enumerate(files):
3316
+ col1, col2, col3 = st.columns([3, 1, 1])
3317
+ with col1:
3318
+ filename = file['filename']
3319
+ size = file['size']
3320
+ meta = file.get('metadata', {})
3321
+ file_info = f"{filename} ({size})"
3322
+ if meta and 'Pages' in meta:
3323
+ file_info += f" - {meta.get('Pages', '')} pages"
3324
+ st.markdown(f"**{i+1}. {file_info}**")
3325
+
3326
+ with col2:
3327
+ # Add direct download button for each file
3328
+ if st.button(f"Download", key=f"direct_dl_{i}"):
3329
+ with st.spinner(f"Downloading {filename}..."):
3330
+ async def download_single_file():
3331
+ async with DownloadManager(use_proxy=use_proxy, proxy=proxy, use_stealth=use_stealth) as dm:
3332
+ path = await dm.download_file(file, download_dir, url)
3333
+ return path
3334
+
3335
+ downloaded_path = asyncio.run(download_single_file())
3336
+ if downloaded_path:
3337
+ with open(downloaded_path, "rb") as f:
3338
+ file_data = f.read()
3339
+
3340
+ st.download_button(
3341
+ label=f"Save {filename}",
3342
+ data=file_data,
3343
+ file_name=filename,
3344
+ mime=mimetypes.guess_type(downloaded_path)[0] or "application/octet-stream",
3345
+ key=f"save_file_{i}"
3346
+ )
3347
+
3348
+ with col3:
3349
+ # Add to selection for batch download
3350
+ if i in st.session_state.selected_files:
3351
+ if st.button("Unselect", key=f"unselect_{i}"):
3352
+ st.session_state.selected_files.remove(i)
3353
+ else:
3354
+ if st.button("Select", key=f"select_{i}"):
3355
+ st.session_state.selected_files.append(i)
3356
  else:
3357
  st.warning("No files found.")
3358
 
 
3400
 
3401
  # Add footer with attribution
3402
  st.markdown('---')
3403
+ st.markdown('Created by [Euler314](https://github.com/yu314coder)')
3404
 
3405
  if __name__ == "__main__":
3406
  main()