diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 82a2748..5942b7f 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -6,17 +6,16 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Setup .NET Core SDK 6.0.x - uses: actions/setup-dotnet@v1.7.2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' - - name: Setup .NET Core SDK 3.1.x - uses: actions/setup-dotnet@v1.7.2 - with: - dotnet-version: '3.1.x' + dotnet-version: | + 8.0.x + 6.0.x + 3.1.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/manual-release.yaml b/.github/workflows/manual-release.yaml index 3d4d461..e8cfab8 100644 --- a/.github/workflows/manual-release.yaml +++ b/.github/workflows/manual-release.yaml @@ -19,13 +19,13 @@ jobs: EXPECTED_VERSION: ${{inputs.version }} EXPECTED_LUMINA_VERSION: ${{inputs.lumina-version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 47752ea..b65908c 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -79,13 +79,13 @@ jobs: EXPECTED_VERSION: ${{needs.tag.outputs.version }} EXPECTED_LUMINA_VERSION: ${{needs.tag-lumina.outputs.version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/NetStone.Test/NetStone.Test.csproj b/NetStone.Test/NetStone.Test.csproj index 77b8b52..bad8b20 100644 --- a/NetStone.Test/NetStone.Test.csproj +++ b/NetStone.Test/NetStone.Test.csproj @@ -1,15 +1,15 @@ - net6.0 + net8.0 false 10.0 - - + + diff --git a/NetStone.Test/Tests.cs b/NetStone.Test/Tests.cs index 13d0992..ef356ce 100644 --- a/NetStone.Test/Tests.cs +++ b/NetStone.Test/Tests.cs @@ -6,6 +6,7 @@ using NetStone.Model.Parseables.Character; using NetStone.Search.Character; using NetStone.Search.FreeCompany; +using NetStone.Search.Linkshell; using NetStone.StaticData; using NUnit.Framework; using SortKind = NetStone.Search.Character.SortKind; @@ -18,6 +19,8 @@ public class Tests private const string TestCharacterIdFull = "24471319"; private const string TestCharacterIdEureka = "14556736"; + private const string TestLinkshell = "18577348462979918"; + private const string TestCWLS = "097b99377634f9980eb0cf0b4ff6cf86807feb2c"; private const string TestCharacterIdEureka2 = "6787158"; private const string TestCharacterIdBare = "9426169"; private const string TestCharacterIdDoH = "42256897"; @@ -176,7 +179,7 @@ public async Task CheckFreeCompanyRecruiting() Assert.AreEqual("Immortal Flames", fc.GrandCompany); Assert.AreEqual("Bedge Lords", fc.Name); Assert.AreEqual("«BEDGE»", fc.Tag); - Assert.AreEqual("Friendly FC with 24/7 buffs, events and a large FC house in Goblet. LF more new amiable Bedgers to join us! Check our Lodestone & come chat for details.", fc.Slogan); + Assert.IsTrue(fc.Slogan.StartsWith("Friendly FC with")); Assert.AreEqual(new DateTime(2022, 12, 04, 19, 47, 07), fc.Formed); Assert.GreaterOrEqual(fc.ActiveMemberCount, 50); Assert.AreEqual(30, fc.Rank); @@ -227,7 +230,7 @@ public async Task CheckFreeCompanyRecruiting() Assert.IsTrue(fc.Focus.Dungeons.IsEnabled); Assert.AreEqual("Guildhests", fc.Focus.Guildhests.Name); - Assert.IsTrue(fc.Focus.Guildhests.IsEnabled); + Assert.IsFalse(fc.Focus.Guildhests.IsEnabled); Assert.AreEqual("Trials", fc.Focus.Trials.Name); Assert.IsTrue(fc.Focus.Trials.IsEnabled); @@ -559,4 +562,146 @@ public async Task CheckCharacterCollectableNotFound() var minions = await this.lodestone.GetCharacterMinion("0"); Assert.IsNull(minions); } + + [Test] + public async Task CheckCrossworldLinkShell() + { + var cwls = await this.lodestone.GetCrossworldLinkshell(TestCWLS); + Assert.IsNotNull(cwls); + Assert.AreEqual("COR and Friends", cwls.Name); + Assert.AreEqual("Light", cwls.DataCenter); + Assert.AreEqual(2, cwls.NumPages); + while (cwls is not null) + { + foreach (var member in cwls.Members) + { + Console.WriteLine($"{member.Name} ({member.Rank}) {member.RankIcon}\n" + + $"\tId: {member.Id}\n" + + $"\tAvatar: {member.Avatar}\n" + + $"\tServer: {member.Server}\n" + + $"\tLS Rank: {member.LinkshellRank}\n" + + $"\tLS Rank Icon: {member.LinkshellRankIcon}"); + } + cwls = await cwls.GetNextPage(); + } + } + + [Test] + public async Task CheckCrossworldLinkShellSearch() + { + var emptyQuery = new CrossworldLinkshellSearchQuery() + { + Name = "abcedfas", + }; + var emptyResult = await this.lodestone.SearchCrossworldLinkshell(emptyQuery); + Assert.IsNotNull(emptyResult); + //Assert.False(emptyResult.HasResults); + var query = new CrossworldLinkshellSearchQuery() + { + Name = "Hell", + ActiveMembers = LinkshellSizeCategory.ElevenToThirty, + DataCenter = "Chaos", + Sorting = LinkshellSortKind.MemberCountDesc, + }; + bool first = true; + var results = await this.lodestone.SearchCrossworldLinkshell(query); + Assert.IsNotNull(results); + Assert.True(results.HasResults); + Assert.AreEqual(2, results.NumPages); + while (results is not null) + { + foreach (var result in results.Results) + { + if (first) + { + first = false; + var shell = await result.GetCrossworldLinkshell(); + Assert.IsNotNull(shell); + Assert.AreEqual(result.Name, shell.Name); + } + Console.WriteLine($"{result.Name} ({result.Id}): {result.ActiveMembers}\n"); + } + results = await results.GetNextPage(); + } + } + + [Test] + public async Task CheckLinkshell() + { + var ls = await this.lodestone.GetLinkshell(TestLinkshell); + Assert.IsNotNull(ls); + Assert.AreEqual("CORshell", ls.Name); + Assert.AreEqual(2, ls.NumPages); + while (ls is not null) + { + foreach (var member in ls.Members) + { + Console.WriteLine($"{member.Name} ({member.Rank}) {member.RankIcon}\n" + + $"Id: {member.Id}\n" + + $"Avatar: {member.Avatar}\n" + + $"Server: {member.Server}\n" + + $"LS Rank: {member.LinkshellRank}\n" + + $"LS Rank Icon: {member.LinkshellRankIcon}"); + + } + ls = await ls.GetNextPage(); + } + + } + + [Test] + public async Task CheckLinkShellSearch() + { + var emptyQuery = new LinkshellSearchQuery() + { + Name = "abcedfas", + }; + var emptyResult = await this.lodestone.SearchLinkshell(emptyQuery); + Assert.IsNotNull(emptyResult); + Assert.False(emptyResult.HasResults); + var query = new LinkshellSearchQuery() + { + Name = "Hell", + ActiveMembers = LinkshellSizeCategory.ElevenToThirty, + DataCenter = "Chaos", + }; + bool first = true; + var results = await this.lodestone.SearchLinkshell(query); + Assert.IsNotNull(results); + Assert.True(results.HasResults); + Assert.AreEqual(2, results.NumPages); + while (results is not null) + { + foreach (var result in results.Results) + { + if (first) + { + first = false; + var shell = await result.GetLinkshell(); + Assert.IsNotNull(shell); + Assert.AreEqual(result.Name, shell.Name); + } + Console.WriteLine($"{result.Name} ({result.Id}): {result.ActiveMembers}\n"); + } + results = await results.GetNextPage(); + } + query = new LinkshellSearchQuery() + { + Name = "Hell", + ActiveMembers = LinkshellSizeCategory.ElevenToThirty, + HomeWorld = "Spriggan", + }; + results = await this.lodestone.SearchLinkshell(query); + Assert.IsNotNull(results); + Assert.True(results.HasResults); + Assert.AreEqual(1, results.NumPages); + while (results is not null) + { + foreach (var result in results.Results) + { + Console.WriteLine($"{result.Name} ({result.Id}): {result.ActiveMembers}\n"); + } + results = await results.GetNextPage(); + } + } } \ No newline at end of file diff --git a/NetStone/Definitions/DefinitionsContainer.cs b/NetStone/Definitions/DefinitionsContainer.cs index bfb7a81..9f16e57 100644 --- a/NetStone/Definitions/DefinitionsContainer.cs +++ b/NetStone/Definitions/DefinitionsContainer.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using NetStone.Definitions.Model; using NetStone.Definitions.Model.Character; +using NetStone.Definitions.Model.CWLS; using NetStone.Definitions.Model.FreeCompany; +using NetStone.Definitions.Model.Linkshell; namespace NetStone.Definitions; @@ -83,6 +85,36 @@ public abstract class DefinitionsContainer : IDisposable /// Definitions for Free company search /// public PagedDefinition FreeCompanySearch { get; protected set; } + + /// + /// Definitions for cross world link shells + /// + public CrossworldLinkshellDefinition CrossworldLinkshell { get; protected set; } + + /// + /// Definitions for cross world link shell members + /// + public PagedDefinition CrossworldLinkshellMember { get; protected set; } + + /// + /// Definitions for cross world link shell searches + /// + public PagedDefinition CrossworldLinkshellSearch { get; protected set; } + + /// + /// Definitions for link shells + /// + public LinkshellDefinition Linkshell { get; protected set; } + + /// + /// Definitions for link shell members + /// + public PagedDefinition LinkshellMember { get; protected set; } + + /// + /// Definitions for link-shell searches + /// + public PagedDefinition LinkshellSearch { get; protected set; } #endregion diff --git a/NetStone/Definitions/Model/CWLS/CrossworldLinkshellDefinition.cs b/NetStone/Definitions/Model/CWLS/CrossworldLinkshellDefinition.cs new file mode 100644 index 0000000..d810596 --- /dev/null +++ b/NetStone/Definitions/Model/CWLS/CrossworldLinkshellDefinition.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace NetStone.Definitions.Model.CWLS; + +/// +/// Definitions for cross world link shell +/// +public class CrossworldLinkshellDefinition : IDefinition +{ + /// + /// Name + /// + [JsonProperty("NAME")] + public DefinitionsPack Name { get; set; } + + /// + /// Name + /// + [JsonProperty("DC")] + public DefinitionsPack DataCenter { get; set; } +} \ No newline at end of file diff --git a/NetStone/Definitions/Model/CWLS/CrossworldLinkshellMemberDefinition.cs b/NetStone/Definitions/Model/CWLS/CrossworldLinkshellMemberDefinition.cs new file mode 100644 index 0000000..a4d2b31 --- /dev/null +++ b/NetStone/Definitions/Model/CWLS/CrossworldLinkshellMemberDefinition.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace NetStone.Definitions.Model.CWLS; + + +/// +/// +/// +public class CrossworldLinkshellMemberEntryDefinition : PagedEntryDefinition +{ + /// + /// Avatar + /// + [JsonProperty("AVATAR")] public DefinitionsPack Avatar { get; set; } + + /// + /// ID + /// + [JsonProperty("ID")] public DefinitionsPack Id { get; set; } + + /// + /// Name + /// + [JsonProperty("NAME")] public DefinitionsPack Name { get; set; } + + /// + /// Rank + /// + [JsonProperty("RANK")] public DefinitionsPack Rank { get; set; } + + /// + /// Rank Icon + /// + [JsonProperty("RANK_ICON")] public DefinitionsPack RankIcon { get; set; } + + /// + /// Linkshell rank + /// + [JsonProperty("LINKSHELL_RANK")] public DefinitionsPack LinkshellRank { get; set; } + + /// + /// Linkshell rank Icon + /// + [JsonProperty("LINKSHELL_RANK_ICON")] public DefinitionsPack LinkshellRankIcon { get; set; } + + /// + /// Server + /// + [JsonProperty("SERVER")] public DefinitionsPack Server { get; set; } +} \ No newline at end of file diff --git a/NetStone/Definitions/Model/CWLS/CrossworldLinkshellSearchDefinition.cs b/NetStone/Definitions/Model/CWLS/CrossworldLinkshellSearchDefinition.cs new file mode 100644 index 0000000..cedc4c9 --- /dev/null +++ b/NetStone/Definitions/Model/CWLS/CrossworldLinkshellSearchDefinition.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace NetStone.Definitions.Model.CWLS; +/// +/// Definition container for one Cross World Link Shell search result entry +/// +public class CrossworldLinkshellSearchEntryDefinition : PagedEntryDefinition +{ + /// + /// ID + /// + [JsonProperty("ID")] public DefinitionsPack Id { get; set; } + + /// + /// Name + /// + [JsonProperty("NAME")] public DefinitionsPack Name { get; set; } + + /// + /// Rank + /// + [JsonProperty("DC")] public DefinitionsPack Dc { get; set; } + + /// + /// Rank Icon + /// + [JsonProperty("ACTIVE_MEMBERS")] public DefinitionsPack ActiveMembers { get; set; } +} \ No newline at end of file diff --git a/NetStone/Definitions/Model/Linkshell/LinkshellDefinition.cs b/NetStone/Definitions/Model/Linkshell/LinkshellDefinition.cs new file mode 100644 index 0000000..e6bd9ba --- /dev/null +++ b/NetStone/Definitions/Model/Linkshell/LinkshellDefinition.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace NetStone.Definitions.Model.Linkshell; + +/// +/// Definitions for link shell +/// +public class LinkshellDefinition : IDefinition +{ + /// + /// Name + /// + [JsonProperty("NAME")] + public DefinitionsPack Name { get; set; } +} \ No newline at end of file diff --git a/NetStone/Definitions/Model/Linkshell/LinkshellMemberDefinition.cs b/NetStone/Definitions/Model/Linkshell/LinkshellMemberDefinition.cs new file mode 100644 index 0000000..801643d --- /dev/null +++ b/NetStone/Definitions/Model/Linkshell/LinkshellMemberDefinition.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json; + +namespace NetStone.Definitions.Model.Linkshell; + +/// +/// Definition for one entry of the linkshell memebr list +/// +public class LinkshellMemberEntryDefinition : PagedEntryDefinition +{ + /// + /// Avatar + /// + [JsonProperty("AVATAR")] public DefinitionsPack Avatar { get; set; } + + /// + /// ID + /// + [JsonProperty("ID")] public DefinitionsPack Id { get; set; } + + /// + /// Name + /// + [JsonProperty("NAME")] public DefinitionsPack Name { get; set; } + + /// + /// Rank + /// + [JsonProperty("RANK")] public DefinitionsPack Rank { get; set; } + + /// + /// Rank Icon + /// + [JsonProperty("RANK_ICON")] public DefinitionsPack RankIcon { get; set; } + + /// + /// Linkshell rank + /// + [JsonProperty("LINKSHELL_RANK")] public DefinitionsPack LinkshellRank { get; set; } + + /// + /// Linkshell rank Icon + /// + [JsonProperty("LINKSHELL_RANK_ICON")] public DefinitionsPack LinkshellRankIcon { get; set; } + + /// + /// Server + /// + [JsonProperty("SERVER")] public DefinitionsPack Server { get; set; } +} \ No newline at end of file diff --git a/NetStone/Definitions/Model/Linkshell/LinkshellSearchEntryDefinition.cs b/NetStone/Definitions/Model/Linkshell/LinkshellSearchEntryDefinition.cs new file mode 100644 index 0000000..9cef7d9 --- /dev/null +++ b/NetStone/Definitions/Model/Linkshell/LinkshellSearchEntryDefinition.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace NetStone.Definitions.Model.Linkshell; + +/// +/// Definition container for one Link-Shell search result entry +/// +public class LinkshellSearchEntryDefinition : PagedEntryDefinition +{ + /// + /// ID + /// + [JsonProperty("ID")] public DefinitionsPack Id { get; set; } + + /// + /// Name + /// + [JsonProperty("NAME")] public DefinitionsPack Name { get; set; } + + /// + /// Rank + /// + [JsonProperty("SERVER")] public DefinitionsPack Server { get; set; } + + /// + /// Rank Icon + /// + [JsonProperty("ACTIVE_MEMBERS")] public DefinitionsPack ActiveMembers { get; set; } +} \ No newline at end of file diff --git a/NetStone/Definitions/Model/MetaDefinition.cs b/NetStone/Definitions/Model/MetaDefinition.cs index ea230b8..4bc4dcb 100644 --- a/NetStone/Definitions/Model/MetaDefinition.cs +++ b/NetStone/Definitions/Model/MetaDefinition.cs @@ -40,13 +40,13 @@ public class ApplicableUris /// /// Uri that holds information about Cross World Link Shells /// - [JsonProperty("linkshell/crossworld/cwls.json")] + [JsonProperty("cwls/cwls.json")] public string? LinkshellCrossworldCwlsJson { get; set; } /// /// Uri that holds information about Cross World Link Shell members /// - [JsonProperty("linkshell/crossworld/members.json")] + [JsonProperty("cwls/members.json")] public string? LinkshellCrossworldMembersJson { get; set; } diff --git a/NetStone/Definitions/Model/PagedDefinition.cs b/NetStone/Definitions/Model/PagedDefinition.cs index 2a17457..c509d80 100644 --- a/NetStone/Definitions/Model/PagedDefinition.cs +++ b/NetStone/Definitions/Model/PagedDefinition.cs @@ -36,7 +36,7 @@ public class PagedDefinition : IDefinition where TEntry : PagedEntryDefi /// DEfinition for node for empty results /// [JsonProperty("NO_RESULTS_FOUND")] - public DefinitionsPack NoResultsFound { get; set; } + public DefinitionsPack? NoResultsFound { get; set; } } /// @@ -47,5 +47,6 @@ public class PagedEntryDefinition /// /// Root node of entry /// + [JsonProperty("ROOT")] public DefinitionsPack Root { get; set; } } \ No newline at end of file diff --git a/NetStone/Definitions/XivApiDefinitionsContainer.cs b/NetStone/Definitions/XivApiDefinitionsContainer.cs index 0e027fe..77fbe88 100644 --- a/NetStone/Definitions/XivApiDefinitionsContainer.cs +++ b/NetStone/Definitions/XivApiDefinitionsContainer.cs @@ -3,7 +3,9 @@ using System.Threading.Tasks; using NetStone.Definitions.Model; using NetStone.Definitions.Model.Character; +using NetStone.Definitions.Model.CWLS; using NetStone.Definitions.Model.FreeCompany; +using NetStone.Definitions.Model.Linkshell; using Newtonsoft.Json; namespace NetStone.Definitions; @@ -55,6 +57,14 @@ public override async Task Reload() this.CharacterSearch = await GetDefinition>("search/character.json"); this.FreeCompanySearch = await GetDefinition>("search/freecompany.json"); + + this.CrossworldLinkshell = await GetDefinition("cwls/cwls.json"); + this.CrossworldLinkshellMember = await GetDefinition>("cwls/members.json"); + this.CrossworldLinkshellSearch = await GetDefinition>("search/cwls.json"); + + this.Linkshell = await GetDefinition("linkshell/ls.json"); + this.LinkshellMember = await GetDefinition>("linkshell/members.json"); + this.LinkshellSearch = await GetDefinition>("search/linkshell.json"); } private async Task GetDefinition(string path) where T : IDefinition diff --git a/NetStone/LodestoneClient.cs b/NetStone/LodestoneClient.cs index 30622cc..144635b 100644 --- a/NetStone/LodestoneClient.cs +++ b/NetStone/LodestoneClient.cs @@ -10,12 +10,17 @@ using NetStone.Model.Parseables.Character.Achievement; using NetStone.Model.Parseables.Character.ClassJob; using NetStone.Model.Parseables.Character.Collectable; +using NetStone.Model.Parseables.CWLS; using NetStone.Model.Parseables.FreeCompany; using NetStone.Model.Parseables.FreeCompany.Members; +using NetStone.Model.Parseables.Linkshell; using NetStone.Model.Parseables.Search.Character; +using NetStone.Model.Parseables.Search.CWLS; using NetStone.Model.Parseables.Search.FreeCompany; +using NetStone.Model.Parseables.Search.Linkshell; using NetStone.Search.Character; using NetStone.Search.FreeCompany; +using NetStone.Search.Linkshell; namespace NetStone; @@ -146,7 +151,50 @@ await GetParsed( public async Task SearchCharacter(CharacterSearchQuery query, int page = 1) => await GetParsed($"/lodestone/character/{query.BuildQueryString()}&page={page}", node => new CharacterSearchPage(this, node, this.Definitions.CharacterSearch, query)); - + + #endregion + + #region Linkshells + /// + /// Gets a cross world link shell by its id. + /// + /// The ID of the cross world linkshell. + /// + /// class containing information about the cross world link shell + public async Task GetCrossworldLinkshell(string id, int page = 1) => + await GetParsed($"/lodestone/crossworld_linkshell/{id}?page={page}", + node => new LodestoneCrossworldLinkshell(this, node, this.Definitions,id)); + + /// + /// Search lodestone for a character with the specified query. + /// + /// object detailing search parameters + /// The page of search results to fetch. + /// containing search results. + public async Task SearchCrossworldLinkshell(CrossworldLinkshellSearchQuery query, int page = 1) => + await GetParsed($"/lodestone/crossworld_linkshell/{query.BuildQueryString()}&page={page}", + node => new CrossworldLinkshellSearchPage(this, node, this.Definitions.CrossworldLinkshellSearch, query)); + + /// + /// Gets a link shell by its id. + /// + /// The ID of the linkshell. + /// + /// class containing information about the cross world link shell + public async Task GetLinkshell(string id, int page = 1) => + await GetParsed($"/lodestone/linkshell/{id}?page={page}", + node => new LodestoneLinkshell(this, node, this.Definitions,id)); + + /// + /// Search lodestone for a linkshell with the specified query. + /// + /// object detailing search parameters + /// The page of search results to fetch. + /// containing search results. + public async Task SearchLinkshell(LinkshellSearchQuery query, int page = 1) => + await GetParsed($"/lodestone/linkshell/{query.BuildQueryString()}&page={page}", + node => new LinkshellSearchPage(this, node, this.Definitions.LinkshellSearch, query)); + #endregion #region FreeCompany diff --git a/NetStone/Model/IPaginatedResult.cs b/NetStone/Model/IPaginatedResult.cs index 1991c9a..9e145c4 100644 --- a/NetStone/Model/IPaginatedResult.cs +++ b/NetStone/Model/IPaginatedResult.cs @@ -1,4 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using HtmlAgilityPack; +using NetStone.Definitions.Model; +using NetStone.Search; namespace NetStone.Model; @@ -23,4 +28,143 @@ public interface IPaginatedResult where T : LodestoneParseable /// /// Task of retrieving next page Task GetNextPage(); +} + +/// +/// Container class holding paginated information +/// +public abstract class PaginatedIdResult + : PaginatedResult where TPage : LodestoneParseable + where TEntry : LodestoneParseable + where TEntryDef : PagedEntryDefinition +{ + /// + protected PaginatedIdResult(HtmlNode rootNode, PagedDefinition pageDefinition, + Func> nextPageFunc, string id) + : base(rootNode, pageDefinition, nextPageFunc, id) + { + } +} +/// +/// Container class holding paginated information +/// +public abstract class PaginatedSearchResult + : PaginatedResult where TPage : LodestoneParseable + where TEntry : LodestoneParseable + where TEntryDef : PagedEntryDefinition + where TQuery : ISearchQuery +{ + /// + protected PaginatedSearchResult(HtmlNode rootNode, PagedDefinition pageDefinition, + Func> nextPageFunc, + TQuery query) + : base(rootNode, pageDefinition, nextPageFunc, query) + { + } +} + + +/// + /// Container class holding paginated information + /// + public abstract class PaginatedResult : LodestoneParseable, IPaginatedResult where TPage : LodestoneParseable where TEntry : LodestoneParseable where TEntryDef : PagedEntryDefinition + { + /// + /// Definition for the paginated type + /// + protected readonly PagedDefinition PageDefinition; + + private readonly TRequest request; + + private readonly Func> nextPageFunc; + + /// + /// + /// + /// The root document node of the page + /// CSS definitions for the paginated type + /// Function to retrieve a page of this type + /// The input used to request further pages. + protected PaginatedResult(HtmlNode rootNode, PagedDefinition pageDefinition,Func> nextPageFunc, TRequest request) : base(rootNode) + { + this.PageDefinition = pageDefinition; + this.request = request; + this.nextPageFunc = nextPageFunc; + } + + /// + /// If there is any data + /// + public bool HasResults => this.PageDefinition.NoResultsFound is null || !HasNode(this.PageDefinition.NoResultsFound); + + private TEntry[]? parsedResults; + + /// + /// List of members + /// + protected IEnumerable Results + { + get + { + if (!this.HasResults) return Array.Empty(); + this.parsedResults ??= ParseResults(); + return this.parsedResults; + } + } + + /// + /// Creates the array of all entries on this page> + /// + protected abstract TEntry[] ParseResults(); + + private int? currentPageVal; + + /// + public int CurrentPage + { + get + { + if (!this.HasResults) + return 0; + if (!this.currentPageVal.HasValue) + ParsePagesCount(); + + return this.currentPageVal!.Value; + } + } + + private int? numPagesVal; + + /// + public int NumPages + { + get + { + if (!this.HasResults) + return 0; + if (!this.numPagesVal.HasValue) + ParsePagesCount(); + + return this.numPagesVal!.Value; + } + } + private void ParsePagesCount() + { + var results = ParseRegex(this.PageDefinition.PageInfo); + + this.currentPageVal = int.Parse(results["CurrentPage"].Value); + this.numPagesVal = int.Parse(results["NumPages"].Value); + } + + /// + public async Task GetNextPage() + { + if (!this.HasResults) + return null; + + if (this.CurrentPage == this.NumPages) + return null; + + return await this.nextPageFunc(this.request, this.CurrentPage + 1); + } } \ No newline at end of file diff --git a/NetStone/Model/Parseables/CWLS/LodestoneCrossworldLinkshell.cs b/NetStone/Model/Parseables/CWLS/LodestoneCrossworldLinkshell.cs new file mode 100644 index 0000000..fb7463c --- /dev/null +++ b/NetStone/Model/Parseables/CWLS/LodestoneCrossworldLinkshell.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using HtmlAgilityPack; +using NetStone.Definitions; +using NetStone.Definitions.Model.CWLS; +using NetStone.Model.Parseables.CWLS.Members; + +namespace NetStone.Model.Parseables.CWLS; + +/// +/// Container class holding information about a cross world linkshell and it's members. +/// +public class LodestoneCrossworldLinkshell : PaginatedIdResult +{ + + private readonly CrossworldLinkshellDefinition definition; + + /// + /// Container class for a parseable corss world linkshell page. + /// + /// The to be used to fetch further information. + /// The root document node of the page. + /// The holding definitions to be used to access data. + /// The ID of the cross world linkshell. + public LodestoneCrossworldLinkshell(LodestoneClient client, HtmlNode rootNode, DefinitionsContainer container, string id) + : base(rootNode,container.CrossworldLinkshellMember,client.GetCrossworldLinkshell,id) + { + this.definition = container.CrossworldLinkshell; + } + + /// + /// Name + /// + public string Name => ParseDirectInnerText(this.definition.Name).Trim(); + + /// + /// Datacenter + /// + public string DataCenter => Parse(this.definition.DataCenter); + + /// + /// Members + /// + public IEnumerable Members => this.Results; + + /// + protected override CrossworldLinkshellMemberEntry[] ParseResults() + { + var nodes = QueryContainer(this.PageDefinition); + + var parsedResults = new CrossworldLinkshellMemberEntry[nodes.Length]; + for (var i = 0; i < parsedResults.Length; i++) + { + parsedResults[i] = new CrossworldLinkshellMemberEntry(nodes[i], this.PageDefinition.Entry); + } + return parsedResults; + } +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/CWLS/Members/CrossworldLinkshellMemberEntry.cs b/NetStone/Model/Parseables/CWLS/Members/CrossworldLinkshellMemberEntry.cs new file mode 100644 index 0000000..f7a7790 --- /dev/null +++ b/NetStone/Model/Parseables/CWLS/Members/CrossworldLinkshellMemberEntry.cs @@ -0,0 +1,61 @@ +using HtmlAgilityPack; +using NetStone.Definitions.Model.CWLS; + +namespace NetStone.Model.Parseables.CWLS.Members; + +/// +/// Container class holding information about a cross-world linkshell member. +/// +public class CrossworldLinkshellMemberEntry : LodestoneParseable +{ + private readonly CrossworldLinkshellMemberEntryDefinition definition; + /// + /// Create instance of member entry for a given node + /// + /// Root html node of this entry + /// Css and regex definition + public CrossworldLinkshellMemberEntry(HtmlNode rootNode, CrossworldLinkshellMemberEntryDefinition definition) : base(rootNode) + { + this.definition = definition; + } + + /// + /// Avatar + /// + public string Avatar => Parse(this.definition.Avatar); + + /// + /// ID + /// + public string? Id => ParseHrefId(this.definition.Id); + + /// + /// Name + /// + public string Name => Parse(this.definition.Name); + + /// + /// Rank + /// + public string Rank => Parse(this.definition.Rank); + + /// + /// Rank Icon + /// + public string RankIcon => Parse(this.definition.RankIcon); + + /// + /// Linkshell rank + /// + public string LinkshellRank => Parse(this.definition.LinkshellRank); + + /// + /// Linkshell rank Icon + /// + public string LinkshellRankIcon => Parse(this.definition.LinkshellRankIcon); + + /// + /// Server + /// + public string Server => Parse(this.definition.Server); +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementEntry.cs b/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementEntry.cs index 30de80c..88a891b 100644 --- a/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementEntry.cs +++ b/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementEntry.cs @@ -25,8 +25,11 @@ public CharacterAchievementEntry(HtmlNode rootNode, CharacterAchievementEntryDef /// /// The Name of this achievement /// +#if NETSTANDARD2_1 public string Name => ParseRegex(this.definition.Name).First(r => r.Name.Equals("Name")).Value; - +#else + public string Name => ParseRegex(this.definition.Name).Values.First(r => r.Name.Equals("Name")).Value; +#endif /// /// ID of this achievement /// diff --git a/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementPage.cs b/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementPage.cs index aa1af8b..c761b48 100644 --- a/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementPage.cs +++ b/NetStone/Model/Parseables/Character/Achievement/CharacterAchievementPage.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Collections.Generic; using HtmlAgilityPack; using NetStone.Definitions.Model.Character; @@ -9,13 +7,9 @@ namespace NetStone.Model.Parseables.Character.Achievement; /// /// Holds information about a characters unlocked achievements /// -public class CharacterAchievementPage : LodestoneParseable, IPaginatedResult +public class CharacterAchievementPage : PaginatedIdResult { - private readonly LodestoneClient client; - - private readonly CharacterAchievementDefinition pageDefinition; - - private readonly string charId; + private readonly CharacterAchievementDefinition definition; /// /// Creates a new instance retrieving information about a characters unlocked achievements @@ -24,13 +18,11 @@ public class CharacterAchievementPage : LodestoneParseable, IPaginatedResultRoot node of the achievement page /// Parse definition pack /// ID of the character - public CharacterAchievementPage(LodestoneClient client, HtmlNode rootNode, CharacterAchievementDefinition definition, - string charId) : base(rootNode) + public CharacterAchievementPage(LodestoneClient client, HtmlNode rootNode, + CharacterAchievementDefinition definition,string charId) + : base(rootNode, definition, client.GetCharacterAchievement, charId) { - this.client = client; - this.charId = charId; - - this.pageDefinition = definition; + this.definition = definition; } /// @@ -40,7 +32,7 @@ public int TotalAchievements { get { - var res = ParseRegex(this.pageDefinition.TotalAchievements); + var res = ParseRegex(this.definition.TotalAchievements); return int.Parse(res["TotalAchievements"].Value); } } @@ -48,94 +40,23 @@ public int TotalAchievements /// /// Number of achievement points for this character /// - public int AchievementPoints => int.Parse(Parse(this.pageDefinition.AchievementPoints)); - - /// - /// Indicates if this hold any results - /// - public bool HasResults => !HasNode(this.pageDefinition.NoResultsFound); - - private CharacterAchievementEntry[]? parsedResults; + public int AchievementPoints => int.Parse(Parse(this.definition.AchievementPoints)); /// /// Unlocked achievements for character /// - public IEnumerable Achievements - { - get - { - if (!this.HasResults) - return Array.Empty(); - - if (this.parsedResults == null) - ParseSearchResults(); - - return this.parsedResults!; - } - } - - private void ParseSearchResults() - { - var nodes = QueryContainer(this.pageDefinition); - - this.parsedResults = new CharacterAchievementEntry[nodes.Length]; - for (var i = 0; i < this.parsedResults.Length; i++) - { - this.parsedResults[i] = new CharacterAchievementEntry(nodes[i], this.pageDefinition.Entry); - } - } - - private int? currentPageVal; + public IEnumerable Achievements => this.Results; /// - public int CurrentPage + protected override CharacterAchievementEntry[] ParseResults() { - get - { - if (!this.HasResults) - return 0; - - if (!this.currentPageVal.HasValue) - ParsePagesCount(); - - return this.currentPageVal!.Value; - } - } - - private int? numPagesVal; + var nodes = QueryContainer(this.definition); - /// - public int NumPages - { - get + var parsedResults = new CharacterAchievementEntry[nodes.Length]; + for (var i = 0; i < parsedResults.Length; i++) { - if (!this.HasResults) - return 0; - - if (!this.numPagesVal.HasValue) - ParsePagesCount(); - - return this.numPagesVal!.Value; + parsedResults[i] = new CharacterAchievementEntry(nodes[i], this.definition.Entry); } - } - - private void ParsePagesCount() - { - var results = ParseRegex(this.pageDefinition.PageInfo); - - this.currentPageVal = int.Parse(results["CurrentPage"].Value); - this.numPagesVal = int.Parse(results["NumPages"].Value); - } - - /// - public async Task GetNextPage() - { - if (!this.HasResults) - return null; - - if (this.CurrentPage == this.NumPages) - return null; - - return await this.client.GetCharacterAchievement(this.charId, this.CurrentPage + 1); + return parsedResults; } } \ No newline at end of file diff --git a/NetStone/Model/Parseables/FreeCompany/Members/FreeCompanyMembers.cs b/NetStone/Model/Parseables/FreeCompany/Members/FreeCompanyMembers.cs index 18ac3ba..638c802 100644 --- a/NetStone/Model/Parseables/FreeCompany/Members/FreeCompanyMembers.cs +++ b/NetStone/Model/Parseables/FreeCompany/Members/FreeCompanyMembers.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Collections.Generic; using HtmlAgilityPack; using NetStone.Definitions.Model; using NetStone.Definitions.Model.FreeCompany; @@ -10,13 +8,8 @@ namespace NetStone.Model.Parseables.FreeCompany.Members; /// /// Information about a Free Company's members /// -public class FreeCompanyMembers : LodestoneParseable, IPaginatedResult +public class FreeCompanyMembers : PaginatedIdResult { - private readonly LodestoneClient client; - private readonly string id; - - private readonly PagedDefinition pageDefinition; - /// /// Constructs member list /// @@ -25,97 +18,25 @@ public class FreeCompanyMembers : LodestoneParseable, IPaginatedResult /// public FreeCompanyMembers(LodestoneClient client, HtmlNode rootNode, PagedDefinition definition, string id) : - base(rootNode) + base(rootNode, definition, client.GetFreeCompanyMembers, id) { - this.client = client; - this.id = id; - - this.pageDefinition = definition; } - /// - /// IF there is data - /// - public bool HasResults => true; - - private FreeCompanyMembersEntry[]? parsedResults; - /// /// Lists all members /// - public IEnumerable Members - { - get - { - if (!this.HasResults) - return Array.Empty(); - - if (this.parsedResults == null) - ParseSearchResults(); - - return this.parsedResults!; - } - } - - private void ParseSearchResults() - { - var nodes = QueryContainer(this.pageDefinition); - - this.parsedResults = new FreeCompanyMembersEntry[nodes.Length]; - for (var i = 0; i < this.parsedResults.Length; i++) - { - this.parsedResults[i] = new FreeCompanyMembersEntry(nodes[i], this.pageDefinition.Entry); - } - } - - private int? currentPageVal; + public IEnumerable Members => this.Results; /// - public int CurrentPage + protected override FreeCompanyMembersEntry[] ParseResults() { - get - { - if (!this.HasResults) - return 0; - - if (!this.currentPageVal.HasValue) - ParsePagesCount(); - - return this.currentPageVal!.Value; - } - } + var nodes = QueryContainer(this.PageDefinition); - private int? numPagesVal; - - /// - public int NumPages - { - get + var parsedResults = new FreeCompanyMembersEntry[nodes.Length]; + for (var i = 0; i < parsedResults.Length; i++) { - if (!this.HasResults) - return 0; - - if (!this.numPagesVal.HasValue) - ParsePagesCount(); - - return this.numPagesVal!.Value; + parsedResults[i] = new FreeCompanyMembersEntry(nodes[i], this.PageDefinition.Entry); } - } - - private void ParsePagesCount() - { - var results = ParseRegex(this.pageDefinition.PageInfo); - - this.currentPageVal = int.Parse(results["CurrentPage"].Value); - this.numPagesVal = int.Parse(results["NumPages"].Value); - } - - /// - public async Task GetNextPage() - { - if (this.CurrentPage == this.NumPages) - return null; - - return await this.client.GetFreeCompanyMembers(this.id, this.CurrentPage + 1); + return parsedResults; } } \ No newline at end of file diff --git a/NetStone/Model/Parseables/Linkshell/LodestoneLinkshell.cs b/NetStone/Model/Parseables/Linkshell/LodestoneLinkshell.cs new file mode 100644 index 0000000..fdd335e --- /dev/null +++ b/NetStone/Model/Parseables/Linkshell/LodestoneLinkshell.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using HtmlAgilityPack; +using NetStone.Definitions; +using NetStone.Definitions.Model.Linkshell; +using NetStone.Model.Parseables.Linkshell.Members; + +namespace NetStone.Model.Parseables.Linkshell; + +/// +/// Container class holding information about a linkshell and it's members. +/// +public class LodestoneLinkshell : PaginatedIdResult +{ + private readonly LinkshellDefinition lsDefinition; + + /// + /// Container class for a parseable linkshell page. + /// + /// The to be used to fetch further information. + /// The root document node of the page. + /// The holding definitions to be used to access data. + /// The ID of the cross world linkshell. + public LodestoneLinkshell(LodestoneClient client, HtmlNode rootNode, DefinitionsContainer container, string id) : base(rootNode,container.LinkshellMember, client.GetLinkshell,id) + { + this.lsDefinition = container.Linkshell; + } + + /// + /// Name + /// + public string Name => Parse(this.lsDefinition.Name); + + /// + /// List of members + /// + public IEnumerable Members => this.Results; + + /// + protected override LinkshellMemberEntry[] ParseResults() + { + var nodes = QueryContainer(this.PageDefinition); + + var parsedResults = new LinkshellMemberEntry[nodes.Length]; + for (var i = 0; i < parsedResults.Length; i++) + { + parsedResults[i] = new LinkshellMemberEntry(nodes[i], this.PageDefinition.Entry); + } + return parsedResults; + } +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/Linkshell/Members/LinkshellMemberEntry.cs b/NetStone/Model/Parseables/Linkshell/Members/LinkshellMemberEntry.cs new file mode 100644 index 0000000..7e25762 --- /dev/null +++ b/NetStone/Model/Parseables/Linkshell/Members/LinkshellMemberEntry.cs @@ -0,0 +1,61 @@ +using HtmlAgilityPack; +using NetStone.Definitions.Model.Linkshell; + +namespace NetStone.Model.Parseables.Linkshell.Members; + +/// +/// Container class holding information about a linkshell member. +/// +public class LinkshellMemberEntry : LodestoneParseable +{ + private readonly LinkshellMemberEntryDefinition definition; + /// + /// Create instance of member entry for a given node + /// + /// Root html node of this entry + /// Css and regex definition + public LinkshellMemberEntry(HtmlNode rootNode, LinkshellMemberEntryDefinition definition) : base(rootNode) + { + this.definition = definition; + } + + /// + /// Avatar + /// + public string Avatar => Parse(this.definition.Avatar); + + /// + /// ID + /// + public string? Id => ParseHrefId(this.definition.Id); + + /// + /// Name + /// + public string Name => Parse(this.definition.Name); + + /// + /// Rank + /// + public string Rank => Parse(this.definition.Rank); + + /// + /// Rank Icon + /// + public string RankIcon => Parse(this.definition.RankIcon); + + /// + /// Linkshell rank + /// + public string LinkshellRank => Parse(this.definition.LinkshellRank); + + /// + /// Linkshell rank Icon + /// + public string LinkshellRankIcon => Parse(this.definition.LinkshellRankIcon); + + /// + /// Server + /// + public string Server => Parse(this.definition.Server); +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/Search/CWLS/CrossworldLinkshellSearchEntry.cs b/NetStone/Model/Parseables/Search/CWLS/CrossworldLinkshellSearchEntry.cs new file mode 100644 index 0000000..d41dd4c --- /dev/null +++ b/NetStone/Model/Parseables/Search/CWLS/CrossworldLinkshellSearchEntry.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using HtmlAgilityPack; +using NetStone.Definitions.Model.CWLS; +using NetStone.Model.Parseables.CWLS; + +namespace NetStone.Model.Parseables.Search.CWLS; + +/// +/// Models one entry in the cwls search results list +/// +public class CrossworldLinkshellSearchEntry : LodestoneParseable +{ + private readonly LodestoneClient client; + private readonly CrossworldLinkshellSearchEntryDefinition definition; + + /// + public CrossworldLinkshellSearchEntry(LodestoneClient client, HtmlNode rootNode, CrossworldLinkshellSearchEntryDefinition definition) : + base(rootNode) + { + this.client = client; + this.definition = definition; + } + + /// + /// Character name + /// + public string Name => Parse(this.definition.Name); + + /// + /// Lodestone Id + /// + public string? Id => ParseHrefId(this.definition.Id); + + /// + /// Datacenter + /// + public string DataCenter => Parse(this.definition.Dc); + + + /// + /// Number of active members + /// + public int ActiveMembers => int.TryParse(Parse(this.definition.ActiveMembers), out var parsed) ? parsed : -1; + + /// + /// Fetch cross world link shell + /// + /// Task of retrieving cwls + public async Task GetCrossworldLinkshell() => + this.Id is null ? null : await this.client.GetCrossworldLinkshell(this.Id); + + /// + public override string ToString() => this.Name; +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/Search/CWLS/CrossworldLinkshellSearchPage.cs b/NetStone/Model/Parseables/Search/CWLS/CrossworldLinkshellSearchPage.cs new file mode 100644 index 0000000..d4db614 --- /dev/null +++ b/NetStone/Model/Parseables/Search/CWLS/CrossworldLinkshellSearchPage.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using HtmlAgilityPack; +using NetStone.Definitions.Model; +using NetStone.Definitions.Model.CWLS; +using NetStone.Search.Linkshell; + +namespace NetStone.Model.Parseables.Search.CWLS; + +/// +/// Models cross world link shell search results +/// +public class CrossworldLinkshellSearchPage + : PaginatedSearchResult +{ + private readonly LodestoneClient client; + /// + /// Constructs character search results + /// + /// + /// + /// + /// + public CrossworldLinkshellSearchPage(LodestoneClient client, HtmlNode rootNode, + PagedDefinition pageDefinition, + CrossworldLinkshellSearchQuery currentQuery) + : base(rootNode, pageDefinition, client.SearchCrossworldLinkshell, currentQuery) + { + this.client = client; + } + + + /// + /// List all results + /// + public new IEnumerable Results => base.Results; + + /// + protected override CrossworldLinkshellSearchEntry[] ParseResults() + { + var container = QueryContainer(this.PageDefinition); + + var parsedResults = new CrossworldLinkshellSearchEntry[container.Length]; + for (var i = 0; i < parsedResults.Length; i++) + { + parsedResults[i] = new CrossworldLinkshellSearchEntry(this.client, container[i], this.PageDefinition.Entry); + } + return parsedResults; + } +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/Search/Character/CharacterSearchPage.cs b/NetStone/Model/Parseables/Search/Character/CharacterSearchPage.cs index aa39d5c..252fb25 100644 --- a/NetStone/Model/Parseables/Search/Character/CharacterSearchPage.cs +++ b/NetStone/Model/Parseables/Search/Character/CharacterSearchPage.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Collections.Generic; using HtmlAgilityPack; using NetStone.Definitions.Model; using NetStone.Definitions.Model.Character; @@ -11,12 +9,10 @@ namespace NetStone.Model.Parseables.Search.Character; /// /// Models character search results /// -public class CharacterSearchPage : LodestoneParseable, IPaginatedResult +public class CharacterSearchPage : PaginatedSearchResult { private readonly LodestoneClient client; - private readonly CharacterSearchQuery currentQuery; - - private readonly PagedDefinition pageDefinition; /// /// Constructs character search results @@ -25,101 +21,30 @@ public class CharacterSearchPage : LodestoneParseable, IPaginatedResult /// /// - public CharacterSearchPage(LodestoneClient client, HtmlNode rootNode, PagedDefinition pageDefinition, - CharacterSearchQuery currentQuery) : base(rootNode) + public CharacterSearchPage(LodestoneClient client, HtmlNode rootNode, + PagedDefinition pageDefinition, + CharacterSearchQuery currentQuery) + : base(rootNode, pageDefinition, client.SearchCharacter, currentQuery) { this.client = client; - this.currentQuery = currentQuery; - - this.pageDefinition = pageDefinition; } - /// - /// Indicates if any results are present - /// - public bool HasResults => !HasNode(this.pageDefinition.NoResultsFound); - - private CharacterSearchEntry[]? parsedResults; - /// /// List all results /// - public IEnumerable Results - { - get - { - if (!this.HasResults) - return Array.Empty(); - - if (this.parsedResults == null) - ParseSearchResults(); - - return this.parsedResults!; - } - } - - private void ParseSearchResults() - { - var container = QueryContainer(this.pageDefinition); - - this.parsedResults = new CharacterSearchEntry[container.Length]; - for (var i = 0; i < this.parsedResults.Length; i++) - { - this.parsedResults[i] = new CharacterSearchEntry(this.client, container[i], this.pageDefinition.Entry); - } - } - - private int? currentPageVal; + public new IEnumerable Results => base.Results; /// - public int CurrentPage + protected override CharacterSearchEntry[] ParseResults() { - get - { - if (!this.HasResults) - return 0; - - if (!this.currentPageVal.HasValue) - ParsePagesCount(); - - return this.currentPageVal!.Value; - } - } + var container = QueryContainer(this.PageDefinition); - private int? numPagesVal; - - /// - public int NumPages - { - get + var parsedResults = new CharacterSearchEntry[container.Length]; + for (var i = 0; i < parsedResults.Length; i++) { - if (!this.HasResults) - return 0; - - if (!this.numPagesVal.HasValue) - ParsePagesCount(); - - return this.numPagesVal!.Value; + parsedResults[i] = new CharacterSearchEntry(this.client, container[i], this.PageDefinition.Entry); } - } - - private void ParsePagesCount() - { - var results = ParseRegex(this.pageDefinition.PageInfo); - - this.currentPageVal = int.Parse(results["CurrentPage"].Value); - this.numPagesVal = int.Parse(results["NumPages"].Value); - } - - /// - public async Task GetNextPage() - { - if (!this.HasResults) - return null; - - if (this.CurrentPage == this.NumPages) - return null; - return await this.client.SearchCharacter(this.currentQuery, this.CurrentPage + 1); + return parsedResults; } } \ No newline at end of file diff --git a/NetStone/Model/Parseables/Search/FreeCompany/FreeCompanySearchPage.cs b/NetStone/Model/Parseables/Search/FreeCompany/FreeCompanySearchPage.cs index 2c9f530..635e9d8 100644 --- a/NetStone/Model/Parseables/Search/FreeCompany/FreeCompanySearchPage.cs +++ b/NetStone/Model/Parseables/Search/FreeCompany/FreeCompanySearchPage.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Collections.Generic; using HtmlAgilityPack; using NetStone.Definitions.Model; using NetStone.Definitions.Model.FreeCompany; @@ -11,12 +9,10 @@ namespace NetStone.Model.Parseables.Search.FreeCompany; /// /// Models Free Company search results /// -public class FreeCompanySearchPage : LodestoneParseable, IPaginatedResult +public class FreeCompanySearchPage : PaginatedSearchResult { private readonly LodestoneClient client; - private readonly FreeCompanySearchQuery currentQuery; - - private readonly PagedDefinition pageDefinition; /// /// Constructs Free Company Search results @@ -25,101 +21,29 @@ public class FreeCompanySearchPage : LodestoneParseable, IPaginatedResult /// /// - public FreeCompanySearchPage(LodestoneClient client, HtmlNode rootNode, PagedDefinition pageDefinition, - FreeCompanySearchQuery currentQuery) : base(rootNode) + public FreeCompanySearchPage(LodestoneClient client, HtmlNode rootNode, + PagedDefinition pageDefinition, + FreeCompanySearchQuery currentQuery) + : base(rootNode, pageDefinition, client.SearchFreeCompany, currentQuery) { this.client = client; - this.currentQuery = currentQuery; - - this.pageDefinition = pageDefinition; } - /// - /// Indicates if any results are present - /// - public bool HasResults => !HasNode(this.pageDefinition.NoResultsFound); - - private FreeCompanySearchEntry[]? parsedResults; - /// /// Lists all search results /// - public IEnumerable Results - { - get - { - if (!this.HasResults) - return Array.Empty(); - - if (this.parsedResults == null) - ParseSearchResults(); - - return this.parsedResults!; - } - } - - private void ParseSearchResults() - { - var container = QueryContainer(this.pageDefinition); - - this.parsedResults = new FreeCompanySearchEntry[container.Length]; - for (var i = 0; i < this.parsedResults.Length; i++) - { - this.parsedResults[i] = new FreeCompanySearchEntry(this.client, container[i], this.pageDefinition.Entry); - } - } - - private int? currentPageVal; + public new IEnumerable Results => base.Results; /// - public int CurrentPage + protected override FreeCompanySearchEntry[] ParseResults() { - get - { - if (!this.HasResults) - return 0; + var container = QueryContainer(this.PageDefinition); - if (!this.currentPageVal.HasValue) - ParsePagesCount(); - - return this.currentPageVal!.Value; - } - } - - private int? numPagesVal; - - /// - public int NumPages - { - get + var parsedResults = new FreeCompanySearchEntry[container.Length]; + for (var i = 0; i < parsedResults.Length; i++) { - if (!this.HasResults) - return 0; - - if (!this.numPagesVal.HasValue) - ParsePagesCount(); - - return this.numPagesVal!.Value; + parsedResults[i] = new FreeCompanySearchEntry(this.client, container[i], this.PageDefinition.Entry); } - } - - private void ParsePagesCount() - { - var results = ParseRegex(this.pageDefinition.PageInfo); - - this.currentPageVal = int.Parse(results["CurrentPage"].Value); - this.numPagesVal = int.Parse(results["NumPages"].Value); - } - - /// - public async Task GetNextPage() - { - if (!this.HasResults) - return null; - - if (this.CurrentPage == this.NumPages) - return null; - - return await this.client.SearchFreeCompany(this.currentQuery, this.CurrentPage + 1); + return parsedResults; } } \ No newline at end of file diff --git a/NetStone/Model/Parseables/Search/Linkshell/LinkshellSearchEntry.cs b/NetStone/Model/Parseables/Search/Linkshell/LinkshellSearchEntry.cs new file mode 100644 index 0000000..37a30db --- /dev/null +++ b/NetStone/Model/Parseables/Search/Linkshell/LinkshellSearchEntry.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using HtmlAgilityPack; +using NetStone.Definitions.Model.Linkshell; +using NetStone.Model.Parseables.Linkshell; + +namespace NetStone.Model.Parseables.Search.Linkshell; + +/// +/// Models one entry in the linkshell search results list +/// +public class LinkshellSearchEntry : LodestoneParseable +{ + private readonly LodestoneClient client; + private readonly LinkshellSearchEntryDefinition definition; + + /// + public LinkshellSearchEntry(LodestoneClient client, HtmlNode rootNode, LinkshellSearchEntryDefinition definition) : + base(rootNode) + { + this.client = client; + this.definition = definition; + } + + /// + /// Character name + /// + public string Name => Parse(this.definition.Name); + + /// + /// Lodestone Id + /// + public string? Id => ParseHrefId(this.definition.Id); + + /// + /// Homeworld / Server + /// + public string HomeWorld => Parse(this.definition.Server); + + /// + /// Number of active members + /// + public int ActiveMembers => int.TryParse(Parse(this.definition.ActiveMembers), out var parsed) ? parsed : -1; + + /// + /// Fetch character profile + /// + /// Task of retrieving character + public async Task GetLinkshell() => + this.Id is null ? null : await this.client.GetLinkshell(this.Id); + + /// + public override string ToString() => this.Name; +} \ No newline at end of file diff --git a/NetStone/Model/Parseables/Search/Linkshell/LinkshellSearchPage.cs b/NetStone/Model/Parseables/Search/Linkshell/LinkshellSearchPage.cs new file mode 100644 index 0000000..5040388 --- /dev/null +++ b/NetStone/Model/Parseables/Search/Linkshell/LinkshellSearchPage.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using HtmlAgilityPack; +using NetStone.Definitions.Model; +using NetStone.Definitions.Model.Linkshell; +using NetStone.Search.Linkshell; + +namespace NetStone.Model.Parseables.Search.Linkshell; + +/// +/// Models link shell search results +/// +public class LinkshellSearchPage : PaginatedSearchResult +{ + private readonly LodestoneClient client; + + /// + /// Constructs character search results + /// + /// + /// + /// + /// + public LinkshellSearchPage(LodestoneClient client, HtmlNode rootNode, + PagedDefinition pageDefinition, + LinkshellSearchQuery currentQuery) + : base(rootNode, pageDefinition, client.SearchLinkshell, currentQuery) + { + this.client = client; + } + + /// + /// List all results + /// + public new IEnumerable Results => base.Results; + + /// + protected override LinkshellSearchEntry[] ParseResults() + { + var container = QueryContainer(this.PageDefinition); + + var parsedResults = new LinkshellSearchEntry[container.Length]; + for (var i = 0; i < parsedResults.Length; i++) + { + parsedResults[i] = new LinkshellSearchEntry(this.client, container[i], this.PageDefinition.Entry); + } + return parsedResults; + } +} \ No newline at end of file diff --git a/NetStone/NetStone.xml b/NetStone/NetStone.xml index ce8b192..dd6e04a 100644 --- a/NetStone/NetStone.xml +++ b/NetStone/NetStone.xml @@ -90,6 +90,36 @@ Definitions for Free company search + + + Definitions for cross world link shells + + + + + Definitions for cross world link shell members + + + + + Definitions for cross world link shell searches + + + + + Definitions for link shells + + + + + Definitions for link shell members + + + + + Definitions for link-shell searches + + Loads the definitions from repo @@ -883,6 +913,91 @@ Homeworld + + + Definitions for cross world link shell + + + + + Name + + + + + Name + + + + + + + + + + Avatar + + + + + ID + + + + + Name + + + + + Rank + + + + + Rank Icon + + + + + Linkshell rank + + + + + Linkshell rank Icon + + + + + Server + + + + + Definition container for one Cross World Link Shell search result entry + + + + + ID + + + + + Name + + + + + Rank + + + + + Rank Icon + + Definitions for FC estate @@ -1239,6 +1354,86 @@ Interface for all node definitions + + + Definitions for link shell + + + + + Name + + + + + Definition for one entry of the linkshell memebr list + + + + + Avatar + + + + + ID + + + + + Name + + + + + Rank + + + + + Rank Icon + + + + + Linkshell rank + + + + + Linkshell rank Icon + + + + + Server + + + + + Definition container for one Link-Shell search result entry + + + + + ID + + + + + Name + + + + + Rank + + + + + Rank Icon + + Hold information about the uri for which the definition packs are valid @@ -1635,6 +1830,38 @@ The page of search results to fetch. containing search results. + + + Gets a cross world link shell by its id. + + The ID of the cross world linkshell. + + class containing information about the cross world link shell + + + + Search lodestone for a character with the specified query. + + object detailing search parameters + The page of search results to fetch. + containing search results. + + + + Gets a link shell by its id. + + The ID of the linkshell. + + class containing information about the cross world link shell + + + + Search lodestone for a linkshell with the specified query. + + object detailing search parameters + The page of search results to fetch. + containing search results. + Get a character by its Lodestone ID. @@ -1715,6 +1942,65 @@ Task of retrieving next page + + + Container class holding paginated information + + + + + + + + Container class holding paginated information + + + + + + + + Container class holding paginated information + + + + + Definition for the paginated type + + + + + + + The root document node of the page + CSS definitions for the paginated type + Function to retrieve a page of this type + The input used to request further pages. + + + + If there is any data + + + + + List of members + + + + + Creates the array of all entries on this page> + + + + + + + + + + + Main superclass for parsed lodestone nodes. @@ -1916,23 +2202,12 @@ Number of achievement points for this character - - - Indicates if this hold any results - - Unlocked achievements for character - - - - - - - + @@ -2850,6 +3125,90 @@ "Name on World" + + + Container class holding information about a cross world linkshell and it's members. + + + + + Container class for a parseable corss world linkshell page. + + The to be used to fetch further information. + The root document node of the page. + The holding definitions to be used to access data. + The ID of the cross world linkshell. + + + + Name + + + + + Datacenter + + + + + Members + + + + + + + + Container class holding information about a cross-world linkshell member. + + + + + Create instance of member entry for a given node + + Root html node of this entry + Css and regex definition + + + + Avatar + + + + + ID + + + + + Name + + + + + Rank + + + + + Rank Icon + + + + + Linkshell rank + + + + + Linkshell rank Icon + + + + + Server + + Information about the Free CCompany's estate @@ -3150,23 +3509,12 @@ - - - IF there is data - - Lists all members - - - - - - - + @@ -3245,6 +3593,85 @@ Link to the top layer image of the icon. + + + Container class holding information about a linkshell and it's members. + + + + + Container class for a parseable linkshell page. + + The to be used to fetch further information. + The root document node of the page. + The holding definitions to be used to access data. + The ID of the cross world linkshell. + + + + Name + + + + + List of members + + + + + + + + Container class holding information about a linkshell member. + + + + + Create instance of member entry for a given node + + Root html node of this entry + Css and regex definition + + + + Avatar + + + + + ID + + + + + Name + + + + + Rank + + + + + Rank Icon + + + + + Linkshell rank + + + + + Linkshell rank Icon + + + + + Server + + Models one entry in the character search results list @@ -3286,23 +3713,71 @@ - - - Indicates if any results are present - - List all results - + - + + + Models one entry in the cwls search results list + + + + + + + + Character name + + + + + Lodestone Id + + + + + Datacenter + + + + + Number of active members + + + + + Fetch cross world link shell + + Task of retrieving cwls + + - + + + Models cross world link shell search results + + + + + Constructs character search results + + + + + + + + + List all results + + + @@ -3396,23 +3871,71 @@ - - - Indicates if any results are present - - Lists all search results - + - + + + Models one entry in the linkshell search results list + + + + + + + + Character name + + + + + Lodestone Id + + + + + Homeworld / Server + + + + + Number of active members + + + + + Fetch character profile + + Task of retrieving character + + - + + + Models link shell search results + + + + + Constructs character search results + + + + + + + + + List all results + + + @@ -3857,6 +4380,131 @@ Search parameters to append to the request uri + + + Models a search for a link shell + + + + + Datacenter + This is ignored if is set + + + + + Home-world + + + + + + + + Models a search for a cross world link shell + + + + + Datacenter + + + + + + + + Models a search for a cross world link shell + + + + + Only search for actively recruiting + + + + + Name + + + + + Active member count + + + + + Sort order + + + + + + + + Available choice for member count + + + + + All + + + + + 1-10 + + + + + 11-30 + + + + + 31-50 + + + + + Over 51 + + + + + Ways to sort linkshell and cwls search results + + + + + Creation date (newest to oldest) + + + + + Creation date (oldest to newest) + + + + + Name (A - Z) + + + + + Name (Z - A) + + + + + Membership (high to low) + + + + + Membership (low to high) + + The ClassJob IDs. diff --git a/NetStone/Search/Linkshell/LinkshellSearchQuery.cs b/NetStone/Search/Linkshell/LinkshellSearchQuery.cs new file mode 100644 index 0000000..1502491 --- /dev/null +++ b/NetStone/Search/Linkshell/LinkshellSearchQuery.cs @@ -0,0 +1,172 @@ +using System; +using System.Text; + +namespace NetStone.Search.Linkshell; + +/// +/// Models a search for a link shell +/// +public class LinkshellSearchQuery : BaseLinkshellSearchQuery +{ + + /// + /// Datacenter + /// This is ignored if is set + /// + public string DataCenter { get; set; } = ""; + + /// + /// Home-world + /// + public string HomeWorld { get; set; } = ""; + /// + public override string BuildQueryString() + { + if(string.IsNullOrEmpty(this.Name)) + throw new ArgumentException("Name must not be empty or null.", nameof(this.Name)); + + var query = new StringBuilder(); + + query.Append($"?q={this.Name}"); + if(this.RecruitingOnly) + query.Append("&cf_public=1"); + query.Append($"&worldname={(string.IsNullOrEmpty(this.HomeWorld) ? $"_dc_{this.DataCenter}" : this.HomeWorld)}"); + query.Append($@"&character_count={this.ActiveMembers switch + { + LinkshellSizeCategory.OneToTen => "1-10", + LinkshellSizeCategory.ElevenToThirty => "11-30", + LinkshellSizeCategory.ThirtyOneToFifty => "31-51", + LinkshellSizeCategory.OverFiftyOne => "51-", + _ => "", + }}"); + query.Append($"&order={this.Sorting:D}"); + + return query.ToString(); + } +} + +/// +/// Models a search for a cross world link shell +/// +public class CrossworldLinkshellSearchQuery : BaseLinkshellSearchQuery +{ + + /// + /// Datacenter + /// + public string DataCenter { get; set; } = ""; + /// + public override string BuildQueryString() + { + if(string.IsNullOrEmpty(this.Name)) + throw new ArgumentException("Name must not be empty or null.", nameof(this.Name)); + + var query = new StringBuilder(); + + query.Append($"?q={this.Name}"); + if(this.RecruitingOnly) + query.Append("&cf_public=1"); + query.Append($"&dcname={this.DataCenter}"); + query.Append($@"&character_count={this.ActiveMembers switch + { + LinkshellSizeCategory.OneToTen => "1-10", + LinkshellSizeCategory.ElevenToThirty => "11-30", + LinkshellSizeCategory.ThirtyOneToFifty => "31-51", + LinkshellSizeCategory.OverFiftyOne => "51-", + _ => "", + }}"); + query.Append($"&order={this.Sorting:D}"); + + return query.ToString(); + } +} + +/// +/// Models a search for a cross world link shell +/// +public abstract class BaseLinkshellSearchQuery : ISearchQuery +{ + /// + /// Only search for actively recruiting + /// + public bool RecruitingOnly { get; set; } + + /// + /// Name + /// + public string Name { get; set; } = ""; + + /// + /// Active member count + /// + public LinkshellSizeCategory ActiveMembers { get; set; } = LinkshellSizeCategory.All; + + + /// + /// Sort order + /// + public LinkshellSortKind Sorting { get; set; } = LinkshellSortKind.CreationDateNewToOld; + + /// + public abstract string BuildQueryString(); +} + +/// +/// Available choice for member count +/// +public enum LinkshellSizeCategory +{ + /// + /// All + /// + All, + /// + /// 1-10 + /// + OneToTen, + /// + /// 11-30 + /// + ElevenToThirty, + /// + /// 31-50 + /// + ThirtyOneToFifty, + /// + /// Over 51 + /// + OverFiftyOne, +} + +/// +/// Ways to sort linkshell and cwls search results +/// +public enum LinkshellSortKind +{ + /// + /// Creation date (newest to oldest) + /// + CreationDateNewToOld = 1, + /// + /// Creation date (oldest to newest) + /// + CreationDateOldToNew = 2, + /// + /// Name (A - Z) + /// + NameAtoZ = 3, + /// + /// Name (Z - A) + /// + NameZtoA = 4, + /// + /// Membership (high to low) + /// + MemberCountDesc = 5, + /// + /// Membership (low to high) + /// + MemberCountAsc = 6, + + +} \ No newline at end of file diff --git a/README.md b/README.md index 65fe30c..ac94016 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ NetStone is a portable and modern .NET FFXIV Lodestone API. - [x] FC Search - [ ] PvP Teams - [ ] PvP Team Search -- [ ] Linkshell -- [ ] Linkshell Search -- [ ] CWLS -- [ ] CWLS Search +- [x] Linkshell +- [x] Linkshell Search +- [x] CWLS +- [x] CWLS Search Eorzea DB support is not planned.