99import tempfile
1010from pathlib import Path
1111from typing import List , Dict , Optional , Any
12- from urllib . request import urlopen , Request
13- from urllib . error import URLError , HTTPError
12+
13+ import requests
1414
1515from .zotero import print_error , print_info , print_success , print_warning
1616
1717
1818# Default font sources for common themes
19+ # Note: Marianne font from French government requires manual download due to Cloudflare protection.
20+ # Configure custom URL in pyproject.toml if you have a mirror or local copy.
1921DEFAULT_FONT_SOURCES = [
2022 {
21- "name" : "Marianne " ,
22- "url" : "https://www.systeme-de-design.gouv.fr/uploads/Marianne_fd0ba9c190 .zip" ,
23- "description" : "French government official font (Système de Design de l'État) " ,
23+ "name" : "Roboto " ,
24+ "url" : "https://github.com/googlefonts/roboto/releases/download/v2.138/roboto-unhinted .zip" ,
25+ "description" : "Google's sans-serif font family " ,
2426 },
2527 {
2628 "name" : "Roboto Mono" ,
27- "url" : "https://fonts.google. com/download?family=Roboto+Mono " ,
29+ "url" : "https://github. com/googlefonts/RobotoMono/archive/refs/heads/main.zip " ,
2830 "description" : "Google's monospace font, good for code" ,
2931 },
3032]
3133
34+ # Additional font sources that may require special handling
35+ OPTIONAL_FONT_SOURCES = {
36+ "Marianne" : {
37+ "name" : "Marianne" ,
38+ "url" : "https://www.info.gouv.fr/upload/media/content/0001/14/e9dd2398914853b3c21c402245866bc74cd3d3c5.zip" ,
39+ "description" : "French government official font - TTF version (Système de Design de l'État)" ,
40+ "note" : "May require manual download due to Cloudflare protection" ,
41+ },
42+ }
43+
3244
3345class FontInstaller :
3446 """Handles downloading and installing fonts for LaTeX projects"""
@@ -163,25 +175,30 @@ def _download_file(self, url: str, dest: Path) -> None:
163175 url: URL to download
164176 dest: Destination path
165177 """
166- # Create request with user agent to avoid blocks
167- headers = {"User-Agent" : "Mozilla/5.0 (compatible; article-cli font installer)" }
168- request = Request (url , headers = headers )
178+ # Use requests library for better redirect and header handling
179+ headers = {
180+ "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ,
181+ "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ,
182+ "Accept-Language" : "en-US,en;q=0.5" ,
183+ }
169184
170185 try :
171- with urlopen (request , timeout = 60 ) as response :
172- # Get file size if available
173- content_length = response .headers .get ("Content-Length" )
174- total_size = int (content_length ) if content_length else None
175-
176- # Download in chunks
177- chunk_size = 8192
178- downloaded = 0
179-
180- with open (dest , "wb" ) as f :
181- while True :
182- chunk = response .read (chunk_size )
183- if not chunk :
184- break
186+ response = requests .get (
187+ url , headers = headers , allow_redirects = True , stream = True , timeout = 60
188+ )
189+ response .raise_for_status ()
190+
191+ # Get file size if available
192+ content_length = response .headers .get ("Content-Length" )
193+ total_size = int (content_length ) if content_length else None
194+
195+ # Download in chunks
196+ chunk_size = 8192
197+ downloaded = 0
198+
199+ with open (dest , "wb" ) as f :
200+ for chunk in response .iter_content (chunk_size = chunk_size ):
201+ if chunk :
185202 f .write (chunk )
186203 downloaded += len (chunk )
187204
@@ -200,14 +217,16 @@ def _download_file(self, url: str, dest: Path) -> None:
200217 flush = True ,
201218 )
202219
203- print () # New line after progress
220+ print () # New line after progress
204221
205- except HTTPError as e :
206- raise RuntimeError (f"HTTP error { e . code } : { e . reason } " )
207- except URLError as e :
208- raise RuntimeError (f"URL error: { e . reason } " )
209- except TimeoutError :
222+ except requests . exceptions . HTTPError as e :
223+ raise RuntimeError (f"HTTP error: { e } " )
224+ except requests . exceptions . ConnectionError as e :
225+ raise RuntimeError (f"Connection error: { e } " )
226+ except requests . exceptions . Timeout :
210227 raise RuntimeError ("Download timed out" )
228+ except requests .exceptions .RequestException as e :
229+ raise RuntimeError (f"Download failed: { e } " )
211230
212231 def _extract_zip (self , zip_path : Path , target_dir : Path ) -> None :
213232 """
0 commit comments