Spaces:
Paused
Paused
PNRxA commited on
Commit ·
bc0c819
1
Parent(s): 66e881f
added latency logging
Browse files- api/src/id/mod.rs +2 -2
- api/src/poster/generate.rs +12 -1
- api/src/poster/serve.rs +74 -1
- api/src/services/ratings.rs +34 -4
api/src/id/mod.rs
CHANGED
|
@@ -125,7 +125,7 @@ async fn resolve_imdb(imdb_id: &str, tmdb: &TmdbClient) -> Result<ResolvedId, Ap
|
|
| 125 |
release_date: tv.first_air_date.clone(),
|
| 126 |
});
|
| 127 |
}
|
| 128 |
-
Err(AppError::IdNotFound(imdb_id
|
| 129 |
}
|
| 130 |
|
| 131 |
async fn resolve_tmdb(id_value: &str, tmdb: &TmdbClient) -> Result<ResolvedId, AppError> {
|
|
@@ -282,5 +282,5 @@ async fn resolve_tvdb(tvdb_id: &str, tmdb: &TmdbClient) -> Result<ResolvedId, Ap
|
|
| 282 |
release_date: details.release_date,
|
| 283 |
});
|
| 284 |
}
|
| 285 |
-
Err(AppError::IdNotFound(tvdb_id
|
| 286 |
}
|
|
|
|
| 125 |
release_date: tv.first_air_date.clone(),
|
| 126 |
});
|
| 127 |
}
|
| 128 |
+
Err(AppError::IdNotFound(format!("{imdb_id} (not found on TMDB)")))
|
| 129 |
}
|
| 130 |
|
| 131 |
async fn resolve_tmdb(id_value: &str, tmdb: &TmdbClient) -> Result<ResolvedId, AppError> {
|
|
|
|
| 282 |
release_date: details.release_date,
|
| 283 |
});
|
| 284 |
}
|
| 285 |
+
Err(AppError::IdNotFound(format!("{tvdb_id} (not found on TMDB via TVDB lookup)")))
|
| 286 |
}
|
api/src/poster/generate.rs
CHANGED
|
@@ -12,6 +12,9 @@ use crate::services::db::STYLE_VERTICAL;
|
|
| 12 |
use crate::services::ratings::RatingBadge;
|
| 13 |
use crate::services::tmdb::TmdbClient;
|
| 14 |
|
|
|
|
|
|
|
|
|
|
| 15 |
const BADGE_SPACING: u32 = 10;
|
| 16 |
const BADGE_BOTTOM_MARGIN: u32 = 10;
|
| 17 |
const BADGE_TOP_MARGIN: u32 = 20;
|
|
@@ -123,7 +126,15 @@ pub async fn generate_poster(params: PosterParams<'_>) -> Result<Vec<u8>, AppErr
|
|
| 123 |
.await
|
| 124 |
.map_err(|e| AppError::Other(e.to_string()))??;
|
| 125 |
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
Ok(buf)
|
| 128 |
}
|
| 129 |
|
|
|
|
| 12 |
use crate::services::ratings::RatingBadge;
|
| 13 |
use crate::services::tmdb::TmdbClient;
|
| 14 |
|
| 15 |
+
/// Threshold (ms) above which render is logged as slow.
|
| 16 |
+
const SLOW_RENDER_MS: u64 = 2000;
|
| 17 |
+
|
| 18 |
const BADGE_SPACING: u32 = 10;
|
| 19 |
const BADGE_BOTTOM_MARGIN: u32 = 10;
|
| 20 |
const BADGE_TOP_MARGIN: u32 = 20;
|
|
|
|
| 126 |
.await
|
| 127 |
.map_err(|e| AppError::Other(e.to_string()))??;
|
| 128 |
|
| 129 |
+
let render_ms = start.elapsed().as_millis() as u64;
|
| 130 |
+
tracing::debug!(elapsed_ms = render_ms, "poster render complete");
|
| 131 |
+
if render_ms > SLOW_RENDER_MS {
|
| 132 |
+
tracing::warn!(
|
| 133 |
+
poster_path,
|
| 134 |
+
render_ms,
|
| 135 |
+
"slow generate_poster"
|
| 136 |
+
);
|
| 137 |
+
}
|
| 138 |
Ok(buf)
|
| 139 |
}
|
| 140 |
|
api/src/poster/serve.rs
CHANGED
|
@@ -18,6 +18,9 @@ use crate::AppState;
|
|
| 18 |
/// Hardcoded CDN TTL for placeholder/fallback images (1 day).
|
| 19 |
pub const PLACEHOLDER_CDN_MAX_AGE: u64 = 86400;
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
/// Alias for the unified image kind enum used throughout the serve layer.
|
| 22 |
pub type ImageKind = cache::ImageType;
|
| 23 |
|
|
@@ -277,7 +280,10 @@ async fn resolve_with_ratings(
|
|
| 277 |
id_type: IdType,
|
| 278 |
id_value: &str,
|
| 279 |
) -> Result<(id::ResolvedId, ratings::RatingsResult, CrossIdInfo), AppError> {
|
|
|
|
| 280 |
let resolved = id::resolve(id_type, id_value, &state.tmdb, &state.id_cache).await?;
|
|
|
|
|
|
|
| 281 |
let ratings_result = ratings::fetch_ratings(
|
| 282 |
&resolved,
|
| 283 |
&state.tmdb,
|
|
@@ -286,6 +292,15 @@ async fn resolve_with_ratings(
|
|
| 286 |
&state.ratings_cache,
|
| 287 |
)
|
| 288 |
.await;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
let cross_ids = CrossIdInfo::from_resolved(&resolved, &ratings_result);
|
| 290 |
Ok((resolved, ratings_result, cross_ids))
|
| 291 |
}
|
|
@@ -474,6 +489,7 @@ pub async fn handle_inner(
|
|
| 474 |
mut settings: PosterSettings,
|
| 475 |
image_size: Option<ImageSize>,
|
| 476 |
) -> Result<(Bytes, Option<String>), AppError> {
|
|
|
|
| 477 |
let id_type = IdType::parse(id_type_str)?;
|
| 478 |
let id_value = id_value_jpg.strip_suffix(".jpg").unwrap_or(id_value_jpg);
|
| 479 |
|
|
@@ -490,7 +506,9 @@ pub async fn handle_inner(
|
|
| 490 |
// Fast path (non-fanart): try to reconstruct the cache key from SQLite-stored
|
| 491 |
// available sources, avoiding external API calls entirely on cache hits.
|
| 492 |
if !use_fanart {
|
|
|
|
| 493 |
if let Some(available) = read_available_ratings_cached(state, &id_key).await {
|
|
|
|
| 494 |
let ratings_suffix = ratings::badges_suffix_from_available(&available, &settings.ratings_order, settings.ratings_limit);
|
| 495 |
let suffix = settings_cache_suffix_with_ratings(&settings, ImageKind::Poster, image_size, &ratings_suffix);
|
| 496 |
let cache_value = format!("{id_value}{suffix}");
|
|
@@ -502,20 +520,47 @@ pub async fn handle_inner(
|
|
| 502 |
let id_value = id_value.to_string();
|
| 503 |
let cache_suffix = cache_suffix.clone();
|
| 504 |
let settings = settings.clone();
|
|
|
|
| 505 |
if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
|
| 506 |
trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
|
| 507 |
}).await? {
|
|
|
|
|
|
|
| 508 |
let release_date = cache::read_meta_db(&state.db, &cache_key).await;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
return Ok((bytes, release_date));
|
| 510 |
}
|
| 511 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
}
|
| 513 |
}
|
| 514 |
|
| 515 |
// Slow path: resolve ID and fetch ratings (moka-cached, so still fast on
|
| 516 |
// repeat requests within the 30-min TTL).
|
|
|
|
|
|
|
| 517 |
let (resolved, ratings_result, cross_ids) =
|
| 518 |
resolve_with_ratings(state, id_type, id_value).await?;
|
|
|
|
| 519 |
|
| 520 |
// Persist available sources for future fast-path lookups (always write,
|
| 521 |
// even with external_cache_only — this is an optimization index, not a
|
|
@@ -572,14 +617,26 @@ pub async fn handle_inner(
|
|
| 572 |
let id_value = id_value.to_string();
|
| 573 |
let cache_suffix = cache_suffix.clone();
|
| 574 |
let settings = settings.clone();
|
|
|
|
| 575 |
if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
|
| 576 |
trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
|
| 577 |
}).await? {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
return Ok((bytes, release_date));
|
| 579 |
}
|
| 580 |
}
|
| 581 |
|
| 582 |
// Request coalescing — concurrent requests for the same poster share one generation
|
|
|
|
| 583 |
let state2 = state.clone();
|
| 584 |
let cache_key2 = cache_key.clone();
|
| 585 |
let cache_path2 = cache_path.clone();
|
|
@@ -600,6 +657,18 @@ pub async fn handle_inner(
|
|
| 600 |
.await
|
| 601 |
.map_err(|e| AppError::Other(e.to_string()))?;
|
| 602 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
// Insert into memory cache
|
| 604 |
state
|
| 605 |
.poster_mem_cache
|
|
@@ -947,7 +1016,11 @@ async fn generate_poster_with_source(
|
|
| 947 |
let poster_path = resolved
|
| 948 |
.poster_path
|
| 949 |
.as_deref()
|
| 950 |
-
.ok_or_else(||
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
|
| 952 |
// Try to fetch poster bytes from fanart.tv if configured
|
| 953 |
let fanart_result = if &*settings.poster_source == SOURCE_FANART || settings.lang_override {
|
|
|
|
| 18 |
/// Hardcoded CDN TTL for placeholder/fallback images (1 day).
|
| 19 |
pub const PLACEHOLDER_CDN_MAX_AGE: u64 = 86400;
|
| 20 |
|
| 21 |
+
/// Threshold (ms) above which requests are logged as slow.
|
| 22 |
+
const SLOW_REQUEST_MS: u64 = 2000;
|
| 23 |
+
|
| 24 |
/// Alias for the unified image kind enum used throughout the serve layer.
|
| 25 |
pub type ImageKind = cache::ImageType;
|
| 26 |
|
|
|
|
| 280 |
id_type: IdType,
|
| 281 |
id_value: &str,
|
| 282 |
) -> Result<(id::ResolvedId, ratings::RatingsResult, CrossIdInfo), AppError> {
|
| 283 |
+
let id_resolve_start = Instant::now();
|
| 284 |
let resolved = id::resolve(id_type, id_value, &state.tmdb, &state.id_cache).await?;
|
| 285 |
+
let id_resolve_ms = id_resolve_start.elapsed().as_millis() as u64;
|
| 286 |
+
|
| 287 |
let ratings_result = ratings::fetch_ratings(
|
| 288 |
&resolved,
|
| 289 |
&state.tmdb,
|
|
|
|
| 292 |
&state.ratings_cache,
|
| 293 |
)
|
| 294 |
.await;
|
| 295 |
+
|
| 296 |
+
if id_resolve_ms > SLOW_REQUEST_MS {
|
| 297 |
+
tracing::warn!(
|
| 298 |
+
id = %id_value,
|
| 299 |
+
id_resolve_ms,
|
| 300 |
+
"slow id resolution"
|
| 301 |
+
);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
let cross_ids = CrossIdInfo::from_resolved(&resolved, &ratings_result);
|
| 305 |
Ok((resolved, ratings_result, cross_ids))
|
| 306 |
}
|
|
|
|
| 489 |
mut settings: PosterSettings,
|
| 490 |
image_size: Option<ImageSize>,
|
| 491 |
) -> Result<(Bytes, Option<String>), AppError> {
|
| 492 |
+
let request_start = Instant::now();
|
| 493 |
let id_type = IdType::parse(id_type_str)?;
|
| 494 |
let id_value = id_value_jpg.strip_suffix(".jpg").unwrap_or(id_value_jpg);
|
| 495 |
|
|
|
|
| 506 |
// Fast path (non-fanart): try to reconstruct the cache key from SQLite-stored
|
| 507 |
// available sources, avoiding external API calls entirely on cache hits.
|
| 508 |
if !use_fanart {
|
| 509 |
+
let fast_path_start = Instant::now();
|
| 510 |
if let Some(available) = read_available_ratings_cached(state, &id_key).await {
|
| 511 |
+
let available_ratings_ms = fast_path_start.elapsed().as_millis() as u64;
|
| 512 |
let ratings_suffix = ratings::badges_suffix_from_available(&available, &settings.ratings_order, settings.ratings_limit);
|
| 513 |
let suffix = settings_cache_suffix_with_ratings(&settings, ImageKind::Poster, image_size, &ratings_suffix);
|
| 514 |
let cache_value = format!("{id_value}{suffix}");
|
|
|
|
| 520 |
let id_value = id_value.to_string();
|
| 521 |
let cache_suffix = cache_suffix.clone();
|
| 522 |
let settings = settings.clone();
|
| 523 |
+
let cache_check_start = Instant::now();
|
| 524 |
if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
|
| 525 |
trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
|
| 526 |
}).await? {
|
| 527 |
+
let cache_check_ms = cache_check_start.elapsed().as_millis() as u64;
|
| 528 |
+
let meta_start = Instant::now();
|
| 529 |
let release_date = cache::read_meta_db(&state.db, &cache_key).await;
|
| 530 |
+
let meta_ms = meta_start.elapsed().as_millis() as u64;
|
| 531 |
+
let total_ms = request_start.elapsed().as_millis() as u64;
|
| 532 |
+
if total_ms > SLOW_REQUEST_MS {
|
| 533 |
+
tracing::warn!(
|
| 534 |
+
id = %id_key,
|
| 535 |
+
total_ms,
|
| 536 |
+
available_ratings_ms,
|
| 537 |
+
cache_check_ms,
|
| 538 |
+
meta_db_ms = meta_ms,
|
| 539 |
+
"slow poster request (fast path hit)"
|
| 540 |
+
);
|
| 541 |
+
}
|
| 542 |
return Ok((bytes, release_date));
|
| 543 |
}
|
| 544 |
}
|
| 545 |
+
let total_fast_path_ms = fast_path_start.elapsed().as_millis() as u64;
|
| 546 |
+
if total_fast_path_ms > SLOW_REQUEST_MS {
|
| 547 |
+
tracing::warn!(
|
| 548 |
+
id = %id_key,
|
| 549 |
+
total_fast_path_ms,
|
| 550 |
+
available_ratings_ms,
|
| 551 |
+
"slow fast path — cache miss, falling to slow path"
|
| 552 |
+
);
|
| 553 |
+
}
|
| 554 |
}
|
| 555 |
}
|
| 556 |
|
| 557 |
// Slow path: resolve ID and fetch ratings (moka-cached, so still fast on
|
| 558 |
// repeat requests within the 30-min TTL).
|
| 559 |
+
let slow_path_start = Instant::now();
|
| 560 |
+
let resolve_start = Instant::now();
|
| 561 |
let (resolved, ratings_result, cross_ids) =
|
| 562 |
resolve_with_ratings(state, id_type, id_value).await?;
|
| 563 |
+
let resolve_ms = resolve_start.elapsed().as_millis() as u64;
|
| 564 |
|
| 565 |
// Persist available sources for future fast-path lookups (always write,
|
| 566 |
// even with external_cache_only — this is an optimization index, not a
|
|
|
|
| 617 |
let id_value = id_value.to_string();
|
| 618 |
let cache_suffix = cache_suffix.clone();
|
| 619 |
let settings = settings.clone();
|
| 620 |
+
let slow_cache_check_start = Instant::now();
|
| 621 |
if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
|
| 622 |
trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
|
| 623 |
}).await? {
|
| 624 |
+
let total_ms = request_start.elapsed().as_millis() as u64;
|
| 625 |
+
if total_ms > SLOW_REQUEST_MS {
|
| 626 |
+
tracing::warn!(
|
| 627 |
+
id = %id_key,
|
| 628 |
+
total_ms,
|
| 629 |
+
resolve_ms,
|
| 630 |
+
cache_check_ms = slow_cache_check_start.elapsed().as_millis() as u64,
|
| 631 |
+
"slow poster request (slow path cache hit)"
|
| 632 |
+
);
|
| 633 |
+
}
|
| 634 |
return Ok((bytes, release_date));
|
| 635 |
}
|
| 636 |
}
|
| 637 |
|
| 638 |
// Request coalescing — concurrent requests for the same poster share one generation
|
| 639 |
+
let generate_start = Instant::now();
|
| 640 |
let state2 = state.clone();
|
| 641 |
let cache_key2 = cache_key.clone();
|
| 642 |
let cache_path2 = cache_path.clone();
|
|
|
|
| 657 |
.await
|
| 658 |
.map_err(|e| AppError::Other(e.to_string()))?;
|
| 659 |
|
| 660 |
+
let total_ms = request_start.elapsed().as_millis() as u64;
|
| 661 |
+
if total_ms > SLOW_REQUEST_MS {
|
| 662 |
+
tracing::warn!(
|
| 663 |
+
id = %id_key,
|
| 664 |
+
total_ms,
|
| 665 |
+
resolve_ms,
|
| 666 |
+
generate_ms = generate_start.elapsed().as_millis() as u64,
|
| 667 |
+
slow_path_ms = slow_path_start.elapsed().as_millis() as u64,
|
| 668 |
+
"slow poster request (generated)"
|
| 669 |
+
);
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
// Insert into memory cache
|
| 673 |
state
|
| 674 |
.poster_mem_cache
|
|
|
|
| 1016 |
let poster_path = resolved
|
| 1017 |
.poster_path
|
| 1018 |
.as_deref()
|
| 1019 |
+
.ok_or_else(|| {
|
| 1020 |
+
let id_desc = resolved.imdb_id.as_deref()
|
| 1021 |
+
.unwrap_or_else(|| "unknown");
|
| 1022 |
+
AppError::Other(format!("no poster available for {id_desc} / tmdb:{} (TMDB has no poster_path)", resolved.tmdb_id))
|
| 1023 |
+
})?;
|
| 1024 |
|
| 1025 |
// Try to fetch poster bytes from fanart.tv if configured
|
| 1026 |
let fanart_result = if &*settings.poster_source == SOURCE_FANART || settings.lang_override {
|
api/src/services/ratings.rs
CHANGED
|
@@ -5,6 +5,9 @@ use crate::services::tmdb::TmdbClient;
|
|
| 5 |
use image::Rgba;
|
| 6 |
use serde::Deserialize;
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 9 |
pub enum RatingSource {
|
| 10 |
Imdb,
|
|
@@ -155,13 +158,40 @@ async fn fetch_ratings_inner(
|
|
| 155 |
omdb: Option<&OmdbClient>,
|
| 156 |
mdblist: Option<&MdblistClient>,
|
| 157 |
) -> RatingsResult {
|
| 158 |
-
let
|
| 159 |
-
let omdb_fut = fetch_omdb_ratings(resolved.imdb_id.as_deref(), omdb);
|
| 160 |
-
let mdblist_fut = fetch_mdblist_ratings(resolved, mdblist);
|
| 161 |
|
| 162 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
tokio::join!(tmdb_fut, omdb_fut, mdblist_fut);
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
let (mdblist_badges, mdb_tmdb_id, mdb_tvdb_id, mdb_imdb_id) = match mdblist_raw {
|
| 166 |
Some((badges, tmdb_id, tvdb_id, imdb_id)) => (Some(badges), tmdb_id, tvdb_id, imdb_id),
|
| 167 |
None => (None, None, None, None),
|
|
|
|
| 5 |
use image::Rgba;
|
| 6 |
use serde::Deserialize;
|
| 7 |
|
| 8 |
+
/// Threshold (ms) above which ratings fetches are logged as slow.
|
| 9 |
+
const SLOW_RATINGS_MS: u64 = 2000;
|
| 10 |
+
|
| 11 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 12 |
pub enum RatingSource {
|
| 13 |
Imdb,
|
|
|
|
| 158 |
omdb: Option<&OmdbClient>,
|
| 159 |
mdblist: Option<&MdblistClient>,
|
| 160 |
) -> RatingsResult {
|
| 161 |
+
let ratings_start = std::time::Instant::now();
|
|
|
|
|
|
|
| 162 |
|
| 163 |
+
let tmdb_fut = async {
|
| 164 |
+
let start = std::time::Instant::now();
|
| 165 |
+
let result = fetch_tmdb_rating(resolved, tmdb).await;
|
| 166 |
+
(result, start.elapsed())
|
| 167 |
+
};
|
| 168 |
+
let omdb_fut = async {
|
| 169 |
+
let start = std::time::Instant::now();
|
| 170 |
+
let result = fetch_omdb_ratings(resolved.imdb_id.as_deref(), omdb).await;
|
| 171 |
+
(result, start.elapsed())
|
| 172 |
+
};
|
| 173 |
+
let mdblist_fut = async {
|
| 174 |
+
let start = std::time::Instant::now();
|
| 175 |
+
let result = fetch_mdblist_ratings(resolved, mdblist).await;
|
| 176 |
+
(result, start.elapsed())
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
let ((tmdb_badges, tmdb_dur), (omdb_badges, omdb_dur), (mdblist_raw, mdblist_dur)) =
|
| 180 |
tokio::join!(tmdb_fut, omdb_fut, mdblist_fut);
|
| 181 |
|
| 182 |
+
let ratings_elapsed = ratings_start.elapsed().as_millis() as u64;
|
| 183 |
+
if ratings_elapsed > SLOW_RATINGS_MS {
|
| 184 |
+
tracing::warn!(
|
| 185 |
+
tmdb_id = resolved.tmdb_id,
|
| 186 |
+
imdb_id = ?resolved.imdb_id,
|
| 187 |
+
total_ms = ratings_elapsed,
|
| 188 |
+
tmdb_ms = tmdb_dur.as_millis() as u64,
|
| 189 |
+
omdb_ms = omdb_dur.as_millis() as u64,
|
| 190 |
+
mdblist_ms = mdblist_dur.as_millis() as u64,
|
| 191 |
+
"slow ratings fetch"
|
| 192 |
+
);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
let (mdblist_badges, mdb_tmdb_id, mdb_tvdb_id, mdb_imdb_id) = match mdblist_raw {
|
| 196 |
Some((badges, tmdb_id, tvdb_id, imdb_id)) => (Some(badges), tmdb_id, tvdb_id, imdb_id),
|
| 197 |
None => (None, None, None, None),
|