Skip to content

Commit

Permalink
feat: search packages
Browse files Browse the repository at this point in the history
  • Loading branch information
AuroraZiling committed Feb 4, 2024
1 parent 54f3799 commit 5437787
Show file tree
Hide file tree
Showing 14 changed files with 416 additions and 76 deletions.
8 changes: 8 additions & 0 deletions src/PipManager.PackageSearch/IPackageSearchService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using PipManager.PackageSearch.Wrappers.Query;

namespace PipManager.PackageSearch;

public interface IPackageSearchService
{
public Task<QueryWrapper> Query(string name, int page=1);
}
65 changes: 65 additions & 0 deletions src/PipManager.PackageSearch/PackageSearchService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using HtmlAgilityPack;
using PipManager.PackageSearch.Wrappers.Query;

namespace PipManager.PackageSearch;

public class PackageSearchService(HttpClient httpClient) : IPackageSearchService
{
public Dictionary<(string, int), QueryWrapper> QueryCaches { get; set; } = [];
public async Task<QueryWrapper> Query(string name, int page=1)
{
if(QueryCaches.ContainsKey((name, page)))
{
return QueryCaches[(name, page)];
}
var htmlContent = "";
try
{
htmlContent = await httpClient.GetStringAsync($"https://pypi.org/search/?q={name}&page={page}");
}
catch (Exception exception) when (exception is TaskCanceledException || exception is HttpRequestException)
{
return new QueryWrapper
{
Status = QueryStatus.Timeout
};
}
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(htmlContent);

var queryWrapper = new QueryWrapper
{
ResultCount = htmlDocument.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/form/div[1]/div[1]/p/strong").InnerText
};
queryWrapper.Status = queryWrapper.ResultCount != "0" ? QueryStatus.Success : QueryStatus.NoResults;
if (queryWrapper.Status == QueryStatus.NoResults)
{
return queryWrapper;
}
var pageNode = htmlDocument.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/form/div[3]/div");
queryWrapper.MaxPageNumber = pageNode == null ? 0 : int.Parse(pageNode.ChildNodes[^4].InnerText);

try
{
var resultList = htmlDocument.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/form/div[3]/ul").ChildNodes.Where(result => result.InnerLength != 15).Select(result => result.ChildNodes[1]);
queryWrapper.Results = [];
foreach (var resultItem in resultList)
{
queryWrapper.Results.Add(new QueryListItemModel
{
Name = resultItem.ChildNodes[1].ChildNodes[1].InnerText,
Version = resultItem.ChildNodes[1].ChildNodes[3].InnerText,
Description = resultItem.ChildNodes[3].InnerText,
UpdateTime = DateTime.ParseExact(resultItem.ChildNodes[1].ChildNodes[5].ChildNodes[0].Attributes["datetime"].Value, "yyyy-MM-ddTHH:mm:sszzz", null, System.Globalization.DateTimeStyles.RoundtripKind)
});
}
}
catch (ArgumentOutOfRangeException)
{
QueryCaches.Add((name, page), queryWrapper);
return queryWrapper;
}
QueryCaches.Add((name, page), queryWrapper);
return queryWrapper;
}
}
14 changes: 14 additions & 0 deletions src/PipManager.PackageSearch/PipManager.PackageSearch.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.58" />
<PackageReference Include="Serilog" Version="3.1.1" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions src/PipManager.PackageSearch/Wrappers/Query/QueryListItemModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PipManager.PackageSearch.Wrappers.Query;

public class QueryListItemModel
{
public required string Name { get; set; }
public required string Version { get; set; }
public required string Description { get; set; }
public DateTime UpdateTime { get; set; }
}

11 changes: 11 additions & 0 deletions src/PipManager.PackageSearch/Wrappers/Query/QueryStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@


namespace PipManager.PackageSearch.Wrappers.Query;

public enum QueryStatus
{
Success,
NoResults,
Timeout
}

10 changes: 10 additions & 0 deletions src/PipManager.PackageSearch/Wrappers/Query/QueryWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PipManager.PackageSearch.Wrappers.Query;

public class QueryWrapper
{
public QueryStatus Status { get; set; }
public string? ResultCount { get; set; }
public List<QueryListItemModel>? Results { get; set;}
public int MaxPageNumber { get; set; }
}

6 changes: 6 additions & 0 deletions src/PipManager.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决
..\README.md = ..\README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipManager.PackageSearch", "PipManager.PackageSearch\PipManager.PackageSearch.csproj", "{759DBA08-4418-474E-BD6F-773829A81368}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -20,6 +22,10 @@ Global
{4C085660-9DCE-403F-89A7-E36C9405BBC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C085660-9DCE-403F-89A7-E36C9405BBC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C085660-9DCE-403F-89A7-E36C9405BBC0}.Release|Any CPU.Build.0 = Release|Any CPU
{759DBA08-4418-474E-BD6F-773829A81368}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{759DBA08-4418-474E-BD6F-773829A81368}.Debug|Any CPU.Build.0 = Debug|Any CPU
{759DBA08-4418-474E-BD6F-773829A81368}.Release|Any CPU.ActiveCfg = Release|Any CPU
{759DBA08-4418-474E-BD6F-773829A81368}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 2 additions & 0 deletions src/PipManager/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PipManager.PackageSearch;
using PipManager.Services;
using PipManager.Services.Action;
using PipManager.Services.Configuration;
Expand Down Expand Up @@ -75,6 +76,7 @@ public partial class App
services.AddSingleton<IThemeService, ThemeService>();
services.AddSingleton<IContentDialogService, ContentDialogService>();
services.AddSingleton<ITaskBarService, TaskBarService>();
services.AddSingleton<IPackageSearchService, PackageSearchService>();

// Pages
services.AddSingleton<LibraryPage>();
Expand Down
45 changes: 45 additions & 0 deletions src/PipManager/Languages/Lang.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/PipManager/Languages/Lang.resx
Original file line number Diff line number Diff line change
Expand Up @@ -655,4 +655,19 @@ Other sources are faster to connect to, but may be incomplete.</value>
<data name="LibraryInstall_Requirements_Download" xml:space="preserve">
<value>Download</value>
</data>
<data name="Search_List_NoDescription" xml:space="preserve">
<value>No Description</value>
</data>
<data name="Search_List_UpdateTime" xml:space="preserve">
<value>Update Time: </value>
</data>
<data name="Search_List_TotalResultNumber" xml:space="preserve">
<value>About {0} results found</value>
</data>
<data name="Search_Query_NoResults" xml:space="preserve">
<value>No results</value>
</data>
<data name="Search_Query_Timeout" xml:space="preserve">
<value>Timeout</value>
</data>
</root>
15 changes: 15 additions & 0 deletions src/PipManager/Languages/Lang.zh-cn.resx
Original file line number Diff line number Diff line change
Expand Up @@ -655,4 +655,19 @@
<data name="LibraryInstall_Requirements_Download" xml:space="preserve">
<value>下载</value>
</data>
<data name="Search_List_NoDescription" xml:space="preserve">
<value>暂无描述</value>
</data>
<data name="Search_List_UpdateTime" xml:space="preserve">
<value>最后更新于: </value>
</data>
<data name="Search_List_TotalResultNumber" xml:space="preserve">
<value>约 {0} 个结果</value>
</data>
<data name="Search_Query_NoResults" xml:space="preserve">
<value>无结果</value>
</data>
<data name="Search_Query_Timeout" xml:space="preserve">
<value>查询超时</value>
</data>
</root>
4 changes: 4 additions & 0 deletions src/PipManager/PipManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,8 @@
<ItemGroup>
<None Include="E:\pipManager\PipManager\src\PipManager\.editorconfig" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PipManager.PackageSearch\PipManager.PackageSearch.csproj" />
</ItemGroup>
</Project>
114 changes: 112 additions & 2 deletions src/PipManager/ViewModels/Pages/Search/SearchViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
using Serilog;
using PipManager.Languages;
using PipManager.PackageSearch;
using PipManager.PackageSearch.Wrappers.Query;
using PipManager.Services.Mask;
using PipManager.Services.Toast;
using Serilog;
using System.Collections.ObjectModel;
using System.Reflection.Metadata;
using Wpf.Ui.Controls;

namespace PipManager.ViewModels.Pages.Search;

public partial class SearchViewModel : ObservableObject, INavigationAware
public partial class SearchViewModel(IPackageSearchService packageSearchService, IToastService toastService, IMaskService maskService) : ObservableObject, INavigationAware
{
private bool _isInitialized;

[ObservableProperty]
private ObservableCollection<QueryListItemModel> _queryList = [];
[ObservableProperty]
private string _queryPackageName = "";
[ObservableProperty]
private string _totalResultNumber = "";
[ObservableProperty]
private bool _successQueried = false;
[ObservableProperty]
private bool _reachesFirstPage = true;
[ObservableProperty]
private bool _reachesLastPage = false;
[ObservableProperty]
private int _currentPage = 1;
[ObservableProperty]
private int _maxPage = 0;

public void OnNavigatedTo()
{
if (!_isInitialized)
Expand All @@ -22,4 +46,90 @@ private void InitializeViewModel()
_isInitialized = true;
Log.Information("[Search] Initialized");
}

[RelayCommand]
public async Task ToPreviousPage()
{
if(CurrentPage == 1)
{
return;
}
maskService.Show();
var result = await packageSearchService.Query(QueryPackageName, CurrentPage-1);
Process(result);
maskService.Hide();
CurrentPage--;
DeterminePageReaches();
}

[RelayCommand]
public async Task ToNextPage()
{
if(CurrentPage == MaxPage)
{
return;
}
maskService.Show();
var result = await packageSearchService.Query(QueryPackageName, CurrentPage+1);
Process(result);
maskService.Hide();
CurrentPage++;
DeterminePageReaches();
}

private void DeterminePageReaches()
{
ReachesFirstPage = CurrentPage == 1;
ReachesLastPage = CurrentPage == MaxPage;
}

private void Process(QueryWrapper queryWrapper)
{
if (queryWrapper.Status == QueryStatus.Success)
{
foreach (var resultItem in queryWrapper.Results!)
{
if (string.IsNullOrEmpty(resultItem.Description))
{
resultItem.Description = Lang.Search_List_NoDescription;
}
}
QueryList = new ObservableCollection<QueryListItemModel>(queryWrapper.Results!);
TotalResultNumber = queryWrapper.ResultCount!;
SuccessQueried = true;
MaxPage = queryWrapper.MaxPageNumber;
DeterminePageReaches();
}
else
{
if (queryWrapper.Status == QueryStatus.NoResults)
{
toastService.Error(Lang.Search_Query_NoResults);
}
else if (queryWrapper.Status == QueryStatus.Timeout)
{
toastService.Error(Lang.Search_Query_Timeout);
}
QueryList.Clear();
TotalResultNumber = "";
SuccessQueried = false;
MaxPage = 0;
}
}

[RelayCommand]
public async Task Search(string? parameter)
{
if (parameter != null && !string.IsNullOrEmpty(parameter))
{
QueryList.Clear();
TotalResultNumber = "";
SuccessQueried = false;
MaxPage = 0;
CurrentPage = 1;
QueryPackageName = parameter;
var result = await packageSearchService.Query(parameter, 1);
Process(result);
}
}
}
Loading

0 comments on commit 5437787

Please sign in to comment.