sync (2696B)
1 #!/usr/bin/env python 2 3 import json 4 import re 5 import subprocess 6 import sys 7 import xml.etree.ElementTree as ET 8 from pathlib import Path 9 from typing import cast 10 from urllib.request import Request, urlopen 11 from urllib.error import URLError, HTTPError 12 13 FEEDS_FILE = Path('feeds') 14 CACHE_FILE = Path('hack/titles.json') 15 16 MINIFLUX_URL = subprocess.check_output( 17 ['pass', 'show', 'miniflux-url'], text=True 18 ).strip() 19 MINIFLUX_TOKEN = subprocess.check_output( 20 ['pass', 'show', 'miniflux-token'], text=True 21 ).strip() 22 23 cache: dict[str, str] = ( 24 json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {} 25 ) 26 27 28 def fetch_title(url: str) -> str: 29 if url in cache: 30 return cache[url] 31 try: 32 req = Request(url, headers={'User-Agent': 'Mozilla/5.0'}) 33 with urlopen(req, timeout=10) as response: 34 content_type = ( 35 response.headers.get_content_charset() or 'utf-8' 36 ) 37 html = cast(bytes, response.read()).decode( 38 content_type, errors='ignore' 39 ) 40 match = re.search(r'<title>([^<]+)</title>', html, re.IGNORECASE) 41 if match: 42 cache[url] = match.group(1).strip() 43 return cache[url] 44 except Exception: 45 pass 46 cache[url] = '' 47 return '' 48 49 50 root = ET.Element('opml', version='1.0') 51 body = ET.SubElement(root, 'body') 52 outline = body 53 54 for line in FEEDS_FILE.read_text().splitlines(): 55 if not (line := line.strip()): 56 continue 57 58 if line.startswith('@'): 59 name = line.split('<')[0].replace('@', '').strip() 60 outline = ET.SubElement(body, 'outline', text=name, title=name) 61 elif line.startswith('http'): 62 url, _, title = line.partition(' ') 63 url = url.strip() 64 title = title.strip() or fetch_title(url) 65 66 if outline == body: 67 outline = ET.SubElement( 68 body, 'outline', text='Uncategorized', title='Uncategorized' 69 ) 70 71 elem = ET.SubElement(outline, 'outline', type='rss', xmlUrl=url) 72 if title: 73 elem.set('text', title) 74 elem.set('title', title) 75 76 ET.indent(root, space=' ') 77 opml_data = cast(bytes, ET.tostring(root, encoding='utf-8', xml_declaration=True)) 78 79 req = Request(f'{MINIFLUX_URL}/v1/import', data=opml_data, method='POST') 80 req.add_header('X-Auth-Token', MINIFLUX_TOKEN) 81 req.add_header('Content-Type', 'application/xml') 82 83 try: 84 urlopen(req) 85 print('Done') 86 except HTTPError as e: 87 print(f'HTTP Error {e.code}', file=sys.stderr) 88 except URLError as e: 89 print(f'URL Error: {e.reason}', file=sys.stderr) 90 finally: 91 _ = CACHE_FILE.write_text(json.dumps(cache, indent=2, ensure_ascii=False))