diff --git a/async_web_fetcher.py b/async_web_fetcher.py index ae9c489..d82e397 100644 --- a/async_web_fetcher.py +++ b/async_web_fetcher.py @@ -44,10 +44,72 @@ IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '. VIDEO_EXTENSIONS = {'.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v', '.ogv', '.flv', '.wmv'} AUDIO_EXTENSIONS = {'.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac', '.wma'} +# Code file extensions +CODE_EXTENSIONS = { + '.py', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', # Python, JavaScript, TypeScript + '.rs', '.go', '.rb', '.php', '.pl', '.pm', # Rust, Go, Ruby, PHP, Perl + '.java', '.kt', '.kts', '.scala', '.groovy', # JVM languages + '.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', # C/C++ + '.cs', '.fs', '.fsx', # .NET + '.swift', '.m', '.mm', # Apple + '.lua', '.r', '.R', '.jl', # Lua, R, Julia + '.sh', '.bash', '.zsh', '.fish', '.ps1', # Shell + '.sql', '.graphql', '.gql', # Query languages + '.yaml', '.yml', '.toml', '.json', '.xml', # Config + '.md', '.rst', '.txt', # Docs + '.zig', '.nim', '.d', '.v', # Modern systems langs + '.ex', '.exs', '.erl', '.hrl', # Erlang/Elixir + '.clj', '.cljs', '.cljc', '.edn', # Clojure + '.hs', '.lhs', # Haskell + '.ml', '.mli', '.re', '.rei', # OCaml/ReasonML + '.lisp', '.cl', '.el', '.scm', '.rkt', # Lisps + '.f90', '.f95', '.f03', '.for', # Fortran + '.asm', '.s', # Assembly + '.cob', '.cbl', # COBOL + '.pro', # Prolog + '.tcl', # Tcl + '.dart', # Dart + '.raku', '.p6', # Raku + '.cr', # Crystal + '.vue', '.svelte', # Frontend frameworks + '.tf', '.hcl', # Terraform + '.dockerfile', '.makefile', # Build files +} + +# Font file extensions +FONT_EXTENSIONS = {'.woff', '.woff2', '.ttf', '.otf', '.eot', '.sfnt'} + +# Style file extensions +STYLE_EXTENSIONS = {'.css', '.scss', '.sass', '.less', '.styl'} + # MIME types by media type IMAGE_MIME_PREFIXES = ('image/',) VIDEO_MIME_PREFIXES = ('video/',) AUDIO_MIME_PREFIXES = ('audio/',) +CODE_MIME_TYPES = { + 'text/x-python', 'application/x-python', 'text/x-python-script', + 'text/javascript', 'application/javascript', 'application/x-javascript', + 'text/typescript', 'application/typescript', + 'text/x-rust', 'text/x-go', 'text/x-ruby', 'application/x-ruby', + 'text/x-java-source', 'text/x-kotlin', 'text/x-scala', + 'text/x-c', 'text/x-c++', 'text/x-csrc', 'text/x-c++src', + 'text/x-csharp', 'text/x-fsharp', + 'text/x-swift', 'text/x-objective-c', + 'text/x-lua', 'text/x-r', 'text/x-julia', + 'text/x-shellscript', 'application/x-sh', 'text/x-bash', + 'application/sql', 'application/graphql', + 'application/json', 'application/xml', 'text/xml', + 'text/yaml', 'application/x-yaml', 'text/x-yaml', + 'text/markdown', 'text/x-markdown', + 'text/plain', # Often used for code +} +FONT_MIME_TYPES = { + 'font/woff', 'font/woff2', 'font/ttf', 'font/otf', 'font/sfnt', + 'application/font-woff', 'application/font-woff2', + 'application/x-font-ttf', 'application/x-font-otf', + 'application/vnd.ms-fontobject', +} +STYLE_MIME_TYPES = {'text/css', 'text/x-scss', 'text/x-sass', 'text/x-less'} @dataclass @@ -376,13 +438,14 @@ def get_media_type_from_extension(url: str) -> Optional[str]: Determine media type from URL extension. Returns: - 'image', 'video', 'audio', or None + 'image', 'video', 'audio', 'code', 'font', 'style', or None """ parsed = Uri(url) if not parsed.path: return None path = parsed.path.lower() + # Check for extension match for ext in IMAGE_EXTENSIONS: if path.endswith(ext): return 'image' @@ -392,6 +455,15 @@ def get_media_type_from_extension(url: str) -> Optional[str]: for ext in AUDIO_EXTENSIONS: if path.endswith(ext): return 'audio' + for ext in CODE_EXTENSIONS: + if path.endswith(ext): + return 'code' + for ext in FONT_EXTENSIONS: + if path.endswith(ext): + return 'font' + for ext in STYLE_EXTENSIONS: + if path.endswith(ext): + return 'style' return None @@ -400,18 +472,26 @@ def get_media_type_from_mime(mime_type: str) -> Optional[str]: Determine media type from MIME type. Returns: - 'image', 'video', 'audio', or None + 'image', 'video', 'audio', 'code', 'font', 'style', or None """ if not mime_type: return None mime_lower = mime_type.lower() + # Check prefixes first if mime_lower.startswith(IMAGE_MIME_PREFIXES): return 'image' if mime_lower.startswith(VIDEO_MIME_PREFIXES): return 'video' if mime_lower.startswith(AUDIO_MIME_PREFIXES): return 'audio' + # Check exact matches for code/font/style + if mime_lower in CODE_MIME_TYPES: + return 'code' + if mime_lower in FONT_MIME_TYPES: + return 'font' + if mime_lower in STYLE_MIME_TYPES: + return 'style' return None @@ -710,16 +790,48 @@ def extract_media_from_html(html: str, base_url: str, mode: CrawlMode = CrawlMod if src: add_media(src, 'audio', title=title) - # Extract from pointing to media files + # Extract from pointing to media/code/font/style files for a in soup.find_all('a', href=True): href = a['href'] media_type = get_media_type_from_extension(href) if media_type: if (media_type == 'image' and collect_images) or \ (media_type == 'video' and collect_videos) or \ - (media_type == 'audio' and collect_audio): + (media_type == 'audio' and collect_audio) or \ + media_type in ('code', 'font', 'style'): # Always collect code/font/style add_media(href, media_type, alt_text=a.get_text(strip=True)[:100]) + # Extract from tags for stylesheets and fonts + for link in soup.find_all('link', href=True): + href = link.get('href') + rel = link.get('rel', []) + as_attr = link.get('as', '') + + if 'stylesheet' in rel: + add_media(href, 'style', alt_text='stylesheet') + elif 'preload' in rel and as_attr == 'font': + add_media(href, 'font', alt_text='preload font') + elif 'preload' in rel and as_attr == 'style': + add_media(href, 'style', alt_text='preload style') + else: + # Check by extension + media_type = get_media_type_from_extension(href) + if media_type in ('font', 'style'): + add_media(href, media_type, alt_text=f'link {media_type}') + + # Extract from - - """ +@app.get("/sandbox", response_class=HTMLResponse) +async def sandbox_page(lang: str = Cookie(None), accept_language: str = Header(None)): + """Sandbox page for uploading archives.""" + if not SANDBOX_MODE: + raise HTTPException(status_code=403, detail="Sandbox mode not enabled (NEOPIG_SANDBOX=1)") + language = get_lang(lang, accept_language) + html = layout("Sandbox", SANDBOX_CONTENT, extra_css=SANDBOX_CSS) + return inject_i18n(html, language) + + # ============================================================================ # Crawler API # ============================================================================