From 777dc600441db88b686f700f51499276005e3d4b Mon Sep 17 00:00:00 2001 From: sharkusmanch <9328629+sharkusmanch@users.noreply.github.com> Date: Sat, 14 Aug 2021 15:57:21 -0700 Subject: [PATCH] Initial. Name and Links metadata --- .gitignore | 5 + PCGWClient.cs | 87 +++++++++++++++ PCGWGame.cs | 77 ++++++++++++++ PCGamingWikiMetadata.cs | 48 +++++++++ PCGamingWikiMetadata.csproj | 31 ++++++ PCGamingWikiMetadataProvider.cs | 128 +++++++++++++++++++++++ PCGamingWikiMetadataSettings.cs | 62 +++++++++++ PCGamingWikiMetadataSettingsView.xaml | 11 ++ PCGamingWikiMetadataSettingsView.xaml.cs | 25 +++++ README.md | 12 +++ extension.yaml | 7 ++ icon.png | Bin 0 -> 9326 bytes packages.config | 5 + 13 files changed, 498 insertions(+) create mode 100644 .gitignore create mode 100644 PCGWClient.cs create mode 100644 PCGWGame.cs create mode 100644 PCGamingWikiMetadata.cs create mode 100644 PCGamingWikiMetadata.csproj create mode 100644 PCGamingWikiMetadataProvider.cs create mode 100644 PCGamingWikiMetadataSettings.cs create mode 100644 PCGamingWikiMetadataSettingsView.xaml create mode 100644 PCGamingWikiMetadataSettingsView.xaml.cs create mode 100644 README.md create mode 100644 extension.yaml create mode 100644 icon.png create mode 100644 packages.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2a23c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +dist/ +obj/ +tmp/ +*.sln \ No newline at end of file diff --git a/PCGWClient.cs b/PCGWClient.cs new file mode 100644 index 0000000..630d023 --- /dev/null +++ b/PCGWClient.cs @@ -0,0 +1,87 @@ +using Playnite.SDK; +using RestSharp; +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace PCGamingWikiMetadata +{ + public class PCGWClient + { + private readonly ILogger logger = LogManager.GetLogger(); + private readonly string baseUrl = @"https://www.pcgamingwiki.com/w/api.php"; + private RestClient client; + + public PCGWClient() + { + client = new RestClient(baseUrl); + } + + public JObject ExecuteRequest(RestRequest request) + { + request.AddParameter("format", "json", ParameterType.QueryString); + request.OnBeforeDeserialization = resp => { resp.ContentType = "application/json"; }; + var fullUrl = client.BuildUri(request); + logger.Info(fullUrl.ToString()); + var response = client.Execute(request); + + if (response.ErrorException != null) + { + const string message = "Error retrieving response. Check inner details for more info."; + var e = new Exception(message, response.ErrorException); + throw e; + } + var content = response.Content; + + logger.Debug(content); + return JObject.Parse(content); + } + + private string NormalizeSearchString(string search) + { + string updated = search.Replace("-", " "); + + return updated; + } + + public List SearchGames(string searchName) + { + List gameResults = new List(); + logger.Info(searchName); + + var request = new RestRequest("/", Method.GET); + request.AddParameter("action", "query", ParameterType.QueryString); + request.AddParameter("list", "search", ParameterType.QueryString); + request.AddParameter("srsearch", NormalizeSearchString(searchName), ParameterType.QueryString); + + try + { + JObject searchResults = ExecuteRequest(request); + JToken error; + + if (searchResults.TryGetValue("error", out error)) + { + logger.Error($"Encountered API error: {error.ToString()}"); + return gameResults; + } + + logger.Debug($"SearchGames {searchResults["query"]["searchinfo"]["totalhits"]} results for {searchName}"); + + foreach (dynamic game in searchResults["query"]["search"]) + { + if (!((string)game.snippet).Contains("#REDIRECT")) + { + PCGWGame g = new PCGWGame((string)game.title, (int)game.pageid); + gameResults.Add(g); + } + } + } + catch (Exception e) + { + logger.Error(e, "Error performing search"); + } + + return gameResults; + } + } +} \ No newline at end of file diff --git a/PCGWGame.cs b/PCGWGame.cs new file mode 100644 index 0000000..b846afb --- /dev/null +++ b/PCGWGame.cs @@ -0,0 +1,77 @@ +using System; +using Playnite.SDK; +using Playnite.SDK.Models; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace PCGamingWikiMetadata +{ + public class PCGWGame : GenericItemOption + { + + public int PageID {get; set; } + + private List links; + public List Links { get { return links; } } + private IDictionary> data; + public IDictionary> Data { get { return data; } } + + private IDictionary> sobj; + public IDictionary> Sobj { get {return sobj;} } + + public PCGWGame() + { + this.links = new List(); + } + + public PCGWGame(string name, int pageid) + { + this.Name = name; + this.PageID = pageid; + this.links = new List(); + this.links.Add(PCGamingWikiLink()); + } + protected Link PCGamingWikiLink() + { + string escapedName = Uri.EscapeUriString(this.Name); + return new Link("PCGamingWiki", $"https://www.pcgamingwiki.com/wiki/{escapedName}"); + } + + public void Update(JObject gameData) + { + UpdateDynamic(gameData); + } + + protected void UpdateDynamic(dynamic gameData) + { + this.data = DataToDictionary(gameData.query.data); + + if (gameData.query.sobj.Count != 1) + { + Console.WriteLine($"Got sobj list of size: {gameData.query.sobj.Count}"); + } + + this.sobj = DataToDictionary(gameData.query.sobj[0].data); + + this.Name = this.Sobj["_SKEY"][0]; + } + + private IDictionary> DataToDictionary(JArray data) + { + IDictionary> dataDict = new Dictionary>(); + + foreach (dynamic attribute in data) + { + IList dataItems = new List(); + foreach (dynamic item in attribute.dataitem) + { + dataItems.Add((string)item.item); + } + + dataDict.Add((string)attribute.property, dataItems); + } + + return dataDict; + } + } +} \ No newline at end of file diff --git a/PCGamingWikiMetadata.cs b/PCGamingWikiMetadata.cs new file mode 100644 index 0000000..a1b8b35 --- /dev/null +++ b/PCGamingWikiMetadata.cs @@ -0,0 +1,48 @@ +using Playnite.SDK; +using Playnite.SDK.Plugins; +using System; +using System.Collections.Generic; +using System.Windows.Controls; + + +namespace PCGamingWikiMetadata +{ + public class PCGamingWikiMetadata : MetadataPlugin + { + private static readonly ILogger logger = LogManager.GetLogger(); + + private PCGamingWikiMetadataSettings settings { get; set; } + + public override Guid Id { get; } = Guid.Parse("c038558e-427b-4551-be4c-be7009ce5a8d"); + + public override List SupportedFields { get; } = new List + { + MetadataField.Name, + MetadataField.Links + }; + + // Change to something more appropriate + public override string Name => "PCGamingWiki"; + + public PCGamingWikiMetadata(IPlayniteAPI api) : base(api) + { + settings = new PCGamingWikiMetadataSettings(this); + } + + public override OnDemandMetadataProvider GetMetadataProvider(MetadataRequestOptions options) + { + return new PCGamingWikiMetadataProvider(options, this); + } + + public override ISettings GetSettings(bool firstRunSettings) + { + return settings; + } + + public override UserControl GetSettingsView(bool firstRunSettings) + { + return new PCGamingWikiMetadataSettingsView(); + } + + } +} \ No newline at end of file diff --git a/PCGamingWikiMetadata.csproj b/PCGamingWikiMetadata.csproj new file mode 100644 index 0000000..6a30e5b --- /dev/null +++ b/PCGamingWikiMetadata.csproj @@ -0,0 +1,31 @@ + + + + net462 + true + + + + + + + + + + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/PCGamingWikiMetadataProvider.cs b/PCGamingWikiMetadataProvider.cs new file mode 100644 index 0000000..6f2bcdf --- /dev/null +++ b/PCGamingWikiMetadataProvider.cs @@ -0,0 +1,128 @@ +using Playnite.SDK.Plugins; +using Playnite.SDK; +using Playnite.SDK.Models; +using System.Collections.Generic; +using System; + +namespace PCGamingWikiMetadata +{ + public class PCGamingWikiMetadataProvider : OnDemandMetadataProvider + { + private readonly MetadataRequestOptions options; + private readonly PCGamingWikiMetadata plugin; + + private PCGWClient client; + + private PCGWGame pcgwData; + private static readonly ILogger logger = LogManager.GetLogger(); + + private List availableFields; + public override List AvailableFields + { + get + { + if (availableFields == null) + { + availableFields = GetAvailableFields(); + } + + return availableFields; + } + } + + private List GetAvailableFields() + { + if (pcgwData == null) + { + GetPCGWMetadata(); + } + + var fields = new List { MetadataField.Name }; + return fields; + } + + private void GetPCGWMetadata() + { + if (pcgwData != null) + { + return; + } + + logger.Debug("GetPCGWMetadata"); + + if (!options.IsBackgroundDownload) + { + logger.Debug("Starting selection..."); + + var item = plugin.PlayniteApi.Dialogs.ChooseItemWithSearch(null, (a) => + { + return client.SearchGames(a); + }, options.GameData.Name); + + if (item != null) + { + var searchItem = item as PCGWGame; + logger.Debug($"GetPCGWMetadata for {searchItem.Name}"); + this.pcgwData = (PCGWGame)item; + } + else + { + this.pcgwData = new PCGWGame(); + logger.Warn($"Cancelled search"); + } + } + else + { + try + { + List results = client.SearchGames(options.GameData.Name); + + if (results.Count == 0) + { + this.pcgwData = new PCGWGame(); + return; + } + + if (results.Count > 1) + { + logger.Warn($"More than one result for {options.GameData.Name}. Using first result."); + } + + this.pcgwData = (PCGWGame)results[0]; + } + catch (Exception e) + { + logger.Error(e, "Failed to get PCGW metadata."); + } + } + } + + public PCGamingWikiMetadataProvider(MetadataRequestOptions options, PCGamingWikiMetadata plugin) + { + this.options = options; + this.plugin = plugin; + this.client = new PCGWClient(); + } + + public override string GetName() + { + if (AvailableFields.Contains(MetadataField.Name)) + { + return this.pcgwData.Name; + } + + return base.GetName(); + } + + + public override List GetLinks() + { + if (AvailableFields.Contains(MetadataField.Links)) + { + return this.pcgwData.Links; + } + + return base.GetLinks(); + } + } +} \ No newline at end of file diff --git a/PCGamingWikiMetadataSettings.cs b/PCGamingWikiMetadataSettings.cs new file mode 100644 index 0000000..335fcf5 --- /dev/null +++ b/PCGamingWikiMetadataSettings.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using Playnite.SDK; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PCGamingWikiMetadata +{ + public class PCGamingWikiMetadataSettings : ISettings + { + private readonly PCGamingWikiMetadata plugin; + + // Parameterless constructor must exist if you want to use LoadPluginSettings method. + public PCGamingWikiMetadataSettings() + { + } + + public PCGamingWikiMetadataSettings(PCGamingWikiMetadata plugin) + { + // Injecting your plugin instance is required for Save/Load method because Playnite saves data to a location based on what plugin requested the operation. + this.plugin = plugin; + + // Load saved settings. + var savedSettings = plugin.LoadPluginSettings(); + + // LoadPluginSettings returns null if not saved data is available. + if (savedSettings != null) + { + // empty for now + } + } + + public void BeginEdit() + { + // Code executed when settings view is opened and user starts editing values. + } + + public void CancelEdit() + { + // Code executed when user decides to cancel any changes made since BeginEdit was called. + // This method should revert any changes made to Option1 and Option2. + } + + public void EndEdit() + { + // Code executed when user decides to confirm changes made since BeginEdit was called. + // This method should save settings made to Option1 and Option2. + plugin.SavePluginSettings(this); + } + + public bool VerifySettings(out List errors) + { + // Code execute when user decides to confirm changes made since BeginEdit was called. + // Executed before EndEdit is called and EndEdit is not called if false is returned. + // List of errors is presented to user if verification fails. + errors = new List(); + return true; + } + } +} \ No newline at end of file diff --git a/PCGamingWikiMetadataSettingsView.xaml b/PCGamingWikiMetadataSettingsView.xaml new file mode 100644 index 0000000..d5f5557 --- /dev/null +++ b/PCGamingWikiMetadataSettingsView.xaml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/PCGamingWikiMetadataSettingsView.xaml.cs b/PCGamingWikiMetadataSettingsView.xaml.cs new file mode 100644 index 0000000..3137f9c --- /dev/null +++ b/PCGamingWikiMetadataSettingsView.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace PCGamingWikiMetadata +{ + public partial class PCGamingWikiMetadataSettingsView : UserControl + { + public PCGamingWikiMetadataSettingsView() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..db46e8e --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Playnite PCGamingWiki Metadata Provider + +## Overview + +Plugin for [Playnite](https://playnite.link) that retrieves game metadata from [PCGamingWiki](https://www.pcgamingwiki.com/wiki/Home) + +## Current Metadata + +* Name +* Links + +Additional metadata will be added in upcoming versions \ No newline at end of file diff --git a/extension.yaml b/extension.yaml new file mode 100644 index 0000000..a5e42fc --- /dev/null +++ b/extension.yaml @@ -0,0 +1,7 @@ +Id: PCGamingWikiMetadata_c038558e-427b-4551-be4c-be7009ce5a8d +Name: PCGamingWiki Metadata Provider +Author: sharkusmanch +Version: 0.1.0 +Module: PCGamingWikiMetadata.dll +Type: MetadataProvider +Icon: icon.png \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5ee3971e67ec67c9aedd5f3fa4811d8ed3ec1c67 GIT binary patch literal 9326 zcmd6Nhg%cP7w&}6q(`Mn6I4LDH0h{85GjHJ0@9>+kO0zSL4t_DCoOb>p&AgRcSS%X z5=!U@*g_YS8tPqt_m8+Y&*s@=cQTnd^*!&|xLd}0tW11N007n-`baYXV9-YxV5Emm z_a2nELMPh5t2gd2Lazu$XAJcHgulL3AOP^He{a~lNta==H?SU(WJ@1YxF^k(pS$do7>CY zu*yv1OG_tb^Ij)0;<{aEX-h4oWnk5RE_HUj<+#IR`kcd_a$d%D+w;7KXX_SyWhSp( zydg`<4O5~eN!TONBJ?`YWeyQuIl)+Foj)z&e3+AAFY?~ejx zeghrjW1;RuA^Vy3=wnZiq2gz!4qjTBnVy9S$2)dDOWj(VJwDf0ZA|0Of)MyZd;;5K zxHU{AEhw76$LZ#zMJBt5e&8XZ!yb4`%F0I95ePnhEXAgmUI=NowfN1pU4upBViR2` zPAO*B0E;jV23T_I;f*Jkf;KSs4wN;ig>IV^ zd*o>6iQ()y24y8BB~`MDsK(>-h3c)!kP-Es*DD@gya3W^cAcN-ln*Ld? ztEF2t4@~s_rJYz*LYR`ZVG&6Z4x;QnDN8k-7g^Tm5j+ z2ZmmPUUBZ1(3QfJ4KWe=^=z{xd5~ysVR5u~w7)hhP+TOQq3AIbfJDh4a+n=>5auDj zVx|{YS3=;s^62o)tgO5fbEo5<&71^HYDfIw+7B-HxUw;GhXu%jX*gPq-d-N7Ja*&X zd+hxEW89MzGdI}f;&;RB+vBG2D%qqSVoNgCt*)j>vX>KOMGAhz>G`>0a-pME&QT6_Z|%NZpNzm|XbV7);%z4U0~>T$|Rg zpx4aSMIL1;vk@DoO7AQo@#GUol!$_m39<)PLe4+cTt|8}2AeD5wtMIpWn*L0G;b}T zWpD|QM@Ax(F&6M$pT|7{FKOkTV(3F&E?fh5LdkqTL-ibwYh0{m8-AtP*bz(LiWI-5 zL3?C+E{_rvVAG8GQofp0%;Kc-Mo2u{wB%Y^aSDxskRkF}P9jf@r=tl{7`G$Ux7Tl^ z@)gBxfcftPn9m8Zn{A8SueeY77T{HNExnuHa^gHL=vT)hc#lbj+}XI3`R8al#@dgz zuDH0kWPxn{dM@?XqKo!*x5M#d(KMIy2H&oTaMpu%MMXsx2}#Lte!h?+!%JuG1X~wq zXm1Ck**>?%QZ+=vh7`Yuu5NNO>k5FSLD`SYV)4OfkjH%~VSvMBBU0&gYXN`VO{>+|2Y8hrv@Vr*`j zxNAI)8+BMQ(JvlmC*^6k=MjVxB$|N#dphnrMP{W&Kue&WOT}I7G3T&*`Mc5E|MmVf zNt3p*$aQ+Hs>=;7N=l|*+OP^(_P?6^!U3n0QB9g?40Lv#1*y%1NuQvMc=cJ9p2l<{yGIf+HxDv-{5(dNQll0HA^i^Br ze^P57R^;`$Y3;>}Ajlcr?yEOz9#8}4UiEsxA}&Bn9IOp^KKiLPs;;cevzm}h?;d{Q zmW|E6DW1#>*b;2wPbua+YpUZWyq|%>^H6cX56rp9f6F;*>TNvMF=x*w#NsTAs~ zMPG1yArg@2pq64wW6g;Ut-I=kYzEK@!cU@aejojoVmF?lTduMnI8Vxh0&Y^qfDGUm zjBqcJ6bevwwN?AiJ;>LOr(46dF7dj2zPVm`dAa&4OdD4{Tm9yL{}v{dg*9nit(%gm zkyfOyVFJiIij}vQCNqK-^>Iqkf(VZ-%v)vHzZFXXw~vHQX*I?Et4+xAVC(jWQ$F<` z@A6`lVLB39s|`w!@pVtc>rI_QkHsyV2WwsxTTk)~N*7h>SH!OouczcKT{u&DM7r1m^7$CfhwpoxA2@$gV9dJD_OE8$v6sa+i`lP9qP6Fp!(LZc{|!|M6M0kogryVXBQICMrVlEfmwNZO59u&X zrdL~tFnOOLcnlY{GCL%`-1LOuj*m?b*SRLpK8BFHLo;+Y0v9+1q>` zMwd1^Ae*Y6dzIzeHA`hBs)diN7a*Xxb zcz8Sjl*A)33(KZyi%E*;iEFWJnr?@CTT_ow-kC+Uu3s|4p)ASUA=ES!w7hA&;&gF8 z>z(TtH5swavqc^k@nm*?;f$w*kynx3q#-r{rEMB80?mbH6Y-peY*)I-!`hLv+H<7L z5(*&gEOP}B)Fx$P)|zcciV+l3cQJ%~8gaZY-GEQtjfCZ`r(N;fLVjKxG|BXUwfOi3 z=p3uGU)vA3Ehc5(c5pamAKjXZ5u~kIdzia&?c)CI=g*&WGog)F)zP_Dc~4aTZ48{B zIpNSmsur!aG#L5C6RYnGb2!-j^9zZ(a_5)oBW#wk_xQShii__|i|WYWU;r7~IY)KchZ$IrV zGcOke13ng~kx!V0EOoYzY1a7>htDy&u=Tob>)XIi1Sba5NPzD%`nUb~FvRyuP3K%X z=!|98euh0hscQvo%fxrO!8B`){a)y=RQp>7zuqvMo~Ti>w>#OvgeNaq+J*jFd@~m2 zu|zH{7RSqo;ZA_E`=d4|xtYpyCVHSPSCPeOvn6yR zETYVZLc~77Hga}QasvPJ`j8JsTJ2x(`taq}rM|KW~ z%fMnYhe(kZ^nk&ja$Q7W+1PK6(WEn<6=MB zBsQ;Zd`2&HTGXGWSa6*!nD+l#*FjWJLN1UU7OHYON42^ zI`0)vbAwuJQ6nkQ80>D>w;$3V4wcn4#Rr_m6daH%Eg-4HK7)J9uH+E-hu-} zpB7!ukvu9Sti#;D2V3MbGB?=?jW7!cM zTlp+hT~4tFDv^r8<;zmXzQQ7M8)pNS#h;ojaR`sU(k?w|6Mw99;BqEMxM}FiT9mI%{4B*mSsS7{G1|@ibTyrXBaN2dkyRUmDb#{b`g9CBaC~u`ADUG`z z36gUn)bUFLMm*)_(p$ABGJeM{mMckad6CN7b1AF}iFBK*o7A`O#Bgc63a(PT+EFcq z+#M(5o(B9#P?@1|up&cc7Yi^UW~q?=&`@^Tj}PBL7-9`XtUt9wDpb77_C!5di%Ign zo+xNLGBY#tl1CO+E#ekDDLYEA;hw}jx5>=RG`>$ETz+c^6OR57@E=!kWU!z@dr3-W zt=Cw^7IlWh@Z8DnfF6O60vD?<2BoKdkeM9V(QCZVs&@C5D@hX-EFfzAi4eb!)u@z6 z6Ak+L6|ZU>4o4{~uLzUP)2gHu1Q5Z{e-794WPABp?!`3Bv)bC)Lp81ds~2Zfd_~cd z;LJPU?Nx(Py^PQ^=z2_vHk)@j#8$vEJ&Yvqf(+}?#Dfrio{#hr_y_}^^Y<<8UCw?f4GHI<*KXF9{drMQ5i_5>VHTZw~(AMkx*Aq`R z1n~Q0zGsJ8B4v=eU#zvVXsrvRh=P*x@_j{t@g2mBg+ctdd=BO@aM zQKKJk(c{q+d_T{67Ru;DP$i;4&!DKH5{;CoO}k`wLpCO?R%*Eq8vKY*MHhA^>3-YM z8rdN$O{`K@;W*+W29Z2n_fO4`SOcNrjjo3NzR>GR#@aTzxp-rBZ4j~3JR4bFR7CM2 zwm?|DEt0cZg++9r*b2@HhzEG#L)9)fJS660&HEoQlGpxY06Z$b_l~0u_cZm9KUOwB zPW(YFZicQ5r@7cFhi`k2sk0eDyh9_zu9agJCNMS6mdanr{^Z%sf9(piF1E6xg%Wt= z@4JX3F9_9CK>7^CQWJsJ_*aN1;C7qP96x^8pRJm!ebFnEA_!iI4tbV~c2t|Zll}I^tFSnNv5J-irE4dnnm%)&HB?RASv@RAS0iC&sH?@rM)v)*MXZ12<$=RvD`d3w4+ z7~GRr;dmF~$U<6Tvmqr%A1>RscM}WaN^&tTIBL0mFjBgY*sq|Xz%2uU3nlPofgP5r z2r4Jkg03WyU`Ro5wPG_7o^cZysF%REC59(gW>9%Mf`u3la%<6aEA^7HJ$(QjL*f zavLX*w0+h2|bydS|(OBVbx*{GA(Vd1Pe71AKLLRwLimVw#Y| z)7UUDIwRzg^*3SuS2^0Iaa6_&){(nr26Wq@-ouKwFRu@oNRXG^AYO&*)BHBgiGR3T zf$>|_ef8%qw5K532%UK=n$<55)a1ir^Kr{Mbl?huK%UdjM>;f65|Lrl_8pA;H?B77 zVo!(YB5Ln;=)q1&@h!XTEv7kVn<^Q`B~>(u;b*ZP%AMGRdTlo5X*H^Q#d~I=R~LV$ zl6Ww|4`-+41T=BiH4!2B=3k&7b2r1xcw%E3+Tkf?# zO!<;E?@?$9?m`}F3Vajwz4F__-mZm^P4y152q1Qs4YGZYcCG1K-l7@qTYe;B{Xf>x z@+Z$$#{qRi+ZdTuvSITCZ&CToakq_y@jk1tnD>Bn@;W;a1eUN4{=OdX@(bO*Cju^B%UH!@=kbMne4Hu zb06t!W2`CK+gY=Z{1(F<7UlDGjIZP9aQ|BS5ySU5B18guS_hRDpCnt4&dmH?Hbz$h z9b`c(cpZR`A3e!@tLsfFHqw@Ux0Tud1?fjPvd18?+k^Wf(d|F%arSe71{ z;c_au(y~aC0rXXC#l2ZgJ-~&&-g1_;*7f@2vSZ&GRy%L+T{>X{K6tIX0BIq6&6HdEJu|owW1sO4`;uw@9dAt5RBkX_H{I%5bnrK-nJ6 z;mm8e-U?+nt#FvY@&jcrkz#@itj?!RmIzyi$62@@H4qjA*H44oPzpAYhVf_LIkaHv z!l)fIn{CZS{N5}?rI@SovOzLcXOIque7g?8iu?oL6QRWQ5 zbGKU9F5RfvtA`p08;auL5Jg6w?F-=T3E9WWk_fGeNkh!Ha3hVaE>^jtKQ+G6JG+u# z`6(CwEnQ{XG0g}$_yvI7Oc=nME<)>Ws$zfVU8w*nVI9L$dU%U}*OqW2axe7Z=wL74 zyBb7&5^pic_tc^?{Z}pxI{gExfNh)#pwG}veAxZ z6j8O%(mBV#fr8VG--BQ7k$V9(V$7Ts5A3uQ{58Zw)JOZSKs^rG&i7i*R<}o5v~!Ad zfAtj{?jviMI?ABJdtU+!MhIFhbPwTgmftQVbApPqxK~X>N8-~@H)VqaXhpEF zlmV1CJ0g-){pNgMn)FnvlQ8o0Kh_U4oay))&n>HUN=E^alk8Lcet}}d@YM7pE4*d7^23kTM*M7LI3fcUQcHoPrI0p!+XTh=!k?k~_i<@V^u)KLk(wf@xacvi3 zsMU=%ka}ItV9#uAV^axr*8+ny&oA^qvIt?}k(<*1J1G3(uZ|Po^g&dHK$q86LYA_~ju8aVc6JJ&NdO5n%;%LZ~BzS|Zgz>CL^y2YCdZg4c3J3aN${P9ZlvgY6JCV=dc6ep|@c*j)ji zRMc+OY!{Jx8j_lN;ES05D3e~5k<2=9ww5tizW8K0q1Ei-46n|Mcpp+y{h|N zH|t_fod2@>=STfT`~}xyuSq**idEXyb>kv9wAfYSm|r)Ch4o)!sn178M`x$h5Mgu> zk}L<)=06pz#PJ%e&XB*GEpghawWb@0;g&7l+^sr04+<=y#!WrYIN`3O-&h+xQTLYS zp)LGjSIpa%P}51bU(((_KCPXj`U^pZ5ELv;;k$uU^$YVHuoIwOVi9cu1A~Kq41zNu zs-XKwJMr>&%%W=d)k?RO@v12-bs22ClkIXSwe}Hzx(20bv<}ug*T6Ww^Ls4wzL;4l z*}ZX0c6^XbuA$QrMW5yf53e1;imh4M{19BFzz@nmYq*Pt<98`H;0?=$3`e!Eco zX!e5l-qalT-aujIK?l*qCGJzzphFBwOu@b(g8IN@M?Yq!4}#U{avLiZ$6n)$+Pj(% z`>73wdRXn}aoP1WqIL)3&c~25)$mV;s2LIy7PQ8Z$i;Q?J#iaG5?$- zsHv^>aiM6t6?HY-t!CFb)ZCfgC^@zIcmKiM^y`9x_ruqP zZFgr*-;t;_e8M?&h$KH;Yu(ZMe!W?^p`fZtUel9YQU%gb_^!~McqN7kYsov z8uRAxlSmuCdojFnOKah3o$$lQnBC`+6}uOzx4B+;CP+SMU!83~c7u9T-_?Ql_}hXU zEO!^m<|l;1vd@kkTunjIKk`Eio(82EzoVo*y}dUNIhS`R>CyHXwg(7;gQ34+6kGK0e_X_h46ROug<9 z@Kyc$Nt9&W_}~{?9wcJ!bo(f3Ap~<#KuE)%>pgKt`+{M{+@691Rh-9l`oV-t`XNt8 ziQypCSVsMe?#yh8ndN8As4FU?6}C1x#>MR>$RABZ2X%)lv+Bpktq^7D$ADKKFnMB? z7NqpI<$Ok55BAD`7_Qf5r9bc+%xXj9;|*>r#r|&E))(_~i=gls9S)Ss>SXN*QG=nQ7I}y6q1V2Z_Y$P_ zU-Nm~$Q&mO&d&FUNX^Zy_+oJE<%i483oS3=_- ztqRasYSG@_UgOUn*l6{uCxGn>e$@x(5K8dfr=6!vP7M~OxkN^fl$jq36e(Z9JP%qP zWv!9&I*FD?Dl+*$1gsC+89pE3~<#^G5w63ghoM*PgjociD#OL;5^mG{bMETn% zTM2`BJmSF&MB>l9uzn)E@Bxz4XLjv_^)`r$CQ;`qgc%?Ie{jg`M*kiI3=*>IQP literal 0 HcmV?d00001 diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..5aab97e --- /dev/null +++ b/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file