├── 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 |
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 |
--------------------------------------------------------------------------------