#!/usr/bin/env python3 """ Threads Reader - Fetch feed, trending posts, and user content """ import os import sys import json import asyncio import argparse from pathlib import Path try: from threads_api.src.threads_api import ThreadsAPI except ImportError: print("Error: threads-api not installed. Run: pip3 install threads-api", file=sys.stderr) sys.exit(1) CONFIG_FILE = Path.home() / ".config/threads/config.json" TOKEN_CACHE = Path.home() / ".config/threads/.token" def load_config(): """Load credentials from env vars or config file""" username = os.getenv("INSTAGRAM_USERNAME") password = os.getenv("INSTAGRAM_PASSWORD") if not username and CONFIG_FILE.exists(): with open(CONFIG_FILE) as f: config = json.load(f) username = config.get("username") password = config.get("password") if not username or not password: print("Error: Set INSTAGRAM_USERNAME and INSTAGRAM_PASSWORD", file=sys.stderr) print("Or create config at ~/.config/threads/config.json", file=sys.stderr) sys.exit(1) return username, password async def get_feed(limit=10): """Fetch For You feed""" username, password = load_config() api = ThreadsAPI() # Create token cache directory TOKEN_CACHE.parent.mkdir(parents=True, exist_ok=True) try: # Login with cached token await api.login(username, password, cached_token_path=str(TOKEN_CACHE)) # Get recommended feed feed = await api.get_timeline() posts = [] if feed and 'data' in feed: threads = feed.get('data', {}).get('mediaData', {}).get('threads', []) for thread in threads[:limit]: thread_items = thread.get('thread_items', []) if thread_items: post = thread_items[0].get('post', {}) user = post.get('user', {}) posts.append({ 'author': user.get('username', 'unknown'), 'text': post.get('caption', {}).get('text', ''), 'likes': post.get('like_count', 0), 'replies': post.get('text_post_app_info', {}).get('direct_reply_count', 0), 'created_at': post.get('taken_at', 0), 'url': f"https://www.threads.net/@{user.get('username')}/post/{post.get('code', '')}" }) await api.close_gracefully() return posts except Exception as e: print(f"Error fetching feed: {e}", file=sys.stderr) await api.close_gracefully() return [] async def get_trending(limit=5): """Fetch trending/popular posts""" # For now, use regular feed and filter by engagement posts = await get_feed(limit=50) # Sort by engagement (likes + replies) posts.sort(key=lambda p: p['likes'] + p['replies'], reverse=True) return posts[:limit] async def get_user_posts(username, limit=5): """Fetch specific user's posts""" user, password = load_config() api = ThreadsAPI() TOKEN_CACHE.parent.mkdir(parents=True, exist_ok=True) try: await api.login(user, password, cached_token_path=str(TOKEN_CACHE)) # Get user ID first user_data = await api.get_user_id_from_username(username.lstrip('@')) if not user_data: print(f"Error: User {username} not found", file=sys.stderr) await api.close_gracefully() return [] user_id = user_data # Get user's threads profile = await api.get_user_profile_threads(user_id) posts = [] if profile and 'data' in profile: threads = profile.get('data', {}).get('mediaData', {}).get('threads', []) for thread in threads[:limit]: thread_items = thread.get('thread_items', []) if thread_items: post = thread_items[0].get('post', {}) user_obj = post.get('user', {}) posts.append({ 'author': user_obj.get('username', username), 'text': post.get('caption', {}).get('text', ''), 'likes': post.get('like_count', 0), 'replies': post.get('text_post_app_info', {}).get('direct_reply_count', 0), 'created_at': post.get('taken_at', 0), 'url': f"https://www.threads.net/@{user_obj.get('username')}/post/{post.get('code', '')}" }) await api.close_gracefully() return posts except Exception as e: print(f"Error fetching user posts: {e}", file=sys.stderr) await api.close_gracefully() return [] def main(): parser = argparse.ArgumentParser(description='Threads Reader') subparsers = parser.add_subparsers(dest='command', help='Commands') # Feed command feed_parser = subparsers.add_parser('feed', help='Get For You feed') feed_parser.add_argument('--limit', type=int, default=10, help='Number of posts') # Trending command trending_parser = subparsers.add_parser('trending', help='Get trending posts') trending_parser.add_argument('--limit', type=int, default=5, help='Number of posts') # User command user_parser = subparsers.add_parser('user', help='Get user posts') user_parser.add_argument('username', help='Username (with or without @)') user_parser.add_argument('--limit', type=int, default=5, help='Number of posts') args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) # Run async command if args.command == 'feed': posts = asyncio.run(get_feed(args.limit)) elif args.command == 'trending': posts = asyncio.run(get_trending(args.limit)) elif args.command == 'user': posts = asyncio.run(get_user_posts(args.username, args.limit)) else: parser.print_help() sys.exit(1) # Output JSON print(json.dumps(posts, indent=2)) if __name__ == '__main__': main()