Skip to content

Commit d9c272b

Browse files
authored
Fix title id search (#446)
1 parent 58cf3c3 commit d9c272b

File tree

7 files changed

+460
-164
lines changed

7 files changed

+460
-164
lines changed

XAU/Models/GitHubResponse.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Newtonsoft.Json;
2+
13
public class EventsUpdateResponse
24
{
35
public int Timestamp { get; set; }
@@ -9,3 +11,22 @@ public class VersionResponse
911
public string? DownloadURL { get; set; }
1012
public string? LatestBuildVersion { get; set; }
1113
}
14+
15+
16+
public class GitHubFile
17+
{
18+
[JsonProperty("name")]
19+
public string Name { get; set; }
20+
21+
[JsonProperty("path")]
22+
public string Path { get; set; }
23+
24+
[JsonProperty("sha")]
25+
public string Sha { get; set; }
26+
27+
[JsonProperty("size")]
28+
public long Size { get; set; }
29+
30+
[JsonProperty("download_url")]
31+
public string DownloadUrl { get; set; }
32+
}

XAU/Models/MiscItems.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
public class GameItem
3+
{
4+
public string Title { get; set; }
5+
public string TitleId { get; set; }
6+
public bool IsTitleBased { get; set; }
7+
}

XAU/Networking/GithubRestApi.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,15 @@ public async Task<dynamic> GetReleaseVersionAsync()
5353
var responseString = await _httpClient.GetStringAsync("https://raw.githubusercontent.com/Fumo-Unlockers/Xbox-Achievement-Unlocker/Events-Data/meta.json");
5454
return JsonConvert.DeserializeObject<EventsUpdateResponse>(responseString);
5555
}
56+
57+
58+
public async Task<GitHubFile?> GetXboxGamesDatabaseInfoAsync()
59+
{
60+
SetDefaultHeaders();
61+
_httpClient.DefaultRequestHeaders.Add(HeaderNames.Host, Hosts.GitHubApi);
62+
var responseString = await _httpClient.GetStringAsync("https://api.github.com/repos/Fumo-Unlockers/XboxGames/contents");
63+
var files = JsonConvert.DeserializeObject<List<GitHubFile>>(responseString);
64+
return files?.FirstOrDefault(f => f.Name.Equals("xbox_games.db", StringComparison.OrdinalIgnoreCase));
65+
}
66+
5667
}

XAU/ViewModels/Pages/HomeViewModel.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,67 @@ private async void UpdateEvents()
247247
_snackbarService.Show("Events Update Complete", "Events have been updated to the latest version.", ControlAppearance.Success, new SymbolIcon(SymbolRegular.Checkmark24), _snackbarDuration);
248248
}
249249

250+
private async void CheckForXboxGamesDatabaseUpdate()
251+
{
252+
try
253+
{
254+
var fileInfo = await _gitHubRestAPI.Value.GetXboxGamesDatabaseInfoAsync();
255+
if (fileInfo == null)
256+
{
257+
_snackbarService.Show("Error", "Could not check for database updates.", ControlAppearance.Danger, new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
258+
return;
259+
}
260+
261+
string titleSearchPath = Path.Combine(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "XAU"), "TitleSearch");
262+
string shaFilePath = Path.Combine(titleSearchPath, "xbox_games_sha.txt");
263+
string dbFilePath = Path.Combine(titleSearchPath, "xbox_games.db");
264+
265+
Directory.CreateDirectory(titleSearchPath);
266+
267+
string currentSha = string.Empty;
268+
try
269+
{
270+
if (File.Exists(shaFilePath))
271+
{
272+
currentSha = (await File.ReadAllTextAsync(shaFilePath)).Trim();
273+
}
274+
}
275+
catch { }
276+
277+
if (string.IsNullOrEmpty(currentSha) || !currentSha.Equals(fileInfo.Sha, StringComparison.OrdinalIgnoreCase))
278+
{
279+
_snackbarService.Show("Database Update", "New Xbox games database available. Downloading...", ControlAppearance.Info, new SymbolIcon(SymbolRegular.Checkmark24), _snackbarDuration);
280+
281+
using var client = new HttpClient();
282+
var response = await client.GetAsync(fileInfo.DownloadUrl);
283+
response.EnsureSuccessStatusCode();
284+
285+
var content = await response.Content.ReadAsByteArrayAsync();
286+
287+
await File.WriteAllBytesAsync(dbFilePath, content);
288+
289+
try
290+
{
291+
await File.WriteAllTextAsync(shaFilePath, fileInfo.Sha);
292+
}
293+
catch (Exception ex)
294+
{
295+
Console.WriteLine($"Failed to store database SHA: {ex.Message}");
296+
}
297+
298+
_snackbarService.Show("Success", "Xbox games database updated successfully!", ControlAppearance.Success, new SymbolIcon(SymbolRegular.Checkmark24), _snackbarDuration);
299+
}
300+
else
301+
{
302+
Console.WriteLine("Xbox games database is up to date.");
303+
}
304+
}
305+
catch (Exception ex)
306+
{
307+
_snackbarService.Show("Error", $"Database update check failed: {ex.Message}", ControlAppearance.Danger, new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
308+
}
309+
}
310+
250311
#endregion
251312

252313
private async Task InitializeViewModel()
@@ -295,6 +356,7 @@ private async Task InitializeViewModel()
295356

296357
}
297358
CheckForEventUpdates();
359+
CheckForXboxGamesDatabaseUpdate();
298360
LoadSettings();
299361
_isInitialized = true;
300362
if (Settings.AutoLaunchXboxAppEnabled && Process.GetProcessesByName(ProcessNames.XboxPcApp).Length == 0)

XAU/ViewModels/Pages/MiscViewModel.cs

Lines changed: 83 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using HtmlAgilityPack;
2+
using Microsoft.Data.Sqlite;
23
using Newtonsoft.Json.Linq;
4+
using System.Data;
35
using System.Diagnostics;
6+
using System.DirectoryServices;
47
using System.IO;
58
using System.Text;
69
using System.Windows.Input;
@@ -18,7 +21,6 @@ public partial class MiscViewModel : ObservableObject, INavigationAware
1821
private readonly ISnackbarService _snackbarService;
1922
private TimeSpan _snackbarDuration = TimeSpan.FromSeconds(2);
2023
private Lazy<XboxRestAPI> _xboxRestAPI = new Lazy<XboxRestAPI>(() => new XboxRestAPI(HomeViewModel.XAUTH));
21-
private Lazy<DBoxRestApi> _dboxRestApi = new Lazy<DBoxRestApi>();
2224

2325

2426

@@ -197,87 +199,126 @@ public async Task Spoofing()
197199
#endregion
198200

199201
#region GameSearch
200-
[ObservableProperty] private JObject _tSearchResponse = new JObject();
202+
[ObservableProperty] private List<GameItem> _tSearchResults = new List<GameItem>();
201203
[ObservableProperty] private List<string> _tSearchTitleNames = new List<string>();
202204
[ObservableProperty] private string _tSearchText = "";
203-
[ObservableProperty] private string _tSearchGameImage = "pack://application:,,,/Assets/cirno.png";
204205
[ObservableProperty] private string _tSearchGameName = "Name: ";
205206
[ObservableProperty] private string _tSearchGameTitleID = "";
207+
[ObservableProperty] private string _tSearchGameTitleBased = "Title Based: Unknown";
208+
209+
private string GetDatabasePath()
210+
{
211+
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "XAU", "TitleSearch", "xbox_games.db");
212+
}
213+
206214
[RelayCommand]
207215
public async Task SearchGame()
208216
{
209217
try
210218
{
211-
var response = await _dboxRestApi.Value.SearchAsync(TSearchText);
219+
if (string.IsNullOrWhiteSpace(TSearchText))
220+
{
221+
TSearchTitleNames = new List<string>();
222+
TSearchResults = new List<GameItem>();
223+
return;
224+
}
225+
226+
string dbPath = GetDatabasePath();
212227

213-
var items = response["items"] as JArray;
214-
if (items == null || items.Count == 0)
228+
if (!File.Exists(dbPath))
215229
{
216-
_snackbarService.Show("Error", $"No results were found for {TSearchText}",
230+
_snackbarService.Show("Error", "Game database not found. Please wait for it to download.",
217231
ControlAppearance.Danger,
218232
new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
219-
TSearchTitleNames = new List<string>();
220-
TSearchResponse = response;
221233
return;
222234
}
223235

224-
// Sort items alphabetically by "name"
225-
var sortedItems = new JArray(items
226-
.OrderBy(item => item["name"]?.ToString() ?? string.Empty, StringComparer.OrdinalIgnoreCase));
236+
var results = await Task.Run(() => SearchGamesInDatabase(dbPath, TSearchText));
227237

228-
// Update TSearchResponse with sorted items
229-
response["items"] = sortedItems;
230-
TSearchResponse = response;
238+
if (!results.Any())
239+
{
240+
_snackbarService.Show("Error", $"No results were found for '{TSearchText}'",
241+
ControlAppearance.Danger,
242+
new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
243+
TSearchTitleNames = new List<string>();
244+
TSearchResults = new List<GameItem>();
245+
return;
246+
}
231247

232-
// Create TSearchTitleNames list from sorted items
233-
TSearchTitleNames = sortedItems
234-
.Select(item => item["name"]?.ToString() ?? string.Empty)
235-
.Where(name => !string.IsNullOrWhiteSpace(name))
236-
.ToList();
248+
results = results.OrderBy(game => game.Title, StringComparer.OrdinalIgnoreCase).ToList();
249+
250+
TSearchResults = results;
251+
TSearchTitleNames = results.Select(game => game.Title).ToList();
237252
}
238-
catch
253+
catch (Exception ex)
239254
{
240-
_snackbarService.Show("Error", $"No results were found for {TSearchText}",
255+
_snackbarService.Show("Error", $"Search failed: {ex.Message}",
241256
ControlAppearance.Danger,
242257
new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
243258
TSearchTitleNames = new List<string>();
244-
TSearchResponse = new JObject();
259+
TSearchResults = new List<GameItem>();
245260
}
246261
}
247262

263+
private List<GameItem> SearchGamesInDatabase(string dbPath, string searchText)
264+
{
265+
var results = new List<GameItem>();
266+
267+
using var connection = new SqliteConnection($"Data Source={dbPath}");
268+
connection.Open();
269+
270+
// Search for games that contain the search text (case-insensitive)
271+
string sql = @"
272+
SELECT title, titleId, isTitleBased
273+
FROM games
274+
WHERE title LIKE @searchText
275+
ORDER BY title COLLATE NOCASE
276+
LIMIT 100";
277+
278+
using var command = new SqliteCommand(sql, connection);
279+
command.Parameters.AddWithValue("@searchText", $"%{searchText}%");
280+
281+
using var reader = command.ExecuteReader();
282+
while (reader.Read())
283+
{
284+
results.Add(new GameItem
285+
{
286+
Title = reader.GetString("title"),
287+
TitleId = reader.GetString("titleId"),
288+
IsTitleBased = reader.GetInt32("isTitleBased") == 1
289+
});
290+
}
291+
292+
return results;
293+
}
294+
248295
public void DisplayGameInfo(int index)
249296
{
250297
try
251298
{
252-
var items = TSearchResponse["items"] as JArray;
253-
if (items == null || items.Count <= index)
299+
if (TSearchResults == null || TSearchResults.Count <= index || index < 0)
254300
{
255-
_snackbarService.Show("Error", "No game found at the selected index.", ControlAppearance.Danger, new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
301+
_snackbarService.Show("Error", "No game found at the selected index.",
302+
ControlAppearance.Danger,
303+
new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
256304
return;
257305
}
258306

259-
var item = items[index];
260-
var titleIdHex = item["title_id"]?.ToString() ?? "0";
261-
var titleIdBase10 = long.TryParse(titleIdHex, System.Globalization.NumberStyles.HexNumber, null, out var base10Id)
262-
? base10Id.ToString()
263-
: "-1";
307+
var selectedGame = TSearchResults[index];
264308

265-
TSearchGameName = "Name: " + (item["name"]?.ToString() ?? "Unknown");
266-
TSearchGameTitleID = titleIdBase10;
309+
TSearchGameName = selectedGame.Title;
310+
TSearchGameTitleID = selectedGame.TitleId;
311+
TSearchGameTitleBased = $"Title Based: {(selectedGame.IsTitleBased ? "True" : "False")}";
267312

268-
if (titleIdBase10 == "-1")
269-
{
270-
_snackbarService.Show("Error: TitleID not found",
271-
$"The TitleID for {TSearchGameName} was not available or invalid.",
272-
ControlAppearance.Danger,
273-
new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
274-
}
275313
}
276-
catch
314+
catch (Exception ex)
277315
{
278-
_snackbarService.Show("Error", "Failed to display game info.", ControlAppearance.Danger, new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
316+
_snackbarService.Show("Error", "Failed to display game info.",
317+
ControlAppearance.Danger,
318+
new SymbolIcon(SymbolRegular.ErrorCircle24), _snackbarDuration);
279319
}
280320
}
321+
281322
#endregion
282323

283324
#region GamertagSearch

0 commit comments

Comments
 (0)