Skip to content

Commit 9dec7e9

Browse files
authored
Create daily_report.py
Signed-off-by: nellins <drewnellins@gmail.com>
1 parent 338d225 commit 9dec7e9

1 file changed

Lines changed: 373 additions & 0 deletions

File tree

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Basic Memory Daily Traction Report - Enhanced with Growth Tracking
4+
Automated tracking across GitHub, Reddit, YouTube with daily change indicators
5+
"""
6+
7+
import os
8+
import requests
9+
import json
10+
from datetime import datetime, timedelta
11+
import praw
12+
from googleapiclient.discovery import build
13+
from dateutil import parser
14+
import base64
15+
16+
class BasicMemoryTracker:
17+
def __init__(self):
18+
self.github_token = os.getenv('GITHUB_TOKEN')
19+
self.discord_webhook = os.getenv('DISCORD_WEBHOOK')
20+
self.youtube_api_key = os.getenv('YOUTUBE_API_KEY')
21+
22+
# Reddit setup
23+
self.reddit = praw.Reddit(
24+
client_id=os.getenv('REDDIT_CLIENT_ID'),
25+
client_secret=os.getenv('REDDIT_SECRET'),
26+
user_agent='BasicMemoryTracker:v1.0'
27+
)
28+
29+
# YouTube setup
30+
self.youtube = build('youtube', 'v3', developerKey=self.youtube_api_key)
31+
32+
self.repo_owner = 'basicmachines-co'
33+
self.repo_name = 'basic-memory'
34+
self.youtube_channel = 'basicmachines-co'
35+
self.metrics_file = 'data/daily_metrics.json'
36+
37+
def get_previous_metrics(self):
38+
"""Get yesterday's metrics from GitHub repo storage"""
39+
try:
40+
headers = {'Authorization': f'token {self.github_token}'}
41+
url = f'https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contents/{self.metrics_file}'
42+
response = requests.get(url, headers=headers)
43+
44+
if response.status_code == 200:
45+
file_data = response.json()
46+
content = base64.b64decode(file_data['content']).decode('utf-8')
47+
return json.loads(content)
48+
else:
49+
print("📝 No previous metrics found - this is the first run!")
50+
return {}
51+
except Exception as e:
52+
print(f"⚠️ Could not load previous metrics: {e}")
53+
return {}
54+
55+
def save_current_metrics(self, metrics):
56+
"""Save today's metrics to GitHub repo for tomorrow's comparison"""
57+
try:
58+
headers = {'Authorization': f'token {self.github_token}'}
59+
60+
# Prepare data
61+
metrics_data = {
62+
'date': datetime.now().isoformat(),
63+
'metrics': metrics
64+
}
65+
content = json.dumps(metrics_data, indent=2)
66+
encoded_content = base64.b64encode(content.encode()).decode()
67+
68+
# Check if file exists
69+
file_url = f'https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contents/{self.metrics_file}'
70+
existing_response = requests.get(file_url, headers=headers)
71+
72+
payload = {
73+
'message': f'📊 Daily metrics update - {datetime.now().strftime("%Y-%m-%d")}',
74+
'content': encoded_content
75+
}
76+
77+
if existing_response.status_code == 200:
78+
# File exists, update it
79+
payload['sha'] = existing_response.json()['sha']
80+
response = requests.put(file_url, headers=headers, json=payload)
81+
else:
82+
# File doesn't exist, create it
83+
response = requests.put(file_url, headers=headers, json=payload)
84+
85+
if response.status_code in [200, 201]:
86+
print("✅ Metrics saved for tomorrow's comparison!")
87+
else:
88+
print(f"⚠️ Failed to save metrics: {response.status_code}")
89+
90+
except Exception as e:
91+
print(f"⚠️ Error saving metrics: {e}")
92+
93+
def calculate_change(self, current, previous, key):
94+
"""Calculate the change between current and previous values"""
95+
if not previous or key not in previous:
96+
return 0, "🆕"
97+
98+
change = current - previous[key]
99+
if change > 0:
100+
return change, "📈"
101+
elif change < 0:
102+
return abs(change), "📉"
103+
else:
104+
return 0, "➡️"
105+
106+
def format_change(self, change, direction):
107+
"""Format the change indicator for display"""
108+
if direction == "🆕":
109+
return "🆕"
110+
elif direction == "📈":
111+
return f"(+{change})"
112+
elif direction == "📉":
113+
return f"(-{change})"
114+
else:
115+
return "(±0)"
116+
117+
def get_github_metrics(self):
118+
"""Get GitHub repository metrics"""
119+
try:
120+
headers = {'Authorization': f'token {self.github_token}'}
121+
122+
# Repository stats
123+
repo_url = f'https://api.github.com/repos/{self.repo_owner}/{self.repo_name}'
124+
repo_response = requests.get(repo_url, headers=headers)
125+
repo_data = repo_response.json()
126+
127+
# Traffic stats (requires push access)
128+
traffic_url = f'https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/traffic/views'
129+
traffic_response = requests.get(traffic_url, headers=headers)
130+
traffic_data = traffic_response.json() if traffic_response.status_code == 200 else {}
131+
132+
# Recent issues
133+
issues_url = f'https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/issues'
134+
issues_response = requests.get(issues_url, headers=headers)
135+
issues_data = issues_response.json() if issues_response.status_code == 200 else []
136+
137+
return {
138+
'stars': repo_data.get('stargazers_count', 0),
139+
'forks': repo_data.get('forks_count', 0),
140+
'watchers': repo_data.get('watchers_count', 0),
141+
'open_issues': repo_data.get('open_issues_count', 0),
142+
'traffic_views': traffic_data.get('count', 0),
143+
'traffic_unique': traffic_data.get('uniques', 0),
144+
'recent_issues': len([i for i in issues_data if
145+
parser.parse(i['created_at']).date() >= (datetime.now() - timedelta(days=1)).date()])
146+
}
147+
except Exception as e:
148+
print(f"GitHub API error: {e}")
149+
return {'error': str(e)}
150+
151+
def get_reddit_metrics(self):
152+
"""Get Reddit metrics for Basic Memory mentions"""
153+
try:
154+
metrics = {
155+
'total_mentions': 0,
156+
'subreddit_members': 0,
157+
'top_posts': [],
158+
'hot_discussions': []
159+
}
160+
161+
# Search for Basic Memory mentions
162+
search_results = list(self.reddit.subreddit('all').search(
163+
'Basic Memory', time_filter='day', limit=25
164+
))
165+
metrics['total_mentions'] = len(search_results)
166+
167+
# Get top posts
168+
for post in search_results[:3]:
169+
metrics['top_posts'].append({
170+
'title': post.title[:50] + '...' if len(post.title) > 50 else post.title,
171+
'score': post.score,
172+
'subreddit': post.subreddit.display_name,
173+
'num_comments': post.num_comments
174+
})
175+
176+
# Check r/BasicMemory if it exists
177+
try:
178+
basic_memory_sub = self.reddit.subreddit('BasicMemory')
179+
metrics['subreddit_members'] = basic_memory_sub.subscribers
180+
except:
181+
metrics['subreddit_members'] = 0
182+
183+
return metrics
184+
except Exception as e:
185+
print(f"Reddit API error: {e}")
186+
return {'error': str(e)}
187+
188+
def get_youtube_metrics(self):
189+
"""Get YouTube channel metrics"""
190+
try:
191+
# Get channel statistics
192+
channel_response = self.youtube.channels().list(
193+
part='statistics,snippet',
194+
forUsername=self.youtube_channel
195+
).execute()
196+
197+
if not channel_response['items']:
198+
# Try by channel handle
199+
search_response = self.youtube.search().list(
200+
part='snippet',
201+
q=f'@{self.youtube_channel}',
202+
type='channel',
203+
maxResults=1
204+
).execute()
205+
206+
if search_response['items']:
207+
channel_id = search_response['items'][0]['snippet']['channelId']
208+
channel_response = self.youtube.channels().list(
209+
part='statistics,snippet',
210+
id=channel_id
211+
).execute()
212+
213+
if channel_response['items']:
214+
stats = channel_response['items'][0]['statistics']
215+
return {
216+
'subscribers': int(stats.get('subscriberCount', 0)),
217+
'total_views': int(stats.get('viewCount', 0)),
218+
'video_count': int(stats.get('videoCount', 0))
219+
}
220+
else:
221+
return {'error': 'Channel not found'}
222+
223+
except Exception as e:
224+
print(f"YouTube API error: {e}")
225+
return {'error': str(e)}
226+
227+
def create_discord_embed(self, current_metrics, previous_metrics):
228+
"""Create beautiful Discord embed with all metrics and growth indicators"""
229+
230+
github_data = current_metrics.get('github', {})
231+
reddit_data = current_metrics.get('reddit', {})
232+
youtube_data = current_metrics.get('youtube', {})
233+
234+
prev_github = previous_metrics.get('github', {})
235+
prev_reddit = previous_metrics.get('reddit', {})
236+
prev_youtube = previous_metrics.get('youtube', {})
237+
238+
# Calculate changes
239+
star_change, star_dir = self.calculate_change(github_data.get('stars', 0), prev_github, 'stars')
240+
sub_change, sub_dir = self.calculate_change(youtube_data.get('subscribers', 0), prev_youtube, 'subscribers')
241+
view_change, view_dir = self.calculate_change(youtube_data.get('total_views', 0), prev_youtube, 'total_views')
242+
reddit_change, reddit_dir = self.calculate_change(reddit_data.get('total_mentions', 0), prev_reddit, 'total_mentions')
243+
member_change, member_dir = self.calculate_change(reddit_data.get('subreddit_members', 0), prev_reddit, 'subreddit_members')
244+
245+
# Calculate total reach
246+
total_reach = (
247+
github_data.get('traffic_unique', 0) +
248+
reddit_data.get('total_mentions', 0) * 100 +
249+
youtube_data.get('total_views', 0)
250+
)
251+
252+
embed = {
253+
"title": "🚀 Basic Memory Daily Traction Report",
254+
"description": f"📅 {datetime.now().strftime('%A, %B %d, %Y')}",
255+
"color": 0x00ff88,
256+
"fields": [
257+
{
258+
"name": "⭐ GitHub Metrics",
259+
"value": f"""
260+
**Stars:** {github_data.get('stars', 'N/A')} {star_dir} {self.format_change(star_change, star_dir)}
261+
**Forks:** {github_data.get('forks', 'N/A')} 🍴
262+
**Traffic:** {github_data.get('traffic_unique', 'N/A')} unique visitors 👀
263+
**Issues:** {github_data.get('recent_issues', 0)} new today 🐛
264+
""".strip(),
265+
"inline": True
266+
},
267+
{
268+
"name": "🗨️ Reddit Activity",
269+
"value": f"""
270+
**Mentions:** {reddit_data.get('total_mentions', 'N/A')} {reddit_dir} {self.format_change(reddit_change, reddit_dir)}
271+
**r/BasicMemory:** {reddit_data.get('subreddit_members', 'N/A')} {member_dir} {self.format_change(member_change, member_dir)}
272+
**Hot Posts:** {len(reddit_data.get('top_posts', []))} trending 🔥
273+
""".strip(),
274+
"inline": True
275+
},
276+
{
277+
"name": "📺 YouTube Stats",
278+
"value": f"""
279+
**Subscribers:** {youtube_data.get('subscribers', 'N/A')} {sub_dir} {self.format_change(sub_change, sub_dir)}
280+
**Total Views:** {youtube_data.get('total_views', 'N/A'):,} {view_dir} {self.format_change(view_change, view_dir)}
281+
**Videos:** {youtube_data.get('video_count', 'N/A')} 🎬
282+
""".strip(),
283+
"inline": True
284+
}
285+
],
286+
"footer": {
287+
"text": f"🤖 Automated by Basic Memory • Daily Reach: {total_reach:,}"
288+
},
289+
"timestamp": datetime.now().isoformat()
290+
}
291+
292+
# Add top Reddit posts if available
293+
if reddit_data.get('top_posts'):
294+
top_post = reddit_data['top_posts'][0]
295+
embed["fields"].append({
296+
"name": "🔥 Top Reddit Post",
297+
"value": f"**{top_post['title']}**\n📊 {top_post['score']} upvotes • 💬 {top_post['num_comments']} comments\n📍 r/{top_post['subreddit']}",
298+
"inline": False
299+
})
300+
301+
return embed
302+
303+
def send_discord_report(self, embed):
304+
"""Send the report to Discord"""
305+
try:
306+
payload = {"embeds": [embed]}
307+
response = requests.post(self.discord_webhook, json=payload)
308+
309+
if response.status_code == 204:
310+
print("✅ Discord report sent successfully!")
311+
return True
312+
else:
313+
print(f"❌ Discord webhook failed: {response.status_code}")
314+
print(response.text)
315+
return False
316+
317+
except Exception as e:
318+
print(f"Discord send error: {e}")
319+
return False
320+
321+
def run_daily_report(self):
322+
"""Main function to generate and send daily report"""
323+
print("🚀 Starting Basic Memory Daily Traction Report...")
324+
325+
# Load previous metrics
326+
print("📊 Loading previous metrics...")
327+
previous_metrics = self.get_previous_metrics()
328+
329+
# Collect current metrics
330+
print("📊 Collecting GitHub metrics...")
331+
github_data = self.get_github_metrics()
332+
333+
print("🗨️ Collecting Reddit metrics...")
334+
reddit_data = self.get_reddit_metrics()
335+
336+
print("📺 Collecting YouTube metrics...")
337+
youtube_data = self.get_youtube_metrics()
338+
339+
# Combine current metrics
340+
current_metrics = {
341+
'github': github_data,
342+
'reddit': reddit_data,
343+
'youtube': youtube_data
344+
}
345+
346+
# Create and send report
347+
print("🎨 Creating Discord embed with growth tracking...")
348+
embed = self.create_discord_embed(current_metrics, previous_metrics.get('metrics', {}))
349+
350+
print("📤 Sending to Discord...")
351+
success = self.send_discord_report(embed)
352+
353+
# Save current metrics for tomorrow
354+
print("💾 Saving metrics for tomorrow's comparison...")
355+
self.save_current_metrics(current_metrics)
356+
357+
if success:
358+
print("🎉 Daily traction report completed successfully!")
359+
else:
360+
print("😞 Report failed to send")
361+
362+
# Print summary for GitHub Actions logs
363+
print(f"""
364+
📊 DAILY SUMMARY:
365+
⭐ GitHub Stars: {github_data.get('stars', 'Error')}
366+
👥 Reddit Mentions: {reddit_data.get('total_mentions', 'Error')}
367+
📺 YouTube Subscribers: {youtube_data.get('subscribers', 'Error')}
368+
📺 YouTube Views: {youtube_data.get('total_views', 'Error')}
369+
""")
370+
371+
if __name__ == "__main__":
372+
tracker = BasicMemoryTracker()
373+
tracker.run_daily_report()

0 commit comments

Comments
 (0)