feat(UI): initial working frontend UI
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
# BDFR API Layer
|
||||
|
||||
The BDFR API layer provides a direct integration interface for the web interface, eliminating the need for subprocess console parsing. It wraps the existing BDFR core classes and provides a clean API with structured progress callbacks.
|
||||
|
||||
## Overview
|
||||
|
||||
The API layer consists of several key components:
|
||||
|
||||
- **BDFRManager**: Main API interface class
|
||||
- **ProgressCallback**: Abstract base class for progress notifications
|
||||
- **ProgressEvent**: Structured progress event data
|
||||
- **DownloadType**: Enumeration of supported download types
|
||||
- **DownloadStatus**: Enumeration of download statuses
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from bdfr.api import BDFRManager, LoggingCallback
|
||||
|
||||
# Create a manager
|
||||
manager = BDFRManager("./downloads")
|
||||
|
||||
# Download from a subreddit
|
||||
download_id = manager.download_subreddit(
|
||||
"python",
|
||||
limit=50,
|
||||
sort="hot",
|
||||
no_dupes=True
|
||||
)
|
||||
|
||||
# Check status
|
||||
status = manager.get_download_status(download_id)
|
||||
print(f"Progress: {status['progress']}%")
|
||||
```
|
||||
|
||||
### Advanced Usage with Custom Callbacks
|
||||
|
||||
```python
|
||||
from bdfr.api import BDFRManager, ProgressCallback, ProgressEvent
|
||||
|
||||
class WebSocketCallback(ProgressCallback):
|
||||
def __init__(self, websocket):
|
||||
self.websocket = websocket
|
||||
|
||||
async def on_progress(self, event: ProgressEvent):
|
||||
await self.websocket.send_json(event.to_dict())
|
||||
|
||||
async def on_error(self, event: ProgressEvent):
|
||||
await self.websocket.send_json(event.to_dict())
|
||||
|
||||
async def on_completed(self, event: ProgressEvent):
|
||||
await self.websocket.send_json(event.to_dict())
|
||||
|
||||
# Use custom callbacks
|
||||
callbacks = [LoggingCallback(), WebSocketCallback(ws)]
|
||||
download_id = manager.download_subreddit("technology", limit=100, progress_callbacks=callbacks)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### BDFRManager
|
||||
|
||||
The main API class that manages downloads and provides the primary interface.
|
||||
|
||||
#### Constructor
|
||||
|
||||
```python
|
||||
BDFRManager(download_directory: Optional[Union[str, Path]] = None)
|
||||
```
|
||||
|
||||
- `download_directory`: Base directory for downloads. Defaults to current directory.
|
||||
|
||||
#### Methods
|
||||
|
||||
##### `create_download()`
|
||||
|
||||
Create a new download operation.
|
||||
|
||||
```python
|
||||
create_download(
|
||||
download_type: DownloadType,
|
||||
name: str,
|
||||
config: Optional[Configuration] = None,
|
||||
progress_callbacks: Optional[List[ProgressCallback]] = None
|
||||
) -> str
|
||||
```
|
||||
|
||||
Returns a download ID for tracking the operation.
|
||||
|
||||
##### `start_download()`
|
||||
|
||||
Start a download operation.
|
||||
|
||||
```python
|
||||
start_download(download_id: str) -> bool
|
||||
```
|
||||
|
||||
Returns `True` if started successfully.
|
||||
|
||||
##### `get_download_status()`
|
||||
|
||||
Get the current status of a download.
|
||||
|
||||
```python
|
||||
get_download_status(download_id: str) -> Optional[Dict[str, Any]]
|
||||
```
|
||||
|
||||
Returns download status information or `None` if not found.
|
||||
|
||||
##### `cancel_download()`
|
||||
|
||||
Cancel a running download.
|
||||
|
||||
```python
|
||||
cancel_download(download_id: str) -> bool
|
||||
```
|
||||
|
||||
Returns `True` if cancelled successfully.
|
||||
|
||||
##### `list_downloads()`
|
||||
|
||||
List all active downloads.
|
||||
|
||||
```python
|
||||
list_downloads() -> Dict[str, Dict[str, Any]]
|
||||
```
|
||||
|
||||
Returns dictionary of download information keyed by download ID.
|
||||
|
||||
##### `cleanup_completed()`
|
||||
|
||||
Clean up old completed downloads.
|
||||
|
||||
```python
|
||||
cleanup_completed(max_age_seconds: int = 3600) -> int
|
||||
```
|
||||
|
||||
Returns number of downloads cleaned up.
|
||||
|
||||
#### Convenience Methods
|
||||
|
||||
##### `download_subreddit()`
|
||||
|
||||
Download content from a subreddit.
|
||||
|
||||
```python
|
||||
download_subreddit(
|
||||
subreddit_name: str,
|
||||
limit: Optional[int] = None,
|
||||
sort: str = "hot",
|
||||
time_filter: str = "all",
|
||||
no_dupes: bool = False,
|
||||
progress_callbacks: Optional[List[ProgressCallback]] = None
|
||||
) -> str
|
||||
```
|
||||
|
||||
##### `download_user()`
|
||||
|
||||
Download content from a user.
|
||||
|
||||
```python
|
||||
download_user(
|
||||
username: str,
|
||||
limit: Optional[int] = None,
|
||||
submitted: bool = True,
|
||||
upvoted: bool = False,
|
||||
saved: bool = False,
|
||||
no_dupes: bool = False,
|
||||
progress_callbacks: Optional[List[ProgressCallback]] = None
|
||||
) -> str
|
||||
```
|
||||
|
||||
##### `archive_subreddit()`
|
||||
|
||||
Archive subreddit data (metadata only).
|
||||
|
||||
```python
|
||||
archive_subreddit(
|
||||
subreddit_name: str,
|
||||
format_type: str = "json",
|
||||
limit: Optional[int] = None,
|
||||
progress_callbacks: Optional[List[ProgressCallback]] = None
|
||||
) -> str
|
||||
```
|
||||
|
||||
##### `clone_subreddit()`
|
||||
|
||||
Clone subreddit (both download and archive).
|
||||
|
||||
```python
|
||||
clone_subreddit(
|
||||
subreddit_name: str,
|
||||
limit: Optional[int] = None,
|
||||
format_type: str = "json",
|
||||
no_dupes: bool = False,
|
||||
progress_callbacks: Optional[List[ProgressCallback]] = None
|
||||
) -> str
|
||||
```
|
||||
|
||||
### ProgressCallback
|
||||
|
||||
Abstract base class for implementing progress callbacks.
|
||||
|
||||
#### Methods
|
||||
|
||||
##### `on_progress(event: ProgressEvent)`
|
||||
|
||||
Called when progress is made.
|
||||
|
||||
##### `on_error(event: ProgressEvent)`
|
||||
|
||||
Called when an error occurs.
|
||||
|
||||
##### `on_completed(event: ProgressEvent)`
|
||||
|
||||
Called when download is completed.
|
||||
|
||||
### ProgressEvent
|
||||
|
||||
Represents a progress event with structured data.
|
||||
|
||||
#### Attributes
|
||||
|
||||
- `event_type`: "progress", "status", "error", or "completed"
|
||||
- `download_id`: Unique identifier for the download
|
||||
- `message`: Human-readable message
|
||||
- `progress`: Progress percentage (0-100)
|
||||
- `data`: Additional structured data
|
||||
- `timestamp`: When the event occurred
|
||||
|
||||
#### Methods
|
||||
|
||||
##### `to_dict() -> Dict[str, Any]`
|
||||
|
||||
Convert to dictionary for JSON serialization.
|
||||
|
||||
### DownloadType
|
||||
|
||||
Enumeration of supported download types:
|
||||
|
||||
- `SUBREDDIT`: Download from subreddit
|
||||
- `USER`: Download from user
|
||||
- `MULTIREDDIT`: Download from multireddit
|
||||
- `SUBMISSIONS`: Download specific submissions
|
||||
- `ARCHIVE`: Archive only (no downloads)
|
||||
- `CLONE`: Both download and archive
|
||||
|
||||
### DownloadStatus
|
||||
|
||||
Enumeration of download statuses:
|
||||
|
||||
- `QUEUED`: Download is queued but not started
|
||||
- `RUNNING`: Download is in progress
|
||||
- `COMPLETED`: Download completed successfully
|
||||
- `FAILED`: Download failed with an error
|
||||
- `CANCELLED`: Download was cancelled
|
||||
- `PAUSED`: Download is paused
|
||||
|
||||
## Configuration
|
||||
|
||||
The API uses the existing BDFR `Configuration` class. You can pass a custom configuration to `create_download()` or use the convenience methods with their specific parameters.
|
||||
|
||||
### Common Configuration Options
|
||||
|
||||
- `limit`: Maximum number of posts to process
|
||||
- `sort`: Sort method (hot, top, new, controversial, rising)
|
||||
- `time`: Time filter (all, hour, day, week, month, year)
|
||||
- `no_dupes`: Avoid duplicate downloads
|
||||
- `make_hard_links`: Create hard links for duplicates
|
||||
- `format`: Archive format (json, xml, yaml)
|
||||
|
||||
## Web Interface Integration
|
||||
|
||||
### FastAPI Integration Example
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, WebSocket
|
||||
from bdfr.api import get_bdfr_manager, WebSocketCallback
|
||||
|
||||
app = FastAPI()
|
||||
manager = get_bdfr_manager("./downloads")
|
||||
|
||||
@app.post("/api/download/subreddit")
|
||||
async def download_subreddit(subreddit: str, limit: int = 10):
|
||||
download_id = manager.download_subreddit(subreddit, limit=limit)
|
||||
return {"download_id": download_id}
|
||||
|
||||
@app.websocket("/ws/progress/{download_id}")
|
||||
async def progress_websocket(websocket: WebSocket, download_id: str):
|
||||
await websocket.accept()
|
||||
|
||||
class FastAPICallback(WebSocketCallback):
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
|
||||
async def on_progress(self, event: ProgressEvent):
|
||||
if event.download_id == download_id:
|
||||
await websocket.send_json(event.to_dict())
|
||||
|
||||
# Add callback to existing download or create new one
|
||||
# (Implementation depends on your specific needs)
|
||||
```
|
||||
|
||||
### Real-time Progress Updates
|
||||
|
||||
The API provides structured progress events that can be easily serialized to JSON for web clients:
|
||||
|
||||
```python
|
||||
# Example progress event
|
||||
{
|
||||
"event_type": "progress",
|
||||
"download_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"message": "Downloaded submission abc123 from r/python",
|
||||
"progress": 45.2,
|
||||
"data": {
|
||||
"items_processed": 12,
|
||||
"items_found": 25,
|
||||
"current_item": "abc123",
|
||||
"phase": "downloading_submission"
|
||||
},
|
||||
"timestamp": "2023-12-07T10:30:45.123456"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API provides comprehensive error handling:
|
||||
|
||||
- **Download errors**: Network issues, authentication problems, etc.
|
||||
- **Configuration errors**: Invalid parameters, missing files, etc.
|
||||
- **System errors**: Disk space, permissions, etc.
|
||||
|
||||
All errors are captured and reported through the progress callback system with detailed error information.
|
||||
|
||||
## Threading and Concurrency
|
||||
|
||||
The API is designed to be thread-safe and supports concurrent downloads:
|
||||
|
||||
- Each download runs in its own thread
|
||||
- Progress callbacks are async-safe
|
||||
- Multiple downloads can run simultaneously
|
||||
- Thread-safe status tracking
|
||||
|
||||
## Logging
|
||||
|
||||
The API integrates with Python's logging system:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Use LoggingCallback for automatic log output
|
||||
callbacks = [LoggingCallback("my_app")]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See `bdfr/examples/api_usage.py` for comprehensive examples including:
|
||||
|
||||
- Basic usage
|
||||
- Custom callbacks
|
||||
- Web integration
|
||||
- Error handling
|
||||
- User downloads
|
||||
- Archive operations
|
||||
|
||||
## Migration from Subprocess
|
||||
|
||||
### Before (Subprocess)
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
# Start BDFR via subprocess
|
||||
proc = subprocess.Popen([
|
||||
"python", "-m", "bdfr", "download",
|
||||
"--subreddit", "python",
|
||||
"--limit", "50",
|
||||
"./downloads"
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
# Parse console output for progress
|
||||
while True:
|
||||
line = proc.stdout.readline().decode().strip()
|
||||
if not line:
|
||||
break
|
||||
# Parse progress from console output...
|
||||
```
|
||||
|
||||
### After (API)
|
||||
|
||||
```python
|
||||
from bdfr.api import BDFRManager, LoggingCallback
|
||||
|
||||
# Use API directly
|
||||
manager = BDFRManager("./downloads")
|
||||
download_id = manager.download_subreddit("python", limit=50)
|
||||
|
||||
# Get structured progress updates
|
||||
status = manager.get_download_status(download_id)
|
||||
print(f"Progress: {status['progress']}%")
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **No subprocess overhead**: Direct integration with BDFR core
|
||||
2. **Structured progress**: Rich progress events instead of console parsing
|
||||
3. **Better error handling**: Detailed error information and stack traces
|
||||
4. **Thread-safe**: Concurrent downloads with proper synchronization
|
||||
5. **Web-friendly**: JSON-serializable progress events
|
||||
6. **Extensible**: Custom progress callbacks for different integrations
|
||||
7. **Maintainable**: Clean separation of concerns
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- Existing BDFR installation
|
||||
- Dependencies: `praw`, `requests`, and other BDFR dependencies
|
||||
|
||||
## Installation
|
||||
|
||||
The API layer is included with BDFR and requires no additional installation. Simply import and use:
|
||||
|
||||
```python
|
||||
from bdfr.api import BDFRManager
|
||||
+1290
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -31,6 +31,7 @@ class Archiver(RedditConnector):
|
||||
|
||||
def download(self):
|
||||
for generator in self.reddit_lists:
|
||||
submission = None
|
||||
try:
|
||||
for submission in generator:
|
||||
try:
|
||||
@@ -50,7 +51,10 @@ class Archiver(RedditConnector):
|
||||
except prawcore.PrawcoreException as e:
|
||||
logger.error(f"Submission {submission.id} failed to be archived due to a PRAW exception: {e}")
|
||||
except prawcore.PrawcoreException as e:
|
||||
logger.error(f"The submission after {submission.id} failed to download due to a PRAW exception: {e}")
|
||||
if submission is not None:
|
||||
logger.error(f"The submission after {submission.id} failed to download due to a PRAW exception: {e}")
|
||||
else:
|
||||
logger.error(f"Download failed due to a PRAW exception: {e}")
|
||||
logger.debug("Waiting 60 seconds to continue")
|
||||
sleep(60)
|
||||
|
||||
|
||||
+5
-1
@@ -20,6 +20,7 @@ class RedditCloner(RedditDownloader, Archiver):
|
||||
|
||||
def download(self):
|
||||
for generator in self.reddit_lists:
|
||||
submission = None
|
||||
try:
|
||||
for submission in generator:
|
||||
try:
|
||||
@@ -28,6 +29,9 @@ class RedditCloner(RedditDownloader, Archiver):
|
||||
except prawcore.PrawcoreException as e:
|
||||
logger.error(f"Submission {submission.id} failed to be cloned due to a PRAW exception: {e}")
|
||||
except prawcore.PrawcoreException as e:
|
||||
logger.error(f"The submission after {submission.id} failed to download due to a PRAW exception: {e}")
|
||||
if submission is not None:
|
||||
logger.error(f"The submission after {submission.id} failed to download due to a PRAW exception: {e}")
|
||||
else:
|
||||
logger.error(f"Download failed due to a PRAW exception: {e}")
|
||||
logger.debug("Waiting 60 seconds to continue")
|
||||
sleep(60)
|
||||
|
||||
@@ -386,6 +386,18 @@ class RedditConnector(metaclass=ABCMeta):
|
||||
generators.append(self.reddit_instance.redditor(user).saved(limit=self.args.limit))
|
||||
except prawcore.PrawcoreException as e:
|
||||
logger.error(f"User {user} failed to be retrieved due to a PRAW exception: {e}")
|
||||
# Detect HTTP 429 (rate limiting) and propagate as a hard failure so the UI can show 'failed'
|
||||
TooManyRequests = getattr(prawcore.exceptions, "TooManyRequests", None)
|
||||
is_rate_limited = False
|
||||
if TooManyRequests is not None and isinstance(e, TooManyRequests):
|
||||
is_rate_limited = True
|
||||
elif (hasattr(e, "response") and getattr(e.response, "status_code", None) == 429) or "429" in str(e):
|
||||
is_rate_limited = True
|
||||
|
||||
if is_rate_limited:
|
||||
logger.error("Received HTTP 429 (rate limited). Propagating error to fail the download.")
|
||||
raise
|
||||
|
||||
logger.debug("Waiting 60 seconds to continue")
|
||||
sleep(60)
|
||||
return generators
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class RedditDownloader(RedditConnector):
|
||||
except prawcore.PrawcoreException as e:
|
||||
logger.error(f"Submission {submission.id} failed to download due to a PRAW exception: {e}")
|
||||
except prawcore.PrawcoreException as e:
|
||||
submission_id = last_submission_id or "unknown"
|
||||
submission_id = last_submission_id if last_submission_id is not None else "unknown"
|
||||
logger.error(f"The submission after {submission_id} failed to download due to a PRAW exception: {e}")
|
||||
logger.debug("Waiting 60 seconds to continue")
|
||||
sleep(60)
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
BDFR API Usage Examples
|
||||
|
||||
This file demonstrates how to use the BDFR API layer for direct integration
|
||||
with the web interface, eliminating the need for subprocess console parsing.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
# Add the parent directory to the path so we can import bdfr
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from bdfr.api import (
|
||||
BDFRManager,
|
||||
DownloadType,
|
||||
DownloadStatus,
|
||||
ProgressEvent,
|
||||
ProgressCallback,
|
||||
LoggingCallback,
|
||||
get_bdfr_manager
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(asctime)s] %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketCallback(ProgressCallback):
|
||||
"""Example callback that simulates WebSocket updates"""
|
||||
|
||||
def __init__(self, websocket_id: str = "demo"):
|
||||
self.websocket_id = websocket_id
|
||||
|
||||
async def on_progress(self, event: ProgressEvent):
|
||||
"""Send progress update to WebSocket"""
|
||||
print(f"📊 [{self.websocket_id}] Progress: {event.message}")
|
||||
if event.progress is not None:
|
||||
print(f" Progress: {event.progress:.1f}%")
|
||||
if event.data:
|
||||
print(f" Data: {event.data}")
|
||||
|
||||
async def on_error(self, event: ProgressEvent):
|
||||
"""Send error update to WebSocket"""
|
||||
print(f"❌ [{self.websocket_id}] ERROR: {event.message}")
|
||||
if event.data.get('exception'):
|
||||
print(f" Exception: {event.data['exception']}")
|
||||
|
||||
async def on_completed(self, event: ProgressEvent):
|
||||
"""Send completion update to WebSocket"""
|
||||
status_icon = "✅" if event.event_type == "completed" else "⚠️"
|
||||
print(f"{status_icon} [{self.websocket_id}] {event.message}")
|
||||
if event.data:
|
||||
print(f" Final stats: {event.data}")
|
||||
|
||||
|
||||
class DatabaseCallback(ProgressCallback):
|
||||
"""Example callback that saves progress to a database"""
|
||||
|
||||
def __init__(self, db_connection_string: str = "sqlite:///progress.db"):
|
||||
self.db_connection = db_connection_string
|
||||
|
||||
async def on_progress(self, event: ProgressEvent):
|
||||
"""Save progress to database"""
|
||||
# In a real implementation, you would save to your database
|
||||
print(f"💾 [DB] Saved progress for {event.download_id}: {event.progress}%")
|
||||
|
||||
async def on_error(self, event: ProgressEvent):
|
||||
"""Save error to database"""
|
||||
print(f"💾 [DB] Saved error for {event.download_id}: {event.message}")
|
||||
|
||||
async def on_completed(self, event: ProgressEvent):
|
||||
"""Save completion to database"""
|
||||
print(f"💾 [DB] Saved completion for {event.download_id}")
|
||||
|
||||
|
||||
async def example_basic_usage():
|
||||
"""Basic usage example"""
|
||||
print("🚀 Basic BDFR API Usage Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Create a BDFR manager
|
||||
manager = BDFRManager("./downloads")
|
||||
|
||||
# Create a download for a subreddit
|
||||
download_id = manager.download_subreddit(
|
||||
"python", # subreddit name
|
||||
limit=10, # download 10 posts
|
||||
sort="hot", # sort by hot
|
||||
no_dupes=True # avoid duplicates
|
||||
)
|
||||
|
||||
print(f"📋 Created download with ID: {download_id}")
|
||||
|
||||
# Monitor progress
|
||||
while True:
|
||||
status = manager.get_download_status(download_id)
|
||||
if not status:
|
||||
print("❌ Download not found!")
|
||||
break
|
||||
|
||||
print(f"📊 Status: {status['status']} | Progress: {status['progress']:.1f}%")
|
||||
|
||||
if status['status'] in ['completed', 'failed', 'cancelled']:
|
||||
print(f"🏁 Download finished with status: {status['status']}")
|
||||
break
|
||||
|
||||
await asyncio.sleep(2) # Check every 2 seconds
|
||||
|
||||
return download_id
|
||||
|
||||
|
||||
async def example_advanced_usage():
|
||||
"""Advanced usage with custom callbacks"""
|
||||
print("\n🎯 Advanced BDFR API Usage Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Create custom callbacks
|
||||
callbacks = [
|
||||
LoggingCallback("web_interface"),
|
||||
WebSocketCallback("user_123"),
|
||||
DatabaseCallback()
|
||||
]
|
||||
|
||||
# Create manager with custom download directory
|
||||
manager = BDFRManager("./custom_downloads")
|
||||
|
||||
# Download from multiple subreddits
|
||||
subreddits = ["programming", "learnprogramming", "Python"]
|
||||
download_ids = []
|
||||
|
||||
for subreddit in subreddits:
|
||||
download_id = manager.create_download(
|
||||
DownloadType.SUBREDDIT,
|
||||
subreddit,
|
||||
progress_callbacks=callbacks
|
||||
)
|
||||
|
||||
# Start the download
|
||||
manager.start_download(download_id)
|
||||
download_ids.append(download_id)
|
||||
print(f"📋 Started download {download_id} for r/{subreddit}")
|
||||
|
||||
# Monitor all downloads
|
||||
while download_ids:
|
||||
active_downloads = []
|
||||
|
||||
for download_id in download_ids[:]: # Copy list to avoid modification during iteration
|
||||
status = manager.get_download_status(download_id)
|
||||
if not status:
|
||||
print(f"❌ Download {download_id} not found")
|
||||
download_ids.remove(download_id)
|
||||
continue
|
||||
|
||||
print(f"📊 {download_id}: {status['status']} ({status['progress']:.1f}%)")
|
||||
|
||||
if status['status'] in ['completed', 'failed', 'cancelled']:
|
||||
print(f"🏁 Download {download_id} finished")
|
||||
download_ids.remove(download_id)
|
||||
else:
|
||||
active_downloads.append(download_id)
|
||||
|
||||
if not active_downloads:
|
||||
break
|
||||
|
||||
await asyncio.sleep(3) # Check every 3 seconds
|
||||
|
||||
return len(download_ids) == 0 # Return success status
|
||||
|
||||
|
||||
async def example_user_download():
|
||||
"""Example of downloading user content"""
|
||||
print("\n👤 User Download Example")
|
||||
print("=" * 50)
|
||||
|
||||
manager = get_bdfr_manager() # Use default manager
|
||||
|
||||
# Download user's submitted posts
|
||||
download_id = manager.download_user(
|
||||
"testuser", # username
|
||||
limit=25, # 25 posts
|
||||
submitted=True,
|
||||
upvoted=False,
|
||||
saved=False
|
||||
)
|
||||
|
||||
print(f"📋 Created user download: {download_id}")
|
||||
|
||||
# Check status periodically
|
||||
for _ in range(10): # Check for up to 20 seconds
|
||||
status = manager.get_download_status(download_id)
|
||||
if not status:
|
||||
print("❌ Download not found")
|
||||
break
|
||||
|
||||
print(f"📊 Status: {status['status']} | Progress: {status['progress']:.1f}%")
|
||||
|
||||
if status['status'] in ['completed', 'failed']:
|
||||
break
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
return download_id
|
||||
|
||||
|
||||
async def example_archive_operation():
|
||||
"""Example of archiving subreddit data"""
|
||||
print("\n📚 Archive Operation Example")
|
||||
print("=" * 50)
|
||||
|
||||
manager = BDFRManager("./archives")
|
||||
|
||||
# Archive subreddit data (metadata only)
|
||||
download_id = manager.archive_subreddit(
|
||||
"dataisbeautiful",
|
||||
format_type="json",
|
||||
limit=50
|
||||
)
|
||||
|
||||
print(f"📋 Created archive operation: {download_id}")
|
||||
|
||||
# Monitor progress
|
||||
while True:
|
||||
status = manager.get_download_status(download_id)
|
||||
if not status:
|
||||
print("❌ Archive not found")
|
||||
break
|
||||
|
||||
print(f"📊 Archive status: {status['status']} | Progress: {status['progress']:.1f}%")
|
||||
|
||||
if status['status'] in ['completed', 'failed']:
|
||||
print(f"🏁 Archive finished with status: {status['status']}")
|
||||
break
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
return download_id
|
||||
|
||||
|
||||
async def example_web_integration():
|
||||
"""Example showing how to integrate with a web application"""
|
||||
print("\n🌐 Web Integration Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Simulate a web application using the API
|
||||
class MockWebApp:
|
||||
def __init__(self):
|
||||
self.manager = BDFRManager("./web_downloads")
|
||||
self.active_sessions = {}
|
||||
|
||||
async def handle_download_request(self, user_id: str, subreddit: str, limit: int):
|
||||
"""Handle a download request from the web interface"""
|
||||
|
||||
# Create custom callback for this user
|
||||
callback = WebSocketCallback(f"ws_{user_id}")
|
||||
|
||||
# Create and start download
|
||||
download_id = self.manager.download_subreddit(
|
||||
subreddit,
|
||||
limit=limit,
|
||||
progress_callbacks=[callback]
|
||||
)
|
||||
|
||||
# Track for this user session
|
||||
if user_id not in self.active_sessions:
|
||||
self.active_sessions[user_id] = []
|
||||
self.active_sessions[user_id].append(download_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"download_id": download_id,
|
||||
"message": f"Started download of r/{subreddit} (limit: {limit})"
|
||||
}
|
||||
|
||||
async def get_user_downloads(self, user_id: str):
|
||||
"""Get all downloads for a user"""
|
||||
if user_id not in self.active_sessions:
|
||||
return []
|
||||
|
||||
downloads = []
|
||||
for download_id in self.active_sessions[user_id]:
|
||||
status = self.manager.get_download_status(download_id)
|
||||
if status:
|
||||
downloads.append(status)
|
||||
|
||||
return downloads
|
||||
|
||||
async def cancel_user_download(self, user_id: str, download_id: str):
|
||||
"""Cancel a specific download for a user"""
|
||||
if user_id in self.active_sessions and download_id in self.active_sessions[user_id]:
|
||||
success = self.manager.cancel_download(download_id)
|
||||
if success:
|
||||
self.active_sessions[user_id].remove(download_id)
|
||||
return {"success": True, "message": "Download cancelled"}
|
||||
else:
|
||||
return {"success": False, "message": "Failed to cancel download"}
|
||||
|
||||
return {"success": False, "message": "Download not found for user"}
|
||||
|
||||
# Simulate web app usage
|
||||
app = MockWebApp()
|
||||
|
||||
# Simulate user requests
|
||||
user_id = "user123"
|
||||
|
||||
# User starts a download
|
||||
result1 = await app.handle_download_request(user_id, "technology", 20)
|
||||
print(f"User request result: {result1}")
|
||||
|
||||
# User starts another download
|
||||
result2 = await app.handle_download_request(user_id, "science", 15)
|
||||
print(f"User request result: {result2}")
|
||||
|
||||
# Check user's downloads
|
||||
user_downloads = await app.get_user_downloads(user_id)
|
||||
print(f"User has {len(user_downloads)} active downloads:")
|
||||
for download in user_downloads:
|
||||
print(f" - {download['id']}: {download['status']} ({download['progress']:.1f}%)")
|
||||
|
||||
# Cancel one download
|
||||
if user_downloads:
|
||||
cancel_result = await app.cancel_user_download(user_id, user_downloads[0]['id'])
|
||||
print(f"Cancel result: {cancel_result}")
|
||||
|
||||
return len(user_downloads)
|
||||
|
||||
|
||||
async def example_error_handling():
|
||||
"""Example of error handling"""
|
||||
print("\n⚠️ Error Handling Example")
|
||||
print("=" * 50)
|
||||
|
||||
manager = BDFRManager("./test_downloads")
|
||||
|
||||
# Try to download from a non-existent subreddit
|
||||
download_id = manager.download_subreddit(
|
||||
"this_subreddit_does_not_exist",
|
||||
limit=5
|
||||
)
|
||||
|
||||
print(f"📋 Created download for non-existent subreddit: {download_id}")
|
||||
|
||||
# Monitor for error
|
||||
for _ in range(5): # Check for up to 10 seconds
|
||||
status = manager.get_download_status(download_id)
|
||||
if not status:
|
||||
print("❌ Download disappeared")
|
||||
break
|
||||
|
||||
print(f"📊 Status: {status['status']}")
|
||||
|
||||
if status['status'] == 'failed':
|
||||
print(f"🏁 Download failed as expected: {status.get('error', 'Unknown error')}")
|
||||
break
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
return download_id
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all examples"""
|
||||
print("🎯 BDFR API Examples")
|
||||
print("=" * 60)
|
||||
print("This demonstrates the new BDFR API layer for direct web integration")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Run basic example
|
||||
await example_basic_usage()
|
||||
|
||||
# Run advanced example
|
||||
await example_advanced_usage()
|
||||
|
||||
# Run user download example
|
||||
await example_user_download()
|
||||
|
||||
# Run archive example
|
||||
await example_archive_operation()
|
||||
|
||||
# Run web integration example
|
||||
await example_web_integration()
|
||||
|
||||
# Run error handling example
|
||||
await example_error_handling()
|
||||
|
||||
print("\n🎉 All examples completed!")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⏹️ Examples interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error running examples: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the examples
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user