| import os | |
| import re | |
| from urllib.request import urlopen, Request | |
| from urllib.error import URLError | |
| from bs4 import BeautifulSoup | |
| SPEC_DIR = os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| "specifications", | |
| ) | |
| ROOT_URL = "https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/" | |
| # Targets to download | |
| TARGETS = ['BAP', 'CAP', 'TMAP', 'HAP', 'PBP', 'MCP', 'CCP', 'MICP', 'VCP', 'ASCS', 'PACS', 'CSIS', 'CSIP', 'BASS', 'AICS'] | |
| def get_all_index_links(): | |
| print(f"Fetching directory listing from {ROOT_URL}...") | |
| headers = {'User-Agent': 'Mozilla/5.0'} | |
| try: | |
| req = Request(ROOT_URL, headers=headers) | |
| with urlopen(req, timeout=30) as response: | |
| html_content = response.read() | |
| soup = BeautifulSoup(html_content, 'html.parser') | |
| links = [] | |
| for a in soup.find_all('a'): | |
| href = a.get('href', '') | |
| if "index-en.html" in href: | |
| links.append(href) | |
| return links | |
| except URLError as e: | |
| print(f"Error fetching root directory: {e}") | |
| return [] | |
| def match_target(path, acronym): | |
| pattern = re.compile(rf"(?:^|/|_|-)({acronym})(?:[._-]|html5)", re.IGNORECASE) | |
| return bool(pattern.search(path)) | |
| def parse_version(path, acronym): | |
| pattern = re.compile(rf"{acronym}_v(\d+(?:[.-]\d+)*)", re.IGNORECASE) | |
| match = pattern.search(path) | |
| if match: | |
| return match.group(1).replace('-', '.') | |
| return "0.0" | |
| def get_latest_link(links, acronym): | |
| target_links = [l for l in links if match_target(l, acronym)] | |
| if not target_links: | |
| return None | |
| timestamp_pattern = re.compile(r"_(\d{10})/") | |
| def sort_key(path): | |
| version_str = parse_version(path, acronym) | |
| version_parts = [] | |
| for x in version_str.split('.'): | |
| try: | |
| version_parts.append(int(x)) | |
| except ValueError: | |
| version_parts.append(0) | |
| timestamp_match = timestamp_pattern.search(path) | |
| timestamp = int(timestamp_match.group(1)) if timestamp_match else 0 | |
| return (version_parts, timestamp) | |
| sorted_links = sorted(target_links, key=sort_key, reverse=True) | |
| return sorted_links[0] | |
| def download_spec(name, relative_url): | |
| url = ROOT_URL + relative_url | |
| print(f"Downloading {name} from {url}...") | |
| headers = {'User-Agent': 'Mozilla/5.0'} | |
| try: | |
| req = Request(url, headers=headers) | |
| with urlopen(req, timeout=30) as response: | |
| html_content = response.read() | |
| # Create spec dir if not exists | |
| os.makedirs(SPEC_DIR, exist_ok=True) | |
| # Save HTML | |
| html_path = os.path.join(SPEC_DIR, f"{name}.html") | |
| with open(html_path, 'wb') as f: | |
| f.write(html_content) | |
| print(f"Saved HTML to {html_path}") | |
| # Save Text/Markdown for searching | |
| soup = BeautifulSoup(html_content, 'html.parser') | |
| text_content = soup.get_text(separator='\n', strip=True) | |
| md_path = os.path.join(SPEC_DIR, f"{name}.md") | |
| with open(md_path, 'w', encoding='utf-8') as f: | |
| f.write(text_content) | |
| print(f"Saved Markdown to {md_path}") | |
| except URLError as e: | |
| print(f"Error downloading {name}: {e}") | |
| except Exception as e: | |
| print(f"An error occurred: {e}") | |
| def main(): | |
| links = get_all_index_links() | |
| if not links: | |
| print("No links found.") | |
| return | |
| for target in TARGETS: | |
| latest_link = get_latest_link(links, target) | |
| if latest_link: | |
| version = parse_version(latest_link, target) | |
| name = f"{target}_v{version}" if version != "0.0" else target | |
| if version == "0.0": | |
| match = re.search(rf"(\d+)-{target}-html5", latest_link, re.IGNORECASE) | |
| if match: | |
| name = f"{target}_ID_{match.group(1)}" | |
| download_spec(name, latest_link) | |
| else: | |
| print(f"No link found for {target}") | |
| if __name__ == "__main__": | |
| main() |