rustboxd 0.2.0

A Letterboxd web scraper and API client library written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# API Documentation

## Core Module

### Client

The `Client` struct provides the HTTP functionality for making requests to Letterboxd.

```rust
use rustboxd::core::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let html = client.get_page("https://letterboxd.com/user/username/").await?;
    // Process HTML...
    Ok(())
}
```

#### Methods

- `new() -> Self` - Creates a new client with default headers
- `get_page(url: &str) -> Result<Html>` - Fetches and parses HTML from a URL

### Error Types

Comprehensive error handling for all operations:

```rust
use rustboxd::Error;

match some_operation().await {
    Ok(result) => println!("Success: {:?}", result),
    Err(Error::PageLoad { url, message }) => {
        eprintln!("Failed to load {}: {}", url, message);
    }
    Err(Error::InvalidUsername(username)) => {
        eprintln!("Invalid username: {}", username);
    }
    Err(Error::MovieNotFound(slug)) => {
        eprintln!("Movie not found: {}", slug);
    }
    Err(e) => eprintln!("Other error: {}", e),
}
```

#### Error Variants

- `Http(reqwest::Error)` - HTTP request errors
- `HtmlParse` - HTML parsing failures
- `PageLoad { url: String, message: String }` - Page loading errors
- `InvalidResponse` - Invalid server responses
- `PrivateRoute` - Access denied to private content
- `InvalidUsername(String)` - Invalid username format
- `MovieNotFound(String)` - Movie slug not found
- `Parse(String)` - General parsing errors
- `Serialization(serde_json::Error)` - JSON serialization errors
- `UrlParse(url::ParseError)` - URL parsing errors

### Constants

Pre-defined constants for URLs, validation, and configuration:

```rust
use rustboxd::core::constants::*;

// Date functions
let year = current_year();
let month = current_month();
let day = current_day();

// URLs
println!("Base domain: {}", DOMAIN);
println!("Valid genres: {:?}", GENRES);
println!("Valid ratings: {:?}", VALID_RATINGS);
```

## Models

### User

The `User` struct represents a Letterboxd user with all their profile information and statistics.

```rust
use rustboxd::User;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let user = User::new("username").await?;
    
    println!("Display name: {}", user.display_name);
    println!("Bio: {:?}", user.bio);
    
    if let Some(stats) = &user.stats {
        println!("Films: {}", stats.films);
        println!("Reviews: {}", stats.reviews);
    }
    
    Ok(())
}
```

#### Fields

- `username: String` - Lowercase username
- `url: String` - Full profile URL
- `id: Option<u64>` - Letterboxd user ID
- `is_hq: bool` - Whether user has HQ membership
- `display_name: String` - Display name as shown on profile
- `bio: Option<String>` - User biography
- `location: Option<String>` - User location
- `website: Option<String>` - User website
- `watchlist_length: Option<u32>` - Number of films in watchlist
- `stats: Option<UserStats>` - User statistics
- `favorites: Option<Vec<String>>` - Favorite films
- `avatar: Option<String>` - Avatar image URL
- `recent: UserRecent` - Recent activity

#### Methods

**Activity & Social**
- `get_activity() -> Result<HashMap<String, Value>>` - Get user activity feed
- `get_followers() -> Result<HashMap<String, Value>>` - Get user's followers
- `get_following() -> Result<HashMap<String, Value>>` - Get users being followed

**Films & Viewing**
- `get_films() -> Result<HashMap<String, Value>>` - Get all watched films
- `get_films_by_rating(rating: f32) -> Result<HashMap<String, Value>>` - Get films with specific rating
- `get_films_not_rated() -> Result<HashMap<String, Value>>` - Get unrated films
- `get_diary(year, month, day, page) -> Result<HashMap<String, Value>>` - Get diary entries
- `get_watchlist() -> Result<HashMap<String, Value>>` - Get watchlist

**Lists & Reviews**
- `get_lists() -> Result<HashMap<String, Value>>` - Get user's lists
- `get_reviews() -> Result<HashMap<String, Value>>` - Get user's reviews
- `get_liked_films() -> Result<HashMap<String, Value>>` - Get liked films
- `get_liked_reviews() -> Result<HashMap<String, Value>>` - Get liked reviews

**Statistics & Analysis**
- `get_genre_info() -> Result<HashMap<String, Value>>` - Get genre viewing statistics
- `get_tags() -> Result<HashMap<String, Value>>` - Get user's tags

### Movie

The `Movie` struct contains comprehensive information about a film.

```rust
use rustboxd::Movie;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let movie = Movie::new("the-matrix").await?;
    
    println!("Title: {}", movie.title);
    println!("Year: {:?}", movie.year);
    println!("Rating: {:?}", movie.rating);
    println!("Genres: {:?}", movie.genres);
    
    // Get additional data
    let reviews = movie.get_reviews().await?;
    let similar = movie.get_similar().await?;
    
    Ok(())
}
```

#### Fields

- `url: String` - Full movie URL
- `slug: String` - Movie slug identifier
- `movie_id: Option<u64>` - Letterboxd movie ID
- `title: String` - Movie title
- `original_title: Option<String>` - Original language title
- `runtime: Option<u32>` - Runtime in minutes
- `rating: Option<f32>` - Average rating (0.5-5.0)
- `year: Option<i32>` - Release year
- `tmdb_link: Option<String>` - TMDB link
- `imdb_link: Option<String>` - IMDB link
- `poster: Option<String>` - Poster image URL
- `banner: Option<String>` - Banner image URL
- `tagline: Option<String>` - Movie tagline
- `description: Option<String>` - Plot description
- `trailer: Option<MovieTrailer>` - Trailer information
- `alternative_titles: Vec<String>` - Alternative titles
- `details: Option<MovieDetails>` - Production details
- `genres: Vec<String>` - Movie genres
- `cast: Vec<MoviePerson>` - Cast members
- `crew: Vec<MoviePerson>` - Crew members
- `popular_reviews: Vec<MovieReview>` - Popular reviews

#### Methods

- `get_watchers() -> Result<HashMap<String, Value>>` - Get users who watched
- `get_reviews() -> Result<HashMap<String, Value>>` - Get movie reviews
- `get_similar() -> Result<HashMap<String, Value>>` - Get similar movies

### Search

The `Search` struct provides search functionality across different content types.

```rust
use rustboxd::Search;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Search films
    let film_search = Search::new("blade runner", Some("films")).await?;
    println!("Found {} films", film_search.results.films.len());
    
    // Search reviews
    let review_search = Search::new("masterpiece", Some("reviews")).await?;
    
    // Search without filter (all types)
    let general_search = Search::new("kubrick", None).await?;
    
    Ok(())
}
```

#### Fields

- `query: String` - Search query
- `search_filter: Option<String>` - Content type filter
- `url: String` - Search URL
- `results: SearchResults` - Search results

#### Available Filters

- `"films"` - Search movies
- `"reviews"` - Search reviews
- `"lists"` - Search lists
- `"original-lists"` - Search original lists only
- `"stories"` - Search stories
- `"cast-crew"` - Search cast and crew
- `"members"` - Search users
- `"tags"` - Search tags
- `"articles"` - Search articles
- `"episodes"` - Search episodes
- `"full-text"` - Full text search

### Films

The `Films` struct handles collections of movies with pagination support.

```rust
use rustboxd::Films;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let films = Films::new("https://letterboxd.com/films/year/2024/").await?;
    
    println!("Found {} films", films.count);
    
    // Filter by year
    let films_2024 = films.filter_by_year(2024);
    
    // Filter by rating
    let high_rated = films.filter_by_rating(4.0);
    
    Ok(())
}
```

#### Methods

- `filter_by_year(year: i32) -> Vec<&FilmEntry>` - Filter by release year
- `filter_by_rating(min_rating: f32) -> Vec<&FilmEntry>` - Filter by minimum rating
- `get_watched() -> Vec<&FilmEntry>` - Get watched films
- `get_liked() -> Vec<&FilmEntry>` - Get liked films
- `get_in_watchlist() -> Vec<&FilmEntry>` - Get watchlisted films

### List

The `List` struct represents user-created film lists.

```rust
use rustboxd::List;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let list = List::new("username", "list-slug").await?;
    
    println!("Title: {}", list.title);
    println!("Films: {}", list.film_count);
    
    // Get list comments
    let comments = list.get_comments().await?;
    
    // Find specific film
    if let Some(film) = list.get_film_by_position(1) {
        println!("First film: {}", film.title);
    }
    
    Ok(())
}
```

#### Fields

- `title: String` - List title
- `description: Option<String>` - List description
- `author: String` - List creator username
- `slug: String` - List slug
- `url: String` - Full list URL
- `film_count: u32` - Number of films
- `likes: u32` - Number of likes
- `comments: u32` - Number of comments
- `is_ranked: bool` - Whether list is ranked
- `films: Vec<ListFilm>` - Films in the list
- `tags: Vec<String>` - List tags

#### Methods

- `from_url(url: &str) -> Result<Self>` - Create from full URL
- `get_comments() -> Result<Vec<ListComment>>` - Get list comments
- `get_film_by_position(position: u32) -> Option<&ListFilm>` - Get film at position
- `get_films_by_year(year: i32) -> Vec<&ListFilm>` - Filter by year

## Utilities

### Parser Functions

```rust
use rustboxd::utils::*;

// Parse shorthand numbers
let count = extract_and_convert_shorthand("1.2K"); // Returns 1200

// Parse ratings
let rating = parse_rating("4.5/5"); // Returns Some(4.5)

// Parse runtime
let minutes = parse_runtime("2h 22m"); // Returns Some(142)

// Clean text
let clean = clean_text("  Multiple   spaces  "); // Returns "Multiple spaces"

// Extract slugs
let slug = extract_film_slug("https://letterboxd.com/film/the-matrix/");
```

### Validation Functions

```rust
use rustboxd::utils::*;

// Validate inputs
assert!(is_valid_username("user123"));
assert!(is_valid_rating(4.5));
assert!(is_valid_year(2024));

// Sanitize for URLs
let slug = sanitize_for_url("The Matrix: Reloaded"); // Returns "the-matrix-reloaded"

// Check safety
assert!(is_safe_text("Normal text"));
assert!(!is_safe_text("<script>alert('xss')</script>"));
```

### Transform Functions

```rust
use rustboxd::utils::*;

// Build URLs
let url = build_user_url("username");
let film_url = build_film_url("the-matrix");
let search_url = build_search_url("query", Some("films"));

// Handle pagination
let ajax_url = get_ajax_url("https://letterboxd.com/films/");
let page_url = add_page_to_url(&ajax_url, 2);
```

## Best Practices

### Error Handling

Always handle errors appropriately:

```rust
use rustboxd::{User, Error};

async fn get_user_safely(username: &str) -> Option<User> {
    match User::new(username).await {
        Ok(user) => Some(user),
        Err(Error::InvalidUsername(_)) => {
            eprintln!("Invalid username format");
            None
        }
        Err(Error::PageLoad { .. }) => {
            eprintln!("User not found or network error");
            None
        }
        Err(e) => {
            eprintln!("Unexpected error: {}", e);
            None
        }
    }
}
```

### Rate Limiting

Implement rate limiting to respect Letterboxd's servers:

```rust
use tokio::time::{sleep, Duration};

async fn fetch_multiple_users(usernames: &[&str]) -> Vec<Option<User>> {
    let mut results = Vec::new();
    
    for username in usernames {
        match User::new(username).await {
            Ok(user) => results.push(Some(user)),
            Err(_) => results.push(None),
        }
        
        // Rate limit: wait 1 second between requests
        sleep(Duration::from_secs(1)).await;
    }
    
    results
}
```

### Memory Efficiency

For large datasets, consider streaming or pagination:

```rust
async fn process_large_list(list_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let list = List::from_url(list_url).await?;
    
    // Process films in chunks to avoid memory issues
    for chunk in list.films.chunks(100) {
        for film in chunk {
            // Process each film
            println!("Processing: {}", film.title);
        }
        
        // Optional: yield to other tasks
        tokio::task::yield_now().await;
    }
    
    Ok(())
}
```

### Caching

Consider caching frequently accessed data:

```rust
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

struct UserCache {
    cache: Arc<Mutex<HashMap<String, User>>>,
}

impl UserCache {
    fn new() -> Self {
        Self {
            cache: Arc::new(Mutex::new(HashMap::new())),
        }
    }
    
    async fn get_user(&self, username: &str) -> Result<User, Box<dyn std::error::Error>> {
        let mut cache = self.cache.lock().await;
        
        if let Some(user) = cache.get(username) {
            return Ok(user.clone());
        }
        
        let user = User::new(username).await?;
        cache.insert(username.to_string(), user.clone());
        Ok(user)
    }
}
```