Skip to content

Commit 874a03b

Browse files
committed
feat(hasheous): add hasheous provider with hash-based matching
1 parent df6d605 commit 874a03b

11 files changed

Lines changed: 827 additions & 0 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ MOBYGAMES_API_KEY=YOUR_MOBYGAMES_API_KEY
3737
# exposed by playmatch for this provider.
3838
# EMUREADY_ENABLED=true
3939

40+
# Hasheous provides hash-based ROM matching against public signature DATs
41+
# (No-Intros, Redump, TOSEC, MAMEArcade, etc.). Set the flag below to join
42+
# Hasheous to the daily match cycle. The hash-lookup endpoints are open and
43+
# require no API key.
44+
# HASHEOUS_ENABLED=true
45+
4046
# OpenVGDB is a bulk metadata source distributed as a small SQLite snapshot
4147
# (~9 MB zip). Setting OPENVGDB_ENABLED=true enables a daily download of the
4248
# OpenVGDB release zip and the import of its ROMs and RELEASES tables into

api/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ use sea_orm::{ConnectOptions, Database};
120120
use service::config::http::X_VERSION_HEADER_API;
121121
use service::db::constants::MAX_CONNECTIONS;
122122
use service::providers::emuready::EmuReadyClient;
123+
use service::providers::hasheous::HasheousClient;
123124
use service::providers::igdb::IgdbClient;
124125
use service::providers::launchbox::LaunchBoxClient;
125126
use service::providers::mobygames::MobyGamesClient;
@@ -202,6 +203,8 @@ async fn start() -> anyhow::Result<()> {
202203

203204
let er_http_client = Client::builder().cookie_store(false).build()?;
204205

206+
let hasheous_http_client = Client::builder().cookie_store(false).build()?;
207+
205208
let tgdb_http_client = Client::builder().cookie_store(false).build()?;
206209

207210
// DAT downloads use a cookieless client so hostile mirrors cannot set cookies that
@@ -223,6 +226,7 @@ async fn start() -> anyhow::Result<()> {
223226
let ra_client_opt =
224227
build_retroachievements_client(ra_http_client, redis_conn.clone(), conn.clone());
225228
let er_client_opt = build_emuready_client(er_http_client, redis_conn.clone());
229+
let hasheous_client_opt = build_hasheous_client(hasheous_http_client, redis_conn.clone());
226230
let tgdb_client_opt =
227231
build_thegamesdb_client(tgdb_http_client, redis_conn.clone(), conn.clone());
228232

@@ -265,6 +269,9 @@ async fn start() -> anyhow::Result<()> {
265269
if let Some(c) = er_client_opt.clone() {
266270
providers.push(c as Arc<dyn MetadataProvider>);
267271
}
272+
if let Some(c) = hasheous_client_opt.clone() {
273+
providers.push(c as Arc<dyn MetadataProvider>);
274+
}
268275
if let Some(c) = tgdb_client_opt.clone() {
269276
providers.push(c as Arc<dyn MetadataProvider>);
270277
}
@@ -579,6 +586,33 @@ fn build_emuready_client(
579586
}
580587
}
581588

589+
/// Returns `None` when HASHEOUS_ENABLED is unset or not "true". The Hasheous
590+
/// hash-lookup endpoints are open and unauthenticated; the flag exists so
591+
/// existing deployments do not silently start hitting an external service on
592+
/// next deploy. Matching only. No proxy routes are exposed.
593+
fn build_hasheous_client(
594+
http: Client,
595+
redis_conn: redis::aio::MultiplexedConnection,
596+
) -> Option<Arc<HasheousClient>> {
597+
let enabled = env::var("HASHEOUS_ENABLED")
598+
.unwrap_or_default()
599+
.eq_ignore_ascii_case("true");
600+
if !enabled {
601+
warn!("HASHEOUS_ENABLED not set to true, Hasheous provider disabled");
602+
return None;
603+
}
604+
match HasheousClient::new(http, redis_conn) {
605+
Ok(c) => {
606+
info!("Hasheous provider enabled");
607+
Some(Arc::new(c))
608+
}
609+
Err(e) => {
610+
warn!("Hasheous provider construction failed, disabled: {e}");
611+
None
612+
}
613+
}
614+
}
615+
582616
/// Returns `None` when TGDB_ENABLED is unset or not "true". TheGamesDB is
583617
/// match-only; no proxy routes are exposed. The bootstrap dataset is seeded
584618
/// via a migration so no daily import job runs. TGDB_API_KEY is optional;

entity/src/sea_orm_active_enums.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ pub enum MetadataProviderEnum {
122122
#[strum(serialize = "thegamesdb")]
123123
#[allow(clippy::upper_case_acronyms)]
124124
TheGamesDB,
125+
#[sea_orm(string_value = "hasheous")]
126+
Hasheous,
125127
}
126128
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize)]
127129
#[sea_orm(

migration/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mod m20260518_120100_create_thegamesdb_tables;
4545
mod m20260518_120200_bootstrap_thegamesdb_seed;
4646
mod m20260522_120000_add_too_many_files_failed_match_reason;
4747
mod m20260523_120000_drop_legacy_api_key_hash;
48+
mod m20260524_120000_add_hasheous_provider_enum_value;
4849

4950
pub struct Migrator;
5051

@@ -97,6 +98,7 @@ impl MigratorTrait for Migrator {
9798
Box::new(m20260518_120200_bootstrap_thegamesdb_seed::Migration),
9899
Box::new(m20260522_120000_add_too_many_files_failed_match_reason::Migration),
99100
Box::new(m20260523_120000_drop_legacy_api_key_hash::Migration),
101+
Box::new(m20260524_120000_add_hasheous_provider_enum_value::Migration),
100102
]
101103
}
102104
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use sea_orm::ConnectionTrait;
2+
use sea_orm_migration::prelude::*;
3+
4+
#[derive(DeriveMigrationName)]
5+
pub struct Migration;
6+
7+
#[async_trait::async_trait]
8+
impl MigrationTrait for Migration {
9+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
10+
manager
11+
.get_connection()
12+
.execute_unprepared(
13+
"ALTER TYPE metadata_provider_enum ADD VALUE IF NOT EXISTS 'hasheous';",
14+
)
15+
.await?;
16+
Ok(())
17+
}
18+
19+
// Postgres has no DROP VALUE for enum types without rewriting every column
20+
// that references it; reverting an enum-extension migration is intentionally
21+
// a no-op. To roll back, drop the dependent rows and recreate the type.
22+
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
23+
Ok(())
24+
}
25+
}

service/src/model/metadata.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ pub enum MetadataProvider {
131131
RetroAchievements,
132132
/// TheGamesDB (https://thegamesdb.net/)
133133
TheGamesDB,
134+
/// Hasheous (https://hasheous.org/)
135+
Hasheous,
134136
}
135137

136138
/// Match types for a game
@@ -252,6 +254,7 @@ impl From<MetadataProviderEnum> for MetadataProvider {
252254
MetadataProviderEnum::OpenVGDB => MetadataProvider::OpenVGDB,
253255
MetadataProviderEnum::RetroAchievements => MetadataProvider::RetroAchievements,
254256
MetadataProviderEnum::TheGamesDB => MetadataProvider::TheGamesDB,
257+
MetadataProviderEnum::Hasheous => MetadataProvider::Hasheous,
255258
}
256259
}
257260
}
@@ -268,6 +271,7 @@ impl From<MetadataProvider> for MetadataProviderEnum {
268271
MetadataProvider::OpenVGDB => MetadataProviderEnum::OpenVGDB,
269272
MetadataProvider::RetroAchievements => MetadataProviderEnum::RetroAchievements,
270273
MetadataProvider::TheGamesDB => MetadataProviderEnum::TheGamesDB,
274+
MetadataProvider::Hasheous => MetadataProviderEnum::Hasheous,
271275
}
272276
}
273277
}

0 commit comments

Comments
 (0)