Skip to content

Commit 3d081c4

Browse files
committed
feat(playmatch): support all metadata providers in commands and list
1 parent be71f52 commit 3d081c4

11 files changed

Lines changed: 617 additions & 161 deletions

File tree

src/abstraction/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pub mod activity_data;
22
pub mod command;
33
pub mod components_v2;
4-
pub mod igdb;
54
pub mod playmatch;
5+
pub mod providers;

src/abstraction/playmatch.rs

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::abstraction::command::{CommandContext, paginate};
22
use crate::abstraction::components_v2::{self, Status};
3+
use crate::abstraction::providers::{ALL_PROVIDERS, short_label};
34
use crate::util::create_discord_markdown_table;
45
use log::warn;
56
use playmatch_client::Error;
67
use playmatch_client::types::{
7-
CompanyMetadataResponse, ExternalMetadata, MetadataMatchType, MetadataProvider,
8-
PlatformMetadataResponse,
8+
CompanyMetadataResponse, ExternalMetadata, MetadataMatchType, PlatformMetadataResponse,
99
};
1010
use reqwest::StatusCode;
1111
use uuid::Uuid;
@@ -99,43 +99,29 @@ where
9999
const SUCCESS_EMOJI: &str = "✅";
100100
const FAILURE_EMOJI: &str = "❌";
101101

102-
let default_headers: Vec<String> = vec![
103-
"Name".to_string(),
104-
"IGDB Match".to_string(),
105-
"IGDB ID".to_string(),
106-
];
102+
let mut default_headers: Vec<String> = Vec::with_capacity(1 + ALL_PROVIDERS.len());
103+
default_headers.push("Name".to_string());
104+
for &provider in ALL_PROVIDERS {
105+
default_headers.push(short_label(provider).to_string());
106+
}
107107

108108
let mapped_markdown_rows = input
109109
.into_iter()
110110
.map(|c| {
111111
let external_metadata = c.get_external_metadata();
112-
let igdb_mapping = external_metadata
113-
.iter()
114-
.find(|ex| ex.provider_name == MetadataProvider::Igdb);
115-
116-
let is_igdb_matched = if let Some(igdb_mapping) = igdb_mapping {
117-
match igdb_mapping.match_type {
118-
MetadataMatchType::Automatic | MetadataMatchType::Manual => true,
119-
MetadataMatchType::Failed | MetadataMatchType::None => false,
120-
}
121-
} else {
122-
false
123-
};
124-
125-
let igdb_id = igdb_mapping
126-
.and_then(|ex| ex.provider_id.clone())
127-
.unwrap_or("".to_string());
128-
129-
vec![
130-
c.get_name(),
131-
if is_igdb_matched {
132-
SUCCESS_EMOJI
133-
} else {
134-
FAILURE_EMOJI
135-
}
136-
.to_string(),
137-
igdb_id,
138-
]
112+
let mut row = Vec::with_capacity(1 + ALL_PROVIDERS.len());
113+
row.push(c.get_name());
114+
for &provider in ALL_PROVIDERS {
115+
let matched = external_metadata.iter().any(|ex| {
116+
ex.provider_name == provider
117+
&& matches!(
118+
ex.match_type,
119+
MetadataMatchType::Automatic | MetadataMatchType::Manual
120+
)
121+
});
122+
row.push(if matched { SUCCESS_EMOJI } else { FAILURE_EMOJI }.to_string());
123+
}
124+
row
139125
})
140126
.collect::<Vec<Vec<String>>>();
141127

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,9 @@
11
use chrono::{DateTime, Utc};
22
use log::{debug, warn};
33
use playmatch_client::Client;
4+
use playmatch_client::types::MetadataProvider;
45

5-
pub struct IgdbGameInfo {
6-
pub name: String,
7-
pub page_url: String,
8-
pub summary: Option<String>,
9-
pub first_release_date: Option<DateTime<Utc>>,
10-
pub cover_url: Option<String>,
11-
pub screenshot_urls: Vec<String>,
12-
}
13-
14-
pub struct IgdbCompanyInfo {
15-
pub name: String,
16-
pub page_url: Option<String>,
17-
pub logo_url: Option<String>,
18-
pub description: Option<String>,
19-
}
20-
21-
pub struct IgdbPlatformInfo {
22-
pub name: String,
23-
pub page_url: String,
24-
pub logo_url: Option<String>,
25-
pub summary: Option<String>,
26-
}
27-
28-
const SUMMARY_MAX: usize = 280;
6+
use super::{ProviderCompanyInfo, ProviderGameInfo, ProviderPlatformInfo, truncate_summary};
297

308
fn normalize_image_url(raw: &str, size: &str) -> String {
319
let https = if let Some(rest) = raw.strip_prefix("//") {
@@ -38,20 +16,11 @@ fn normalize_image_url(raw: &str, size: &str) -> String {
3816
https.replacen("t_thumb", size, 1)
3917
}
4018

41-
fn truncate_summary(s: &str) -> String {
42-
if s.chars().count() <= SUMMARY_MAX {
43-
s.to_string()
44-
} else {
45-
let truncated: String = s.chars().take(SUMMARY_MAX - 1).collect();
46-
format!("{truncated}…")
47-
}
48-
}
49-
5019
fn timestamp_to_datetime(ts: i64) -> Option<DateTime<Utc>> {
5120
DateTime::<Utc>::from_timestamp(ts, 0)
5221
}
5322

54-
pub async fn fetch_game(client: &Client, provider_id: &str) -> Option<IgdbGameInfo> {
23+
pub async fn fetch_game(client: &Client, provider_id: &str) -> Option<ProviderGameInfo> {
5524
let id: i32 = match provider_id.parse() {
5625
Ok(id) => id,
5726
Err(e) => {
@@ -110,17 +79,18 @@ pub async fn fetch_game(client: &Client, provider_id: &str) -> Option<IgdbGameIn
11079
}
11180
}
11281

113-
Some(IgdbGameInfo {
82+
Some(ProviderGameInfo {
83+
provider: MetadataProvider::Igdb,
11484
name: game.name,
115-
page_url: game.url,
85+
page_url: Some(game.url),
11686
summary: game.summary.map(|s| truncate_summary(&s)),
11787
first_release_date: game.first_release_date.and_then(timestamp_to_datetime),
11888
cover_url,
11989
screenshot_urls,
12090
})
12191
}
12292

123-
pub async fn fetch_company(client: &Client, provider_id: &str) -> Option<IgdbCompanyInfo> {
93+
pub async fn fetch_company(client: &Client, provider_id: &str) -> Option<ProviderCompanyInfo> {
12494
let id: i32 = match provider_id.parse() {
12595
Ok(id) => id,
12696
Err(e) => {
@@ -157,15 +127,16 @@ pub async fn fetch_company(client: &Client, provider_id: &str) -> Option<IgdbCom
157127
None => None,
158128
};
159129

160-
Some(IgdbCompanyInfo {
130+
Some(ProviderCompanyInfo {
131+
provider: MetadataProvider::Igdb,
161132
name: company.name,
162133
page_url: company.url,
163134
logo_url,
164135
description: company.description.map(|s| truncate_summary(&s)),
165136
})
166137
}
167138

168-
pub async fn fetch_platform(client: &Client, provider_id: &str) -> Option<IgdbPlatformInfo> {
139+
pub async fn fetch_platform(client: &Client, provider_id: &str) -> Option<ProviderPlatformInfo> {
169140
let id: i32 = match provider_id.parse() {
170141
Ok(id) => id,
171142
Err(e) => {
@@ -202,9 +173,10 @@ pub async fn fetch_platform(client: &Client, provider_id: &str) -> Option<IgdbPl
202173
None => None,
203174
};
204175

205-
Some(IgdbPlatformInfo {
176+
Some(ProviderPlatformInfo {
177+
provider: MetadataProvider::Igdb,
206178
name: platform.name,
207-
page_url: platform.url,
179+
page_url: Some(platform.url),
208180
logo_url,
209181
summary: platform.summary.map(|s| truncate_summary(&s)),
210182
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use log::warn;
2+
use playmatch_client::Client;
3+
use playmatch_client::types::{LbGameImage, MetadataProvider};
4+
5+
use super::{ProviderGameInfo, truncate_summary};
6+
7+
fn is_cover(img: &LbGameImage) -> bool {
8+
let t = img.image_type.to_ascii_lowercase();
9+
t.contains("box - front") || t.contains("clear logo")
10+
}
11+
12+
fn is_screenshot(img: &LbGameImage) -> bool {
13+
img.image_type.to_ascii_lowercase().contains("screenshot")
14+
}
15+
16+
pub async fn fetch_game(client: &Client, provider_id: &str) -> Option<ProviderGameInfo> {
17+
let id: i64 = match provider_id.parse() {
18+
Ok(id) => id,
19+
Err(e) => {
20+
warn!("LaunchBox game provider_id '{provider_id}' is not a valid i64: {e}");
21+
return None;
22+
}
23+
};
24+
25+
let game = match client.get_lb_game_by_id().id(id).send().await {
26+
Ok(resp) => resp.into_inner(),
27+
Err(e) => {
28+
warn!("LaunchBox game lookup failed for id {id}: {e}");
29+
return None;
30+
}
31+
};
32+
33+
let (cover_url, screenshot_urls) = match client.get_lb_game_images().game_id(id).send().await {
34+
Ok(resp) => {
35+
let images = resp.into_inner();
36+
let cover = images
37+
.iter()
38+
.find(|i| is_cover(i))
39+
.or_else(|| images.first())
40+
.map(|i| i.file_name.clone());
41+
let screenshots: Vec<String> = images
42+
.iter()
43+
.filter(|i| is_screenshot(i))
44+
.take(3)
45+
.map(|i| i.file_name.clone())
46+
.collect();
47+
(cover, screenshots)
48+
}
49+
Err(e) => {
50+
warn!("LaunchBox image lookup failed for id {id}: {e}");
51+
(None, Vec::new())
52+
}
53+
};
54+
55+
Some(ProviderGameInfo {
56+
provider: MetadataProvider::LaunchBox,
57+
name: game.name,
58+
page_url: game.wikipedia_url,
59+
summary: game.overview.map(|s| truncate_summary(&s)),
60+
first_release_date: None,
61+
cover_url,
62+
screenshot_urls,
63+
})
64+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use log::warn;
2+
use playmatch_client::Client;
3+
use playmatch_client::types::MetadataProvider;
4+
5+
use super::{ProviderGameInfo, truncate_summary};
6+
7+
pub async fn fetch_game(client: &Client, provider_id: &str) -> Option<ProviderGameInfo> {
8+
let id: i64 = match provider_id.parse() {
9+
Ok(id) => id,
10+
Err(e) => {
11+
warn!("MobyGames game provider_id '{provider_id}' is not a valid i64: {e}");
12+
return None;
13+
}
14+
};
15+
16+
let game = match client.get_mg_game_by_id().id(id).send().await {
17+
Ok(resp) => resp.into_inner(),
18+
Err(e) => {
19+
warn!("MobyGames game lookup failed for id {id}: {e}");
20+
return None;
21+
}
22+
};
23+
24+
let cover_url = game.sample_cover.as_ref().map(|c| c.image.clone());
25+
let screenshot_urls = game
26+
.sample_screenshots
27+
.as_ref()
28+
.map(|ss| ss.iter().take(3).map(|s| s.image.clone()).collect())
29+
.unwrap_or_default();
30+
31+
Some(ProviderGameInfo {
32+
provider: MetadataProvider::MobyGames,
33+
name: game.title,
34+
page_url: game.moby_url,
35+
summary: game.description.map(|s| truncate_summary(&s)),
36+
first_release_date: None,
37+
cover_url,
38+
screenshot_urls,
39+
})
40+
}

0 commit comments

Comments
 (0)