├── config.json ├── install.sh ├── README.md └── reddit_notebook.py /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reddit": { 3 | "client_id": "", /* Your Reddit API client ID */ 4 | "client_secret": "", /* Your Reddit API client secret */ 5 | "user_agent": "", /* Your app’s user agent string */ 6 | "comment_age_threshold_days": 30 /* Ignore comments from accounts younger than this */ 7 | }, 8 | 9 | "subreddits": [ 10 | "worldnews", 11 | "technews" 12 | ], 13 | 14 | "default": { 15 | "hours": 8, 16 | "top_posts": 10 17 | }, 18 | 19 | "output": { 20 | "local_dir": "./output", 21 | "include_comments": true 22 | }, 23 | 24 | "drive": { 25 | "enabled": false, 26 | "credentials_file": "", 27 | "folder_name": "" 28 | }, 29 | 30 | "urls": { 31 | "stocks": [ 32 | "https://finance.yahoo.com/quote/AMD/" 33 | ], 34 | "commodities": [ 35 | "https://finance.yahoo.com/quote/GC=F/" 36 | ], 37 | "fx": [ 38 | "https://finance.yahoo.com/quote/AUDUSD=X/" 39 | ], 40 | "weather": "https://www.accuweather.com/en/gb/london/ec4a-2/weather-forecast/328328" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # One-line installer for reddit-digest 5 | # Usage: bash <(curl -s https://raw.githubusercontent.com/farsonic/reddit-digest/main/install.sh) 6 | 7 | # 1️⃣ Clone (or pull) the repo 8 | if [ -d "reddit-digest" ]; then 9 | echo "Updating existing reddit-digest directory..." 10 | cd reddit-digest && git pull origin main 11 | else 12 | echo "Cloning reddit-digest..." 13 | git clone https://github.com/farsonic/reddit-digest.git 14 | cd reddit-digest 15 | fi 16 | 17 | # 2️⃣ Create a Python virtual environment 18 | echo "Creating virtual environment..." 19 | python3 -m venv venv 20 | # shellcheck disable=SC1091 21 | source venv/bin/activate 22 | 23 | # 3️⃣ Upgrade pip & install dependencies 24 | echo "Installing Python dependencies..." 25 | pip install --upgrade pip 26 | 27 | # Core Reddit + Google APIs 28 | pip install \ 29 | praw \ 30 | requests \ 31 | google-api-python-client \ 32 | google-auth \ 33 | google-auth-httplib2 \ 34 | google-auth-oauthlib 35 | 36 | # 4️⃣ Remind to configure 37 | cat <](https://buymeacoffee.com/farsonic) 4 | 5 | A Python script to fetch top posts (and optional comments/links) from a Reddit subreddit within a defined time window, render them to Markdown, and optionally convert to a Google Doc and upload to your personal Google Drive. There is also the ability to hardcode specific subreddits in a local config file as well as specific stock, commodities and weather to embed into the Markdown. The expectation is that once the specified groups have been converted to markdown they can be uploaded into NotepadLM for analysis and podcast creation. 6 | 7 | --- 8 | 9 | ## Features 10 | 11 | - Fetch posts from any single or group of public subreddits over the last _N_ hours 12 | - Return either all or top _X_ posts by score, with the score stored in the resulting markdown file. 13 | - Optionally extract top-level comments and any URLs within them, while also ignoring posted with new accounts of a specific age 14 | - Generate a timestamped Markdown report locally to a nominated directory 15 | - Embed specific stocks, commodites and local weather URL's into the markdown text. 16 | - Optionally convert the Markdown to a Google Doc 17 | - Optionally upload the Doc into a specified folder in your personal Drive 18 | 19 | --- 20 | 21 | ## Repository Layout 22 | 23 | ``` 24 | reddit-digest/ 25 | ├── config.json # Reddit + Drive configuration 26 | ├── gdrive-creds.json # OAuth client JSON from Google 27 | ├── token.json # Cached OAuth tokens (auto-generated) 28 | ├── reddit-notepad.py # Main Python script 29 | └── install.sh # Installer script 30 | ``` 31 | 32 | --- 33 | 34 | ## Prerequisites 35 | 36 | 1. **Python & pip** 37 | - Python 3.8+ (python3.12-venv) 38 | - `pip3` installed 39 | 40 | 2. **Google Cloud Project Setup (Optional)** 41 | 1. **Enable APIs** 42 | - Go to **APIs & Services → Library**, search for _Google Drive API_ and _Google Docs API_, and click **Enable** on each. 43 | 2. **Configure OAuth consent screen** 44 | - In **APIs & Services → OAuth consent screen**, choose **External**. 45 | - Fill in **App name** (e.g. “Reddit Notepad Uploader”) and **User support email**. 46 | - Under **Scopes**, add: 47 | ``` 48 | https://www.googleapis.com/auth/drive.file 49 | https://www.googleapis.com/auth/documents 50 | ``` 51 | - Under **Test users**, add your Google email, then **Publish**. 52 | 3. **Create OAuth Client ID** 53 | - In **APIs & Services → Credentials**, click **Create Credentials → OAuth client ID**. 54 | - Choose **Desktop app**, name it, and download the JSON as `gdrive-creds.json`. 55 | 56 | 3. **Others** (for installer) 57 | - git 58 | - curl 59 | - 60 | 61 | --- 62 | 63 | ## config.json 64 | 65 | Create a `config.json` in the root of this repo with **your** Reddit & Drive settings: 66 | 67 | ```json 68 | { 69 | "reddit": { 70 | "client_id": "", /* Your Reddit API client ID */ 71 | "client_secret": "", /* Your Reddit API client secret */ 72 | "user_agent": "", /* Your app’s user agent string */ 73 | "comment_age_threshold_days": 0 /* Ignore comments from accounts younger than this */ 74 | }, 75 | 76 | "subreddits": [ 77 | /* List your subreddits here, e.g. "worldnews", "tech" */ 78 | ], 79 | 80 | "default": { 81 | "hours": 0, /* Look back this many hours */ 82 | "top_posts": 0 /* Number of top posts (0 = all new posts) */ 83 | }, 84 | 85 | "output": { 86 | "local_dir": "", /* Where to save the .md files */ 87 | "include_comments": false /* true to pull comments & links */ 88 | }, 89 | 90 | "drive": { 91 | "enabled": false, /* true to enable Google Drive upload */ 92 | "credentials_file": "", /* Path to your OAuth client JSON */ 93 | "folder_name": "" /* Name of the Drive folder to use */ 94 | }, 95 | 96 | "urls": { 97 | "stocks": [ 98 | /* e.g. "https://finance.yahoo.com/quote/AMD/" */ 99 | ], 100 | "commodities": [ 101 | /* e.g. "https://finance.yahoo.com/quote/GC=F/" */ 102 | ], 103 | "fx": [ 104 | /* e.g. "https://finance.yahoo.com/quote/AUDUSD=X/" */ 105 | ], 106 | "weather": "" /* e.g. "https://www.accuweather.com/…" */ 107 | } 108 | } 109 | ``` 110 | 111 | --- 112 | 113 | ## Python Dependencies 114 | 115 | Install via pip: 116 | 117 | ```bash 118 | pip3 install praw \ 119 | google-api-python-client \ 120 | google-auth \ 121 | google-auth-httplib2 \ 122 | google-auth-oauthlib 123 | ``` 124 | 125 | --- 126 | 127 | ## Usage 128 | 129 | ### 1. Interactive Mode 130 | 131 | ```bash 132 | python3 reddit-notepad.py 133 | ``` 134 | 135 | On launch you’ll be prompted for: 136 | 137 | - **Subreddit** (e.g. `worldnews`) 138 | - **Hours back** (e.g. `24`) 139 | - **Top posts** (`0` = all, or e.g. `10`) 140 | - **Fetch comments & links?** (`y` or `n`) 141 | 142 | If Drive upload is enabled in your `config.json`, the first time you choose to upload, a browser window will open to authorize. A `token.json` file will then be saved for subsequent runs. 143 | 144 | --- 145 | 146 | ### 2. Fully Non-Interactive (All Flags) 147 | 148 | You can override every prompt via flags: 149 | 150 | ```bash 151 | python3 reddit-notepad.py \ 152 | --subreddits worldnews technews amiga \ 153 | --hours 12 \ 154 | --topn 20 \ 155 | --comments \ 156 | --debug 157 | ``` 158 | 159 | This will: 160 | 161 | - Fetch the top 20 posts from **worldnews**, **technews**, and **amiga**, from the last 12 hours 162 | - Include comments & links 163 | - Emit debug logging to stderr 164 | - Use your Drive settings from `config.json` to upload (unless you pass `--no-drive`) 165 | 166 | --- 167 | 168 | ### 3. Examples 169 | 170 | #### a) Just grab the last 5 new posts from r/networking, no comments, no Drive 171 | 172 | ```bash 173 | python3 reddit-notepad.py \ 174 | --subreddits networking \ 175 | --hours 24 \ 176 | --topn 5 \ 177 | --no-drive 178 | ``` 179 | 180 | #### b) Scan multiple subreddits, include comments, upload to Drive 181 | 182 | ```bash 183 | python3 reddit-notepad.py \ 184 | -s worldnews networking technews \ 185 | -H 6 \ 186 | -n 10 \ 187 | -c 188 | ``` 189 | 190 | #### c) Debug run for troubleshooting 191 | 192 | ```bash 193 | python3 reddit-notepad.py --debug 194 | ``` 195 | 196 | You’ll see detailed logging of API calls, folder creation, etc. 197 | 198 | #### d) Use defaults from `config.json` but disable comments 199 | 200 | ```bash 201 | python3 reddit-notepad.py --no-drive 202 | ``` 203 | 204 | *(This will still run against the subreddits, hours, and top_posts defined under `"default"` in your config, but skip both comments and Drive upload.)* 205 | 206 | --- 207 | 208 | ### 4. Output 209 | 210 | 1. **Local Markdown** 211 | Saved to the `local_dir` you set in `config.json` (default `./output/`). 212 | 213 | 2. **Google Doc** (if enabled) 214 | Placed in **My Drive → _folder_name_** in your account, then in a subfolder named today’s date. 215 | 216 | ## Installer Script 217 | 218 | Install in one line: 219 | 220 | ```bash 221 | bash <(curl -s https://raw.githubusercontent.com/farsonic/reddit-digest/main/install.sh) 222 | ``` 223 | 224 | This will clone or update the repo, create a virtual environment, install all Python deps, and prompt you to configure `config.json` & place `gdrive-creds.json`. 225 | 226 | --- 227 | 228 | ### 5. Create Reddit API Credentials. 229 | 230 | 1. ** Create API Key 231 | 232 | From https://reddit.com/prefs/apps create a personal use script using the following as an example. use the personal use script key for the client_id and the secret for client_secret in the config.json file. 233 | 234 | Screenshot 2025-07-01 at 2 32 20 pm 235 | 236 | ### 6. NotebookLM ingest 237 | 238 | You will now have the ability to create a markdown file that will be held locally as well as stored in google drive if requested. You can now take this and load into NotebookLM for analysis. If using this for creating a podcast this is my current working prompt to cusotmise the output. 239 | 240 | ``` 241 | You are a production-grade podcast writer. Your input is: 242 | • A list of stock URLs use 243 | • A list of commodity URLs 244 | • A list of FX URLs 245 | • One weather forecast URL 246 | • A set of Reddit post URLs grouped by community 247 | • Read all the URL links provided and use these for analysis of all components. 248 | • Extract every link from the post, perform deep research on the articles linked and use this as the basis for discussion. The article content is primary concern followed by the comments and sentiment. 249 | 250 | 251 | Your job is to output a 10-minute podcast in four parts: 252 | 253 | 1️⃣ **Quick Markets & Weather (≈30 s)** 254 | - State each stock name and say it is trading at the provided amount and the trend for the day 255 | - Do the same for commodities and FX. 256 | - Read the weather URL once: “Today's weather is..." 257 | 258 | 2️⃣ **Headline Segments** 259 | For each group (World News → Tech News → Niche Communities → Hobbies): 260 | - **Segment title** 261 | - **Bullet 1:** “Top story at URL: …” + a one‐sentence **fact** summary pulled **only** from the article title or first line. 262 | - **Bullet 2:** Community sentiment (positive/negative/mixed) based on upvotes & comments count—do **not** dive into the comments themselves here. 263 | - **Bullet 3 (only if AMD mentioned):** Name the URL and say “AMD mention detected here.” 264 | - **Pick up to two standout user comments (accounts ≥ 30 days old) that illustrate community reaction briefly—one sentence each. 265 | 266 | 4️⃣ **Outro Teaser** 267 | - One sentence: “See you tomorrow, when we’ll cover…” 268 | 269 | **Formatting & Tone** 270 | - Never say hashtag anything 271 | - Reference the site where the story comes from. 272 | - Use spoken-audio phrasing (“Up first…,” “Listeners are reacting…”). 273 | - Get immediately to the point of discussion within 5 seconds. 274 | - Keep each bullet super-tight. 275 | - Script length: ~10 minutes. 276 | ``` 277 | 278 | ## License 279 | 280 | MIT © Your Name 281 | -------------------------------------------------------------------------------- /reddit_notebook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import json 5 | import re 6 | import time 7 | import datetime 8 | import argparse 9 | import requests 10 | import praw 11 | from datetime import timezone 12 | from prawcore import ResponseException, Redirect, TooManyRequests 13 | 14 | # Alpha Vantage for stocks 15 | from alpha_vantage.timeseries import TimeSeries 16 | 17 | # Google APIs 18 | from google_auth_oauthlib.flow import InstalledAppFlow 19 | from googleapiclient.discovery import build 20 | from googleapiclient.http import MediaFileUpload 21 | from google.auth.transport.requests import Request 22 | from google.oauth2.credentials import Credentials 23 | 24 | # ── GoldAPI endpoints ─────────────────────────────────────────────── 25 | GOLDAPI_URLS = { 26 | "Gold": "https://www.goldapi.io/api/XAU/USD", 27 | "Silver": "https://www.goldapi.io/api/XAG/USD", 28 | } 29 | 30 | # ── Load configuration ─────────────────────────────────────────────── 31 | with open("config.json", "r", encoding="utf-8") as f: 32 | cfg = json.load(f) 33 | 34 | # ── Persistent author cache ───────────────────────────────────────── 35 | CACHE_PATH = "author_cache.json" 36 | if os.path.exists(CACHE_PATH): 37 | with open(CACHE_PATH, "r", encoding="utf-8") as cf: 38 | author_cache = json.load(cf) 39 | else: 40 | author_cache = {} 41 | 42 | # ── Read thresholds & defaults ─────────────────────────────────────── 43 | comment_age_thresh = cfg.get("comment_age_threshold_days", 0) 44 | default_subs = cfg.get("subreddits", []) 45 | default_hours = cfg.get("default", {}).get("hours", 24) 46 | default_topn = cfg.get("default", {}).get("top_posts", 0) 47 | default_comments = cfg.get("output", {}).get("include_comments", False) 48 | 49 | # ── Stocks config ──────────────────────────────────────────────────── 50 | stocks_cfg = cfg.get("stocks", {}) 51 | stocks_enabled = stocks_cfg.get("enabled", False) 52 | stock_symbols = stocks_cfg.get("symbols", []) 53 | av_cfg = stocks_cfg.get("alpha_vantage", {}) 54 | av_api_key = av_cfg.get("api_key", "") 55 | 56 | # ── Commodities config ─────────────────────────────────────────────── 57 | comms_cfg = cfg.get("commodities", {}) 58 | comms_enabled = comms_cfg.get("enabled", False) 59 | commodity_items = comms_cfg.get("items", []) 60 | goldapi_cfg = comms_cfg.get("goldapi", {}) 61 | goldapi_token = goldapi_cfg.get("access_token", "") 62 | 63 | # ── Weather config ────────────────────────────────────────────────── 64 | weather_cfg = cfg.get("weather", {}) 65 | weather_enabled = weather_cfg.get("enabled", False) 66 | wa_api_key = weather_cfg.get("api_key", "") 67 | loc = weather_cfg.get("location", {}) 68 | units = weather_cfg.get("units", "metric") 69 | 70 | # ── CLI args ───────────────────────────────────────────────────────── 71 | parser = argparse.ArgumentParser( 72 | description="Fetch Reddit posts, market data, weather, and optionally upload to Google Docs." 73 | ) 74 | parser.add_argument("-s", "--subreddits", nargs="+", 75 | help="Override subreddits in config") 76 | parser.add_argument("-H", "--hours", type=int, default=default_hours, 77 | help="Hours to look back") 78 | parser.add_argument("-n", "--topn", type=int, default=default_topn, 79 | help="How many top posts (0=all)") 80 | parser.add_argument("-c", "--comments", action="store_true", 81 | default=default_comments, help="Include comments & links") 82 | parser.add_argument("--no-drive", dest="drive", action="store_false", 83 | help="Disable Google Drive upload") 84 | args = parser.parse_args() 85 | 86 | subs = args.subreddits if args.subreddits else default_subs 87 | if not subs: 88 | print("ERROR: No subreddits specified.", file=sys.stderr) 89 | sys.exit(1) 90 | 91 | # ── Reddit client setup ────────────────────────────────────────────── 92 | rd = cfg["reddit"] 93 | reddit = praw.Reddit( 94 | client_id=rd["client_id"], 95 | client_secret=rd["client_secret"], 96 | user_agent=rd["user_agent"], 97 | check_for_async=False 98 | ) 99 | 100 | # ── Google Drive & Docs setup ──────────────────────────────────────── 101 | drive_service = docs_service = None 102 | if args.drive and cfg.get("drive", {}).get("enabled", False): 103 | SCOPES = ["https://www.googleapis.com/auth/drive.file", 104 | "https://www.googleapis.com/auth/documents"] 105 | token_path = "token.json" 106 | creds = None 107 | if os.path.exists(token_path): 108 | creds = Credentials.from_authorized_user_file(token_path, SCOPES) 109 | if not creds or not creds.valid: 110 | if creds and creds.expired and creds.refresh_token: 111 | creds.refresh(Request()) 112 | else: 113 | flow = InstalledAppFlow.from_client_secrets_file( 114 | cfg["drive"]["credentials_file"], SCOPES) 115 | creds = flow.run_local_server(port=0) 116 | with open(token_path, "w") as tok: 117 | tok.write(creds.to_json()) 118 | drive_service = build("drive", "v3", credentials=creds) 119 | docs_service = build("docs", "v1", credentials=creds) 120 | 121 | # ── Fetch stock prices via Alpha Vantage ───────────────────────────── 122 | def fetch_stock_prices(symbols): 123 | if not (stocks_enabled and av_api_key and symbols): 124 | return {} 125 | ts = TimeSeries(key=av_api_key, output_format="json") 126 | out = {} 127 | for s in symbols: 128 | try: 129 | data, _ = ts.get_quote_endpoint(symbol=s) 130 | out[s] = float(data.get("05. price", 0.0)) 131 | except Exception: 132 | out[s] = None 133 | return out 134 | 135 | # ── Fetch metal prices via GoldAPI ────────────────────────────────── 136 | def fetch_commodity_prices(items): 137 | if not (comms_enabled and goldapi_token and items): 138 | return {} 139 | headers = {"x-access-token": goldapi_token} 140 | out = {} 141 | for name in items: 142 | url = GOLDAPI_URLS.get(name) 143 | if not url: 144 | out[name] = None; continue 145 | try: 146 | r = requests.get(url, headers=headers, timeout=5) 147 | r.raise_for_status() 148 | out[name] = r.json().get("price") 149 | except Exception: 150 | out[name] = None 151 | return out 152 | 153 | # ── Fetch weather via free v2.5 endpoint ──────────────────────────── 154 | def fetch_weather(api_key, lat, lon, units="metric"): 155 | if not (weather_enabled and api_key and lat is not None and lon is not None): 156 | return {} 157 | url = (f"https://api.openweathermap.org/data/2.5/weather" 158 | f"?lat={lat}&lon={lon}&units={units}&appid={api_key}") 159 | r = requests.get(url, timeout=5) 160 | r.raise_for_status() 161 | w = r.json() 162 | return { 163 | "summary": w["weather"][0]["description"].title(), 164 | "temp": w["main"]["temp"], 165 | "humidity": w["main"]["humidity"], 166 | "wind": w["wind"].get("speed") 167 | } 168 | 169 | # ── Google Drive folder helpers ────────────────────────────────────── 170 | def ensure_drive_folder(name: str) -> str: 171 | resp = drive_service.files().list( 172 | q=("mimeType='application/vnd.google-apps.folder' " 173 | f"and name='{name}' and trashed=false"), 174 | fields="files(id,name)" 175 | ).execute() 176 | items = resp.get("files", []) 177 | if items: 178 | return items[0]["id"] 179 | folder = drive_service.files().create( 180 | body={"name": name, "mimeType": "application/vnd.google-apps.folder"}, 181 | fields="id" 182 | ).execute() 183 | return folder["id"] 184 | 185 | def ensure_folder_in_parent(parent_id: str, name: str) -> str: 186 | q = ("mimeType='application/vnd.google-apps.folder' " 187 | f"and name='{name}' and '{parent_id}' in parents and trashed=false") 188 | resp = drive_service.files().list(q=q, fields="files(id,name)").execute() 189 | items = resp.get("files", []) 190 | if items: 191 | return items[0]["id"] 192 | meta = {"name": name, 193 | "mimeType":"application/vnd.google-apps.folder", 194 | "parents":[parent_id]} 195 | folder = drive_service.files().create(body=meta, fields="id").execute() 196 | return folder["id"] 197 | 198 | # ── Reddit fetch ───────────────────────────────────────────────────── 199 | def fetch_posts(sr, hrs, topn): 200 | try: 201 | sub = reddit.subreddit(sr); _ = sub.display_name 202 | except (Redirect, ResponseException) as e: 203 | print(f"ERROR fetching r/{sr}: {e}", file=sys.stderr) 204 | sys.exit(1) 205 | cutoff = (datetime.datetime.now(timezone.utc) 206 | - datetime.timedelta(hours=hrs)).timestamp() 207 | posts, total = [], 0 208 | tf_map = {1:"hour",24:"day",168:"week",720:"month",8760:"year"} 209 | if topn>0 and hrs in tf_map: 210 | for p in sub.top(time_filter=tf_map[hrs], limit=topn): 211 | total += 1; posts.append(p) 212 | return total, posts 213 | for p in sub.new(limit=None): 214 | if p.created_utc < cutoff: 215 | break 216 | total += 1; posts.append(p) 217 | if topn>0: 218 | posts = sorted(posts, key=lambda x: x.score, reverse=True)[:topn] 219 | return total, posts 220 | 221 | # ── Comment extraction with age filter ─────────────────────────────── 222 | def extract_comments(subm): 223 | subm.comments.replace_more(limit=None) 224 | flat = subm.comments.list() 225 | pat = re.compile(r"https?://\S+") 226 | out = [] 227 | for c in flat: 228 | if c.author and hasattr(c.author, "created_utc"): 229 | age = (datetime.datetime.now(timezone.utc) - 230 | datetime.datetime.fromtimestamp(c.author.created_utc, 231 | tz=timezone.utc)).days 232 | else: 233 | age = None 234 | if age is not None and age < comment_age_thresh: 235 | continue 236 | 237 | depth = getattr(c, "depth", 0) 238 | author = c.author.name if c.author else "[deleted]" 239 | acct_iso = author_cache.get(author, "") 240 | if author and author not in author_cache: 241 | try: 242 | acct = reddit.redditor(author) 243 | acct_iso = datetime.datetime.fromtimestamp( 244 | acct.created_utc, tz=timezone.utc).isoformat() 245 | author_cache[author] = acct_iso 246 | time.sleep(0.2) 247 | except TooManyRequests: 248 | author_cache[author] = "" 249 | except: 250 | author_cache[author] = "" 251 | 252 | text = c.body.replace("\n", " ") 253 | links = pat.findall(text) 254 | out.append((depth, author, acct_iso, text, links)) 255 | return out 256 | 257 | # ── Markdown writer ───────────────────────────────────────────────── 258 | def write_markdown(subs, hrs, total_map, posts_map, 259 | grab, stocks, metals, weather, out_dir): 260 | now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 261 | fname = f"reddit_digest_{now}.md" 262 | os.makedirs(out_dir, exist_ok=True) 263 | path = os.path.join(out_dir, fname) 264 | 265 | with open(path, "w", encoding="utf-8") as md: 266 | md.write(f"# Reddit Digest — Last {hrs}h\n\n") 267 | 268 | if weather: 269 | md.write("## Weather\n") 270 | md.write(f"- Condition: {weather['summary']}\n") 271 | md.write(f"- Temp: {weather['temp']}°{'C' if units=='metric' else 'F'}\n") 272 | md.write(f"- Humidity: {weather['humidity']}%\n") 273 | md.write(f"- Wind: {weather['wind']} m/s\n\n") 274 | 275 | if stocks: 276 | md.write("## Stock Prices\n") 277 | for s, p in stocks.items(): 278 | md.write(f"- {s}: {p}\n") 279 | md.write("\n") 280 | 281 | if metals: 282 | md.write("## Commodity Prices\n") 283 | for m, p in metals.items(): 284 | md.write(f"- {m}: {p}\n") 285 | md.write("\n") 286 | 287 | for sr in subs: 288 | total, posts = total_map[sr], posts_map[sr] 289 | md.write(f"---\n## r/{sr} — {len(posts)} of {total} posts\n\n") 290 | for i, p in enumerate(posts, 1): 291 | created = datetime.datetime.fromtimestamp(p.created_utc, tz=timezone.utc) 292 | md.write(f"### {i}. {p.title}\n") 293 | md.write(f"- URL: {p.url}\n") 294 | md.write(f"- Permalink: https://reddit.com{p.permalink}\n") 295 | md.write(f"- Score: {p.score} | Comments: {p.num_comments} | Created (UTC): {created.isoformat()}\n\n") 296 | if grab: 297 | comments = extract_comments(p) 298 | if comments: 299 | md.write("#### Comments\n") 300 | for idx, (depth, author, acct_iso, body, links) in enumerate(comments, 1): 301 | indent = " " * depth 302 | age_str = "" 303 | if acct_iso: 304 | dt = datetime.datetime.fromisoformat(acct_iso) 305 | age_days = (datetime.datetime.now(timezone.utc) - dt).days 306 | age_str = f" (age:{age_days}d)" 307 | md.write(f"{indent}{idx}. **u/{author}**{age_str}: {body}\n") 308 | for l in links: 309 | md.write(f"{indent} - 🔗 {l}\n") 310 | md.write("\n") 311 | return path 312 | 313 | def upload_markdown_as_doc(md_path, folder_id): 314 | text = open(md_path, "r", encoding="utf-8").read() 315 | doc = docs_service.documents().create( 316 | body={"title": os.path.basename(md_path).replace(".md","")} 317 | ).execute() 318 | did = doc["documentId"] 319 | docs_service.documents().batchUpdate( 320 | documentId=did, 321 | body={"requests":[{"insertText":{"location":{"index":1},"text":text}}]} 322 | ).execute() 323 | drive_service.files().update( 324 | fileId=did, addParents=folder_id, fields="id,parents" 325 | ).execute() 326 | print(f"Created Google Doc: https://docs.google.com/document/d/{did}/edit") 327 | 328 | # ── Main ───────────────────────────────────────────────────────────── 329 | def main(): 330 | stock_data = fetch_stock_prices(stock_symbols) 331 | commodity_data = fetch_commodity_prices(commodity_items) 332 | weather_data = fetch_weather(wa_api_key, 333 | loc.get("lat"), 334 | loc.get("lon"), 335 | units) 336 | 337 | total_map, post_map = {}, {} 338 | for sr in subs: 339 | t, p = fetch_posts(sr, args.hours, args.topn) 340 | total_map[sr] = t 341 | post_map[sr] = p 342 | 343 | out_dir = cfg["output"]["local_dir"] 344 | md_path = write_markdown(subs, args.hours, 345 | total_map, post_map, 346 | args.comments, 347 | stock_data, 348 | commodity_data, 349 | weather_data, 350 | out_dir) 351 | print(f"Saved markdown to {md_path}") 352 | 353 | with open(CACHE_PATH, "w", encoding="utf-8") as cf: 354 | json.dump(author_cache, cf) 355 | 356 | if args.drive and cfg.get("drive", {}).get("enabled", False): 357 | parent_id = ensure_drive_folder(cfg["drive"]["folder_name"]) 358 | today = datetime.datetime.now().strftime("%Y-%m-%d") 359 | folder_id = ensure_folder_in_parent(parent_id, today) 360 | upload_markdown_as_doc(md_path, folder_id) 361 | 362 | if __name__ == "__main__": 363 | main() 364 | --------------------------------------------------------------------------------