diff --git a/Classes/PCGWCache.cs b/Classes/PCGWCache.cs new file mode 100644 index 0000000..45f72e5 --- /dev/null +++ b/Classes/PCGWCache.cs @@ -0,0 +1,105 @@ +using Bluscream; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace PCGWMetaData.Classes +{ + public class Cache + { + internal List games; + internal DirectoryInfo cacheDir; + internal PCGWMetaDataPlugin plugin; + + public Cache(PCGWMetaDataPlugin plugin) + { + this.plugin = plugin; + cacheDir = new DirectoryInfo(plugin.GetPluginUserDataPath()).Combine("cache"); + cacheDir.Create(); + games = new List(); + refresh(); + } + + public List refresh() + { + games.Clear(); + foreach (var file in cacheDir.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly)) + { + games.Add(new Game(file.FileNameWithoutExtension(), this)); + } + // games = .Select(f => f.FileNameWithoutExtension()).ToList(); + return games; + } + + private Game addGame(string name) + { + var game = new Game(name, this); + var json = plugin.webClient.DownloadString(string.Format(PCGWMetaDataPlugin.url_base, game.EncodedName())); + game.File().WriteAllText(json); + games.Add(game); + return game; + } + + private Game _getGame(string name) + { + return games.FirstOrDefault(g => g.Name == name.ToLowerInvariant()); + } + + public Game getGame(string name) + { + var game = _getGame(name); + if (game is null) addGame(name); + return game; + } + + public void Purge() + { + foreach (var file in cacheDir.EnumerateFiles()) + file.Delete(); + refresh(); + } + + public void PurgeOutdated() + { + foreach (var game in games) + { + if (game.isOutdated()) + game.File().Delete(); + } + refresh(); + } + } + + public class Game + { + public string Name; + private Cache _cache; + + public Game(string Name, Cache _cache) + { + this.Name = Name.ToLowerInvariant(); this._cache = _cache; + } + + public FileInfo File() + { + return _cache.cacheDir.CombineFile(string.Format("{0}.json", this.EncodedName())); + } + + public ApiResult Data() + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(File().ReadAllText()); + } + + public string EncodedName() => HttpUtility.UrlEncode(this.Name); + // public string DecodedName() => HttpUtility.HtmlDecode(this.Name); + + public bool isOutdated() + { + return DateTime.Now - System.IO.File.GetLastWriteTime(File().FullName) > TimeSpan.FromDays(30); // option + } + } +} \ No newline at end of file diff --git a/PCGWMetaData.cs b/PCGWMetaData.cs index f170581..19aadff 100644 --- a/PCGWMetaData.cs +++ b/PCGWMetaData.cs @@ -6,6 +6,9 @@ using System.Linq; using System.Net; using System.Web; +using Bluscream; +using PCGWMetaData.Classes; +using System.Windows.Controls; namespace PCGWMetaData { @@ -16,30 +19,32 @@ public class PCGWMetaDataPlugin : MetadataPlugin public override Guid Id { get; } = Guid.Parse("111001DB-DBD1-46C6-B5D0-B1BA559D10E4"); public override List SupportedFields { get; } = new List { MetadataField.Tags }; public IPlayniteAPI api; + internal Cache cache; + internal WebClient webClient = new WebClient(); + internal const string url_base = "https://www.pcgamingwiki.com/w/api.php?action=browsebysubject&format=json&subject={0}"; public PCGWMetaDataPlugin(IPlayniteAPI playniteAPI) : base(playniteAPI) { - api = playniteAPI; + this.api = playniteAPI; + this.cache = new Cache(this); } public override OnDemandMetadataProvider GetMetadataProvider(MetadataRequestOptions options) { - return new PCGWMetadataProvider(options, api); + return new PCGWMetadataProvider(options, this); } } public class PCGWMetadataProvider : OnDemandMetadataProvider { - private WebClient webClient = new WebClient(); - private const string url_base = "https://www.pcgamingwiki.com/w/api.php?action=browsebysubject&format=json&subject="; private readonly MetadataRequestOptions options; private List availableFields; - public IPlayniteAPI api; + private PCGWMetaDataPlugin plugin; - public PCGWMetadataProvider(MetadataRequestOptions options, IPlayniteAPI api) + public PCGWMetadataProvider(MetadataRequestOptions options, PCGWMetaDataPlugin plugin) { this.options = options; - this.api = api; + this.plugin = plugin; } public override List AvailableFields @@ -64,7 +69,7 @@ public override List GetTags() { // api.Dialogs.ShowMessage("Requested metadata for game " + options.GameData.Name); var tags = new List(); - var l_ = api.Database.Games.FirstOrDefault(g => g.Id == options.GameData.Id); + var l_ = plugin.api.Database.Games.FirstOrDefault(g => g.Id == options.GameData.Id); if (l_ != null) { var l__ = l_.Tags; @@ -73,15 +78,18 @@ public override List GetTags() tags = l__.Select(t => t.Name).ToList(); } } - var url = url_base + HttpUtility.HtmlEncode(options.GameData.Name); - var json = webClient.DownloadString(url); - var result = JsonConvert.DeserializeObject(json); - // api.Dialogs.ShowMessage(JsonConvert.SerializeObject(result)); - if (result is null || result.Query is null || result.Query.Data is null) return null; - var engine = result.Query.Data.Where(i => i.Property == "Uses_engine").FirstOrDefault()?.Dataitem.FirstOrDefault().Item; - if (engine != null) - tags.Add("engine:" + engine.Replace("#404#", "")); - // api.Dialogs.ShowMessage(JsonConvert.SerializeObject(tags)); + var _result = plugin.cache.getGame(options.GameData.Name); + if (_result != null) + { + var result = _result.Data(); + // api.Dialogs.ShowMessage(JsonConvert.SerializeObject(result)); + if (result is null || result.Query is null || result.Query.Data is null) return null; + var engine = result.Query.Data.Where(i => i.Property == "Uses_engine").FirstOrDefault()?.Dataitem.FirstOrDefault().Item; + if (engine != null) + tags.Add("engine:" + engine.Replace("#404#", "")); + // api.Dialogs.ShowMessage(JsonConvert.SerializeObject(tags)); + } + tags.ForEach(t => t.ToLowerInvariant().Trim()); // Todo: option return tags; } } diff --git a/PCGWMetaData.csproj b/PCGWMetaData.csproj index 550dec9..50df041 100644 --- a/PCGWMetaData.csproj +++ b/PCGWMetaData.csproj @@ -50,8 +50,11 @@ + + + diff --git a/Utils/Extensions.cs b/Utils/Extensions.cs new file mode 100644 index 0000000..ec2d993 --- /dev/null +++ b/Utils/Extensions.cs @@ -0,0 +1,455 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null' + +namespace Bluscream +{ + internal static class Extensions + { + #region Reflection + + public static Dictionary ToDictionary(this object instanceToConvert) + { + return instanceToConvert.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .ToDictionary( + propertyInfo => propertyInfo.Name, + propertyInfo => Extensions.ConvertPropertyToDictionary(propertyInfo, instanceToConvert)); + } + + private static object ConvertPropertyToDictionary(PropertyInfo propertyInfo, object owner) + { + Type propertyType = propertyInfo.PropertyType; + object propertyValue = propertyInfo.GetValue(owner); + + // If property is a collection don't traverse collection properties but the items instead + if (!propertyType.Equals(typeof(string)) && (typeof(ICollection<>).Name.Equals(propertyValue.GetType().BaseType.Name) || typeof(Collection<>).Name.Equals(propertyValue.GetType().BaseType.Name))) + { + var collectionItems = new List>(); + var count = (int)propertyType.GetProperty("Count").GetValue(propertyValue); + PropertyInfo indexerProperty = propertyType.GetProperty("Item"); + + // Convert collection items to dictionary + for (var index = 0; index < count; index++) + { + object item = indexerProperty.GetValue(propertyValue, new object[] { index }); + PropertyInfo[] itemProperties = item.GetType().GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + + if (itemProperties.Any()) + { + Dictionary dictionary = itemProperties + .ToDictionary( + subtypePropertyInfo => subtypePropertyInfo.Name, + subtypePropertyInfo => Extensions.ConvertPropertyToDictionary(subtypePropertyInfo, item)); + collectionItems.Add(dictionary); + } + } + + return collectionItems; + } + + // If property is a string stop traversal (ignore that string is a char[]) + if (propertyType.IsPrimitive || propertyType.Equals(typeof(string))) + { + return propertyValue; + } + + PropertyInfo[] properties = propertyType.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + if (properties.Any()) + { + return properties.ToDictionary( + subtypePropertyInfo => subtypePropertyInfo.Name, + subtypePropertyInfo => (object)Extensions.ConvertPropertyToDictionary(subtypePropertyInfo, propertyValue)); + } + + return propertyValue; + } + + #endregion Reflection + + #region DateTime + + public static bool ExpiredSince(this DateTime dateTime, int minutes) + { + return (dateTime - DateTime.Now).TotalMinutes < minutes; + } + + public static TimeSpan StripMilliseconds(this TimeSpan time) + { + return new TimeSpan(time.Days, time.Hours, time.Minutes, time.Seconds); + } + + #endregion DateTime + + #region DirectoryInfo + + public static string GetRelativePathFrom(this FileSystemInfo to, FileSystemInfo from) + { + return from.GetRelativePathTo(to); + } + + public static string GetRelativePathTo(this FileSystemInfo from, FileSystemInfo to) + { + string getPath(FileSystemInfo fsi) + { + return !(fsi is DirectoryInfo d) ? fsi.FullName : d.FullName.TrimEnd('\\') + "\\"; + } + + var fromPath = getPath(from); + var toPath = getPath(to); + + var fromUri = new Uri(fromPath); + var toUri = new Uri(toPath); + + var relativeUri = fromUri.MakeRelativeUri(toUri); + var relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + + return relativePath.Replace('/', Path.DirectorySeparatorChar); + } + + public static DirectoryInfo Combine(this DirectoryInfo dir, params string[] paths) + { + var final = dir.FullName; + foreach (var path in paths) + { + final = Path.Combine(final, path); + } + return new DirectoryInfo(final); + } + + #endregion DirectoryInfo + + #region FileInfo + + public static FileInfo CombineFile(this DirectoryInfo dir, params string[] paths) + { + var final = dir.FullName; + foreach (var path in paths) + { + final = Path.Combine(final, path); + } + return new FileInfo(final); + } + + public static FileInfo Combine(this FileInfo file, params string[] paths) + { + var final = file.DirectoryName; + foreach (var path in paths) + { + final = Path.Combine(final, path); + } + return new FileInfo(final); + } + + public static string FileNameWithoutExtension(this FileInfo file) + { + return Path.GetFileNameWithoutExtension(file.Name); + } + /*public static string Extension(this FileInfo file) { + return Path.GetExtension(file.Name); + }*/ + + public static void AppendLine(this FileInfo file, string line) + { + try + { + if (!file.Exists) file.Create(); + File.AppendAllLines(file.FullName, new string[] { line }); + } + catch { } + } + + public static void WriteAllText(this FileInfo file, string text) => File.WriteAllText(file.FullName, text); + + public static string ReadAllText(this FileInfo file) => File.ReadAllText(file.FullName); + + public static List ReadAllLines(this FileInfo file) => File.ReadAllLines(file.FullName).ToList(); + + #endregion FileInfo + + #region String + + public static string Repeat(this char input, int count) + { + if (input == null) + { + return string.Empty; + } + return new String(input, count); + } + + public static string Repeat(this string input, int count) + { + if (!string.IsNullOrEmpty(input)) + { + StringBuilder builder = new StringBuilder(input.Length * count); + + for (int i = 0; i < count; i++) builder.Append(input); + + return builder.ToString(); + } + + return string.Empty; + } + + public static bool Contains(this string source, string toCheck, StringComparison comp) + { + return source?.IndexOf(toCheck, comp) >= 0; + } + + public static IEnumerable SplitToLines(this string input) + { + if (input == null) + { + yield break; + } + + using (System.IO.StringReader reader = new System.IO.StringReader(input)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + } + + public static string ToTitleCase(this string source, string langCode = "en-US") + { + return new CultureInfo(langCode, false).TextInfo.ToTitleCase(source); + } + + public static bool IsNullOrEmpty(this string source) + { + return string.IsNullOrEmpty(source); + } + + public static string[] Split(this string source, string split, int count = -1, StringSplitOptions options = StringSplitOptions.None) + { + if (count != -1) return source.Split(new string[] { split }, count, options); + return source.Split(new string[] { split }, options); + } + + public static string Remove(this string Source, string Replace) + { + return Source.Replace(Replace, string.Empty); + } + + public static string ReplaceLastOccurrence(this string Source, string Find, string Replace) + { + int place = Source.LastIndexOf(Find); + if (place == -1) + return Source; + string result = Source.Remove(place, Find.Length).Insert(place, Replace); + return result; + } + + public static string EscapeLineBreaks(this string source) + { + return Regex.Replace(source, @"\r\n?|\n", @"\$&"); + } + + public static string Ext(this string text, string extension) + { + return text + "." + extension; + } + + public static string Quote(this string text) + { + return SurroundWith(text, "\""); + } + + public static string Enclose(this string text) + { + return SurroundWith(text, "(", ")"); + } + + public static string Brackets(this string text) + { + return SurroundWith(text, "[", "]"); + } + + public static string SurroundWith(this string text, string surrounds) + { + return surrounds + text + surrounds; + } + + public static string SurroundWith(this string text, string starts, string ends) + { + return starts + text + ends; + } + + #endregion String + + #region Dict + + public static void AddSafe(this IDictionary dictionary, string key, string value) + { + if (!dictionary.ContainsKey(key)) + dictionary.Add(key, value); + } + + #endregion Dict + + #region List + + public static string ToQueryString(this NameValueCollection nvc) + { + if (nvc == null) return string.Empty; + + StringBuilder sb = new StringBuilder(); + + foreach (string key in nvc.Keys) + { + if (string.IsNullOrWhiteSpace(key)) continue; + + string[] values = nvc.GetValues(key); + if (values == null) continue; + + foreach (string value in values) + { + sb.Append(sb.Length == 0 ? "?" : "&"); + sb.AppendFormat("{0}={1}", key, value); + } + } + + return sb.ToString(); + } + + public static bool GetBool(this NameValueCollection collection, string key, bool defaultValue = false) + { + if (!collection.AllKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) return false; + var trueValues = new string[] { true.ToString(), "yes", "1" }; + if (trueValues.Contains(collection[key], StringComparer.OrdinalIgnoreCase)) return true; + var falseValues = new string[] { false.ToString(), "no", "0" }; + if (falseValues.Contains(collection[key], StringComparer.OrdinalIgnoreCase)) return true; + return defaultValue; + } + + public static string GetString(this NameValueCollection collection, string key) + { + if (!collection.AllKeys.Contains(key)) return collection[key]; + return null; + } + + public static T PopFirst(this IEnumerable list) => list.ToList().PopAt(0); + + public static T PopLast(this IEnumerable list) => list.ToList().PopAt(list.Count() - 1); + + public static T PopAt(this List list, int index) + { + T r = list.ElementAt(index); + list.RemoveAt(index); + return r; + } + + #endregion List + + #region Uri + + private static readonly Regex QueryRegex = new Regex(@"[?&](\w[\w.]*)=([^?&]+)"); + + public static IReadOnlyDictionary ParseQueryString(this Uri uri) + { + var match = QueryRegex.Match(uri.PathAndQuery); + var paramaters = new Dictionary(); + while (match.Success) + { + paramaters.Add(match.Groups[1].Value, match.Groups[2].Value); + match = match.NextMatch(); + } + return paramaters; + } + + #endregion Uri + + #region Enum + + public static string GetDescription(this Enum value) + { + Type type = value.GetType(); + string name = Enum.GetName(type, value); + if (name != null) + { + FieldInfo field = type.GetField(name); + if (field != null) + { + DescriptionAttribute attr = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute; + if (attr != null) + { + return attr.Description; + } + } + } + return null; + } + + public static T GetValueFromDescription(string description, bool returnDefault = false) + { + var type = typeof(T); + if (!type.IsEnum) throw new InvalidOperationException(); + foreach (var field in type.GetFields()) + { + var attribute = Attribute.GetCustomAttribute(field, + typeof(DescriptionAttribute)) as DescriptionAttribute; + if (attribute != null) + { + if (attribute.Description == description) + return (T)field.GetValue(null); + } + else + { + if (field.Name == description) + return (T)field.GetValue(null); + } + } + if (returnDefault) return default(T); + else throw new ArgumentException("Not found.", "description"); + } + + #endregion Enum + + #region Task + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { + using (var timeoutCancellationTokenSource = new CancellationTokenSource()) + { + var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)); + if (completedTask == task) + { + timeoutCancellationTokenSource.Cancel(); + return await task; // Very important in order to propagate exceptions + } + else + { + return default(TResult); + } + } + } + + #endregion Task + + #region bool + + public static string ToYesNo(this bool input) => input ? "Yes" : "No"; + + public static string ToEnabledDisabled(this bool input) => input ? "Enabled" : "Disabled"; + + public static string ToOnOff(this bool input) => input ? "On" : "Off"; + + #endregion bool + } +} \ No newline at end of file diff --git a/Utils/Utils.cs b/Utils/Utils.cs new file mode 100644 index 0000000..0651d3a --- /dev/null +++ b/Utils/Utils.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bluscream +{ + internal class Utils + { + public class Console + { + public static bool Confirm(string title) + { + ConsoleKey response; + do + { + System.Console.Write($"{ title } [y/n] "); + response = System.Console.ReadKey(false).Key; + if (response != ConsoleKey.Enter) + { + System.Console.WriteLine(); + } + } while (response != ConsoleKey.Y && response != ConsoleKey.N); + + return (response == ConsoleKey.Y); + } + } + } +} \ No newline at end of file