@@ -2,7 +2,7 @@ use tauri::State;
22use chrono:: Utc ;
33use uuid:: Uuid ;
44use crate :: db:: { DbState , queries} ;
5- use crate :: models:: { Game , Note , Session , CreateGamePayload , UpdateGamePayload , HltbData , WeeklyPlaytime } ;
5+ use crate :: models:: { Game , Note , Session , CreateGamePayload , UpdateGamePayload , HltbData , WeeklyPlaytime , IgdbMetadata , LibraryGrowthEntry } ;
66
77#[ tauri:: command]
88pub fn get_all_games ( state : State < DbState > ) -> Result < Vec < Game > , String > {
@@ -43,6 +43,11 @@ pub fn create_game(state: State<DbState>, payload: CreateGamePayload) -> Result<
4343 custom_fields : std:: collections:: HashMap :: new ( ) ,
4444 hltb_main_mins : None ,
4545 hltb_extra_mins : None ,
46+ genre : None ,
47+ developer : None ,
48+ publisher : None ,
49+ release_year : None ,
50+ igdb_skipped : false ,
4651 } ;
4752 queries:: insert_game ( & conn, & game) . map_err ( |e| e. to_string ( ) ) ?;
4853 Ok ( game)
@@ -83,6 +88,10 @@ pub fn update_game(state: State<DbState>, payload: UpdateGamePayload) -> Result<
8388 if let Some ( exe) = payload. exe_path { game. exe_path = Some ( exe) ; }
8489 if let Some ( dir) = payload. install_dir { game. install_dir = Some ( dir) ; }
8590 if let Some ( cf) = payload. custom_fields { game. custom_fields = cf; }
91+ if let Some ( v) = payload. genre { game. genre = if v. is_empty ( ) { None } else { Some ( v) } ; }
92+ if let Some ( v) = payload. developer { game. developer = if v. is_empty ( ) { None } else { Some ( v) } ; }
93+ if let Some ( v) = payload. publisher { game. publisher = if v. is_empty ( ) { None } else { Some ( v) } ; }
94+ if let Some ( v) = payload. release_year { game. release_year = Some ( v) ; }
8695
8796 queries:: update_game ( & conn, & game) . map_err ( |e| e. to_string ( ) ) ?;
8897 Ok ( game)
@@ -373,3 +382,145 @@ pub fn batch_update_games(
373382 tx. commit ( ) . map_err ( |e| e. to_string ( ) ) ?;
374383 Ok ( ( ) )
375384}
385+
386+ #[ tauri:: command]
387+ pub fn get_library_growth ( state : State < DbState > ) -> Result < Vec < LibraryGrowthEntry > , String > {
388+ let conn = state. 0 . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
389+ let mut stmt = conn. prepare (
390+ "SELECT strftime('%Y-%m', date_added) as month, platform, COUNT(*) as cnt
391+ FROM games WHERE deleted_at IS NULL AND date_added IS NOT NULL
392+ GROUP BY month, platform ORDER BY month ASC"
393+ ) . map_err ( |e| e. to_string ( ) ) ?;
394+
395+ let rows: Vec < ( String , String , i64 ) > = stmt. query_map ( [ ] , |row| {
396+ Ok ( ( row. get ( 0 ) ?, row. get ( 1 ) ?, row. get ( 2 ) ?) )
397+ } ) . map_err ( |e| e. to_string ( ) ) ?
398+ . filter_map ( |r| r. ok ( ) )
399+ . collect ( ) ;
400+ drop ( stmt) ;
401+
402+ let mut month_map: std:: collections:: BTreeMap < String , LibraryGrowthEntry > = std:: collections:: BTreeMap :: new ( ) ;
403+ for ( month, platform, cnt) in rows {
404+ let entry = month_map. entry ( month. clone ( ) ) . or_insert ( LibraryGrowthEntry {
405+ month,
406+ steam : 0 , epic : 0 , gog : 0 , custom : 0 ,
407+ } ) ;
408+ match platform. as_str ( ) {
409+ "steam" => entry. steam += cnt,
410+ "epic" => entry. epic += cnt,
411+ "gog" => entry. gog += cnt,
412+ "custom" => entry. custom += cnt,
413+ _ => { }
414+ }
415+ }
416+
417+ Ok ( month_map. into_values ( ) . collect ( ) )
418+ }
419+
420+ #[ tauri:: command]
421+ pub fn fetch_igdb_metadata (
422+ state : State < DbState > ,
423+ game_id : String ,
424+ game_name : String ,
425+ client_id : String ,
426+ client_secret : String ,
427+ ) -> Result < Option < IgdbMetadata > , String > {
428+ if client_id. trim ( ) . is_empty ( ) || client_secret. trim ( ) . is_empty ( ) {
429+ return Err ( "IGDB client_id and client_secret are required" . to_string ( ) ) ;
430+ }
431+
432+ let token_url = "https://id.twitch.tv/oauth2/token" ;
433+ let token_body = format ! (
434+ "client_id={}&client_secret={}&grant_type=client_credentials" ,
435+ client_id. trim( ) , client_secret. trim( )
436+ ) ;
437+ let token_resp = ureq:: post ( token_url)
438+ . set ( "Content-Type" , "application/x-www-form-urlencoded" )
439+ . timeout ( std:: time:: Duration :: from_secs ( 15 ) )
440+ . send_string ( & token_body)
441+ . map_err ( |e| format ! ( "Failed to get IGDB token: {}" , e) ) ?;
442+ let token_json: serde_json:: Value = serde_json:: from_str (
443+ & token_resp. into_string ( ) . map_err ( |e| e. to_string ( ) ) ?
444+ ) . map_err ( |e| e. to_string ( ) ) ?;
445+ let access_token = token_json[ "access_token" ]
446+ . as_str ( )
447+ . ok_or ( "Failed to parse access token" ) ?
448+ . to_string ( ) ;
449+
450+ let query = format ! (
451+ "fields name,genres.name,involved_companies.company.name,involved_companies.developer,involved_companies.publisher,first_release_date,summary; search \" {}\" ; limit 5;" ,
452+ game_name. replace( '"' , "" )
453+ ) ;
454+ let igdb_resp = ureq:: post ( "https://api.igdb.com/v4/games" )
455+ . set ( "Client-ID" , client_id. trim ( ) )
456+ . set ( "Authorization" , & format ! ( "Bearer {}" , access_token) )
457+ . set ( "Content-Type" , "text/plain" )
458+ . timeout ( std:: time:: Duration :: from_secs ( 15 ) )
459+ . send_string ( & query)
460+ . map_err ( |e| format ! ( "IGDB request failed: {}" , e) ) ?;
461+ let igdb_games: serde_json:: Value = serde_json:: from_str (
462+ & igdb_resp. into_string ( ) . map_err ( |e| e. to_string ( ) ) ?
463+ ) . map_err ( |e| e. to_string ( ) ) ?;
464+
465+ let entry = match igdb_games. as_array ( ) . and_then ( |a| a. first ( ) ) {
466+ Some ( e) => e. clone ( ) ,
467+ None => return Ok ( None ) ,
468+ } ;
469+
470+ let genre = entry[ "genres" ] . as_array ( )
471+ . and_then ( |a| a. first ( ) )
472+ . and_then ( |g| g[ "name" ] . as_str ( ) )
473+ . map ( |s| s. to_string ( ) ) ;
474+
475+ let mut developer: Option < String > = None ;
476+ let mut publisher: Option < String > = None ;
477+ if let Some ( companies) = entry[ "involved_companies" ] . as_array ( ) {
478+ for ic in companies {
479+ let name = ic[ "company" ] [ "name" ] . as_str ( ) . map ( |s| s. to_string ( ) ) ;
480+ if ic[ "developer" ] . as_bool ( ) . unwrap_or ( false ) && developer. is_none ( ) {
481+ developer = name. clone ( ) ;
482+ }
483+ if ic[ "publisher" ] . as_bool ( ) . unwrap_or ( false ) && publisher. is_none ( ) {
484+ publisher = name;
485+ }
486+ }
487+ }
488+
489+ let release_year = entry[ "first_release_date" ] . as_i64 ( ) . map ( |ts| {
490+ let dt = chrono:: DateTime :: < chrono:: Utc > :: from_timestamp ( ts, 0 )
491+ . unwrap_or_else ( || chrono:: Utc :: now ( ) ) ;
492+ dt. format ( "%Y" ) . to_string ( ) . parse :: < i64 > ( ) . unwrap_or ( 0 )
493+ } ) ;
494+
495+ let summary = entry[ "summary" ] . as_str ( ) . map ( |s| s. to_string ( ) ) ;
496+
497+ let meta = IgdbMetadata {
498+ genre : genre. clone ( ) ,
499+ developer : developer. clone ( ) ,
500+ publisher : publisher. clone ( ) ,
501+ release_year,
502+ summary : summary. clone ( ) ,
503+ } ;
504+
505+ {
506+ let conn = state. 0 . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
507+ let _ = conn. execute (
508+ "UPDATE games SET genre=?1, developer=?2, publisher=?3, release_year=?4 WHERE id=?5" ,
509+ rusqlite:: params![ genre, developer, publisher, release_year, game_id] ,
510+ ) ;
511+ if let Some ( ref s) = summary {
512+ let _ = conn. execute (
513+ "UPDATE games SET description=?1 WHERE id=?2 AND (description IS NULL OR description = '')" ,
514+ rusqlite:: params![ s, game_id] ,
515+ ) ;
516+ }
517+ }
518+
519+ Ok ( Some ( meta) )
520+ }
521+
522+ #[ tauri:: command]
523+ pub fn clear_igdb_data ( state : State < DbState > , id : String ) -> Result < ( ) , String > {
524+ let conn = state. 0 . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
525+ queries:: clear_igdb_data ( & conn, & id) . map_err ( |e| e. to_string ( ) )
526+ }
0 commit comments