diff --git a/Gallery.Share/App.cs b/Gallery.Share/App.cs index f31de39..106229b 100644 --- a/Gallery.Share/App.cs +++ b/Gallery.Share/App.cs @@ -4,6 +4,8 @@ using Xamarin.Essentials; using Gallery.Resources; using Gallery.Util; using Gallery.Resources.Theme; +using System.Collections.Generic; +using Gallery.Util.Interface; namespace Gallery { @@ -12,8 +14,19 @@ namespace Gallery public static AppTheme CurrentTheme { get; private set; } public static PlatformCulture CurrentCulture { get; private set; } + public static List GallerySources { get; } = new() + { + new Yandere.GallerySource(), // https://yande.re + new Danbooru.GallerySource(), // https://danbooru.donmai.us + new Gelbooru.GallerySource() // https://gelbooru.com + }; + public App() { + Preferences.Set(Config.IsProxiedKey, true); + Preferences.Set(Config.ProxyHostKey, "192.168.25.9"); + Preferences.Set(Config.ProxyPortKey, 1081); + DependencyService.Register(); } @@ -25,7 +38,33 @@ namespace Gallery private void InitPreference() { + Config.Proxy = null; + var isProxied = Preferences.Get(Config.IsProxiedKey, false); + if (isProxied) + { + var host = Preferences.Get(Config.ProxyHostKey, null); + var port = Preferences.Get(Config.ProxyPortKey, 0); + if (!string.IsNullOrEmpty(host) && port > 0) + { + try + { + if (host.IndexOf(':') >= 0) + { + host = $"[{host}]"; + } + var uri = new System.Uri($"http://{host}:{port}"); + Config.Proxy = new System.Net.WebProxy(uri, true); +#if DEBUG + Log.Print($"load proxy: {uri}"); +#endif + } + catch (System.Exception ex) + { + Log.Error("preferences.init", $"failed to parse proxy: {host}:{port}, error: {ex.Message}"); + } + } + } } private void InitLanguage() diff --git a/Gallery.Share/Views/AboutPage.xaml.cs b/Gallery.Share/Views/AboutPage.xaml.cs index 9f73b18..2706b93 100644 --- a/Gallery.Share/Views/AboutPage.xaml.cs +++ b/Gallery.Share/Views/AboutPage.xaml.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Gallery.Util; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -11,5 +12,20 @@ namespace Gallery.Views { InitializeComponent(); } + + protected override async void OnAppearing() + { + base.OnAppearing(); + + var result = await App.GallerySources[0].GetRecentItemsAsync(1); + if (result != null) + { + for (var i = 0; i < result.Length; i++) + { + var item = result[i]; + Log.Print($"id: {item.Id}, url: {item.RawUrl}"); + } + } + } } } diff --git a/Gallery.Util/Extensions.cs b/Gallery.Util/Extensions.cs index 75dff13..498297d 100644 --- a/Gallery.Util/Extensions.cs +++ b/Gallery.Util/Extensions.cs @@ -95,5 +95,17 @@ namespace Gallery.Util } return false; } + + public static DateTime ToLocalTime(this long time) + { + //return new DateTime(1970, 1, 1, 0, 0, 0).AddMilliseconds(time).ToLocalTime(); + return new DateTime(621355968000000000L + time * 10000).ToLocalTime(); + } + + public static long ToTimestamp(this DateTime datetime) + { + var ticks = datetime.Ticks; + return (ticks - 621355968000000000L) / 10000; + } } } diff --git a/Gallery.Util/Gallery.Util.csproj b/Gallery.Util/Gallery.Util.csproj index 2b5fedd..228032b 100644 --- a/Gallery.Util/Gallery.Util.csproj +++ b/Gallery.Util/Gallery.Util.csproj @@ -17,10 +17,12 @@ + + diff --git a/Gallery.Util/Interface/IGallerySource.cs b/Gallery.Util/Interface/IGallerySource.cs index a985cc0..8b5524b 100644 --- a/Gallery.Util/Interface/IGallerySource.cs +++ b/Gallery.Util/Interface/IGallerySource.cs @@ -1,11 +1,16 @@ -using Gallery.Util.Model; +using System.Threading.Tasks; +using Gallery.Util.Model; namespace Gallery.Util.Interface { public interface IGallerySource { + string Name { get; } + + string HomePage { get; } + void SetCookie(); - GalleryItem[] GetRecentItems(int page); + Task GetRecentItemsAsync(int page); } } diff --git a/Gallery.Util/Model/GalleryItem.cs b/Gallery.Util/Model/GalleryItem.cs index e8c9aaf..b4f5f50 100644 --- a/Gallery.Util/Model/GalleryItem.cs +++ b/Gallery.Util/Model/GalleryItem.cs @@ -75,5 +75,18 @@ namespace Gallery.Util.Model } } } + + internal GalleryItem() { } + + public GalleryItem(long id) + { + Id = id; + } + + public override string ToString() + { + var source = string.IsNullOrEmpty(Source) ? RawUrl : Source; + return $"{Id}, {source}"; + } } } diff --git a/Gallery.Util/NetHelper.cs b/Gallery.Util/NetHelper.cs new file mode 100644 index 0000000..48e7f65 --- /dev/null +++ b/Gallery.Util/NetHelper.cs @@ -0,0 +1,149 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Xamarin.Essentials; + +namespace Gallery.Util +{ + public static class NetHelper + { + public static bool NetworkAvailable + { + get + { + try + { + return Connectivity.NetworkAccess == NetworkAccess.Internet + || Connectivity.NetworkAccess == NetworkAccess.ConstrainedInternet; + } + catch + { + return false; + } + } + } + + public static async Task<(T result, string error)> RequestObject(string url, + string referer = null, + HttpContent post = null, + Action headerHandler = null, + Func contentHandler = null, + Func @return = null) + { + var response = await Request(url, headers => + { + if (referer != null) + { + headers.Referrer = new Uri(referer); + } + headers.Add("User-Agent", Config.UserAgent); + headerHandler?.Invoke(headers); + }, post); + if (response == null) + { + return (default, "response is null"); + } + if (!response.IsSuccessStatusCode) + { + Log.Print($"http failed with code: {(int)response.StatusCode} - {response.StatusCode}"); + return (default, response.StatusCode.ToString()); + } + string content; + using (response) + { + try + { + content = await response.Content.ReadAsStringAsync(); + if (contentHandler != null) + { + content = contentHandler(content); + } + if (@return != null) + { + return (@return(content), null); + } + } + catch (Exception ex) + { + Log.Error("stream.load", $"failed to read stream, error: {ex.Message}"); + return (default, ex.Message); + } + } + + if (content == null) + { + content = string.Empty; + } + try + { + var result = JsonSerializer.Deserialize(content); + return (result, null); + } + catch (Exception ex) + { + var memo = content.Length < 20 ? content : content[0..20] + "..."; + Log.Error("content.deserialize", $"failed to parse JSON object, content: {memo}, error: {ex.Message}"); + return (default, content); + } + } + + private static async Task Request(string url, Action headerHandler, HttpContent post = null) + { +#if DEBUG + var method = post == null ? "GET" : "POST"; + Log.Print($"{method}: {url}"); +#endif + var uri = new Uri(url); + var proxy = Config.Proxy; + var handler = new HttpClientHandler + { + AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate, + UseCookies = false + }; + if (proxy != null) + { + handler.Proxy = proxy; + handler.UseProxy = true; + } + var client = new HttpClient(handler, true) + { + BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}:{uri.Port}"), + Timeout = Config.Timeout + }; + return await TryCount(() => + { + using var request = new HttpRequestMessage(post == null ? HttpMethod.Get : HttpMethod.Post, uri.PathAndQuery); + var headers = request.Headers; + headerHandler?.Invoke(headers); + headers.Add("Accept-Language", Config.AcceptLanguage); + if (post != null) + { + request.Content = post; + } + return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + }); + } + + private static T TryCount(Func func, int tryCount = 2) + { + int tries = 0; + while (tries < tryCount) + { + try + { + return func(); + } + catch (Exception ex) + { + tries++; + Thread.Sleep(1000); + Log.Error("try.do", $"tries: {tries}, error: {ex.Message}"); + } + } + return default; + } + } +} diff --git a/Gallery.Util/Store.cs b/Gallery.Util/Store.cs new file mode 100644 index 0000000..6aa8dd9 --- /dev/null +++ b/Gallery.Util/Store.cs @@ -0,0 +1,26 @@ +using System; +using System.Net; +using Xamarin.Essentials; + +namespace Gallery.Util +{ + public static class Store + { + public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); + public static readonly string CacheFolder = FileSystem.CacheDirectory; + } + + public static class Config + { + public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30); + + public const string IsProxiedKey = "is_proxied"; + public const string ProxyHostKey = "proxy_host"; + public const string ProxyPortKey = "proxy_port"; + + public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"; + public const string AcceptLanguage = "zh-cn"; + + public static WebProxy Proxy; + } +} diff --git a/Gallery.iOS/Gallery.iOS.csproj b/Gallery.iOS/Gallery.iOS.csproj index 4beeb2f..9d62482 100644 --- a/Gallery.iOS/Gallery.iOS.csproj +++ b/Gallery.iOS/Gallery.iOS.csproj @@ -155,6 +155,18 @@ {222C22EC-3A47-4CF5-B9FB-CA28DE9F4BC8} Gallery.Util + + {F7ECCC03-28AC-4326-B0D1-F24C08808B9F} + Gallery.Yandere + + + {83760017-F2A6-4450-A4F8-8E143E800C2F} + Gallery.Gelbooru + + + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34} + Gallery.Danbooru + diff --git a/Gallery.iOS/obj/iPhoneSimulator/Debug/Gallery.iOS.csproj.AssemblyReference.cache b/Gallery.iOS/obj/iPhoneSimulator/Debug/Gallery.iOS.csproj.AssemblyReference.cache deleted file mode 100644 index f5e894a..0000000 Binary files a/Gallery.iOS/obj/iPhoneSimulator/Debug/Gallery.iOS.csproj.AssemblyReference.cache and /dev/null differ diff --git a/Gallery.sln b/Gallery.sln index 028326f..d2e2783 100644 --- a/Gallery.sln +++ b/Gallery.sln @@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GallerySources", "GallerySo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Yandere", "GallerySources\Gallery.Yandere\Gallery.Yandere.csproj", "{F7ECCC03-28AC-4326-B0D1-F24C08808B9F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Danbooru", "GallerySources\Gallery.Danbooru\Gallery.Danbooru.csproj", "{F9187AE4-BC64-4906-9CAF-89BE43CD4A34}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Gelbooru", "GallerySources\Gallery.Gelbooru\Gallery.Gelbooru.csproj", "{83760017-F2A6-4450-A4F8-8E143E800C2F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|iPhoneSimulator = Debug|iPhoneSimulator @@ -59,6 +63,30 @@ Global {F7ECCC03-28AC-4326-B0D1-F24C08808B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7ECCC03-28AC-4326-B0D1-F24C08808B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7ECCC03-28AC-4326-B0D1-F24C08808B9F}.Release|Any CPU.Build.0 = Release|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Debug|iPhone.Build.0 = Debug|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Release|iPhone.ActiveCfg = Release|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Release|iPhone.Build.0 = Release|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34}.Release|Any CPU.Build.0 = Release|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Debug|iPhone.Build.0 = Debug|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Release|iPhone.ActiveCfg = Release|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Release|iPhone.Build.0 = Release|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83760017-F2A6-4450-A4F8-8E143E800C2F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -68,5 +96,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {F7ECCC03-28AC-4326-B0D1-F24C08808B9F} = {F37B4FEC-D2B1-4289-BA6D-A154F783572A} + {F9187AE4-BC64-4906-9CAF-89BE43CD4A34} = {F37B4FEC-D2B1-4289-BA6D-A154F783572A} + {83760017-F2A6-4450-A4F8-8E143E800C2F} = {F37B4FEC-D2B1-4289-BA6D-A154F783572A} EndGlobalSection EndGlobal diff --git a/GallerySources/Gallery.Danbooru/Gallery.Danbooru.csproj b/GallerySources/Gallery.Danbooru/Gallery.Danbooru.csproj new file mode 100644 index 0000000..5fbb153 --- /dev/null +++ b/GallerySources/Gallery.Danbooru/Gallery.Danbooru.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.1 + + + + 9.0 + + + 9.0 + + + + + diff --git a/GallerySources/Gallery.Danbooru/GallerySource.cs b/GallerySources/Gallery.Danbooru/GallerySource.cs new file mode 100644 index 0000000..354548a --- /dev/null +++ b/GallerySources/Gallery.Danbooru/GallerySource.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Gallery.Util.Interface; +using Gallery.Util.Model; + +namespace Gallery.Danbooru +{ + public class GallerySource : IGallerySource + { + public string Name => "Danbooru"; + + public string HomePage => "https://danbooru.donmai.us"; + + public Task GetRecentItemsAsync(int page) + { + throw new NotImplementedException(); + } + + public void SetCookie() + { + throw new NotImplementedException(); + } + } +} diff --git a/GallerySources/Gallery.Gelbooru/Gallery.Gelbooru.csproj b/GallerySources/Gallery.Gelbooru/Gallery.Gelbooru.csproj new file mode 100644 index 0000000..5fbb153 --- /dev/null +++ b/GallerySources/Gallery.Gelbooru/Gallery.Gelbooru.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.1 + + + + 9.0 + + + 9.0 + + + + + diff --git a/GallerySources/Gallery.Gelbooru/GallerySource.cs b/GallerySources/Gallery.Gelbooru/GallerySource.cs new file mode 100644 index 0000000..2859100 --- /dev/null +++ b/GallerySources/Gallery.Gelbooru/GallerySource.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Gallery.Util.Interface; +using Gallery.Util.Model; + +namespace Gallery.Gelbooru +{ + public class GallerySource : IGallerySource + { + public string Name => "Gelbooru"; + + public string HomePage => "https://gelbooru.com"; + + public Task GetRecentItemsAsync(int page) + { + throw new NotImplementedException(); + } + + public void SetCookie() + { + throw new NotImplementedException(); + } + } +} diff --git a/GallerySources/Gallery.Yandere/Gallery.Yandere.csproj b/GallerySources/Gallery.Yandere/Gallery.Yandere.csproj index da48539..5fbb153 100644 --- a/GallerySources/Gallery.Yandere/Gallery.Yandere.csproj +++ b/GallerySources/Gallery.Yandere/Gallery.Yandere.csproj @@ -4,6 +4,12 @@ netstandard2.1 + + 9.0 + + + 9.0 + diff --git a/GallerySources/Gallery.Yandere/GallerySource.cs b/GallerySources/Gallery.Yandere/GallerySource.cs new file mode 100644 index 0000000..dd62ca6 --- /dev/null +++ b/GallerySources/Gallery.Yandere/GallerySource.cs @@ -0,0 +1,65 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Gallery.Util; +using Gallery.Util.Interface; +using Gallery.Util.Model; + +namespace Gallery.Yandere +{ + public class GallerySource : IGallerySource + { + public string Name => "Yande.re"; + public string HomePage => "https://yande.re"; + + public async Task GetRecentItemsAsync(int page) + { + var url = $"https://yande.re/post?page={page}"; + var (result, error) = await NetHelper.RequestObject(url, contentHandler: ContentHandler); + + if (result == null || !string.IsNullOrEmpty(error)) + { + Log.Error("yandere.content.load", $"failed to load content array, error: {error}"); + return null; + } + + var items = new GalleryItem[result.Length]; + for (var i = 0; i < items.Length; i++) + { + var y = result[i]; + var item = new GalleryItem(y.id) + { + Tags = y.tags?.Split(' '), + CreatedTime = y.created_at.ToLocalTime(), + UpdatedTime = y.updated_at.ToLocalTime(), + UserId = y.creator_id.ToString(), + UserName = y.author, + Source = y.source, + PreviewUrl = y.preview_url, + RawUrl = y.file_url, + Width = y.width, + Height = y.height + }; + items[i] = item; + } + return items; + } + + private string ContentHandler(string content) + { + var regex = new Regex(@"Post\.register\((\{.+\})\)\s*$", RegexOptions.Multiline); + var matches = regex.Matches(content); + var array = new string[matches.Count]; + for (var i = 0; i < array.Length; i++) + { + array[i] = matches[i].Groups[1].Value; + } + return $"[{string.Join(',', array)}]"; + } + + public void SetCookie() + { + throw new NotImplementedException(); + } + } +} diff --git a/GallerySources/Gallery.Yandere/YandereItem.cs b/GallerySources/Gallery.Yandere/YandereItem.cs new file mode 100644 index 0000000..8a292fa --- /dev/null +++ b/GallerySources/Gallery.Yandere/YandereItem.cs @@ -0,0 +1,29 @@ +using System; + +namespace Gallery.Yandere +{ + public class YandereItem + { +#pragma warning disable IDE1006 // Naming Styles + + public long id { get; set; } + public string tags { get; set; } + public long created_at { get; set; } + public long updated_at { get; set; } + public long creator_id { get; set; } + public string author { get; set; } + public string source { get; set; } + public int score { get; set; } + public int file_size { get; set; } + public string file_ext { get; set; } + public string file_url { get; set; } + public string preview_url { get; set; } + public int actual_preview_width { get; set; } + public int actual_preview_height { get; set; } + public string rating { get; set; } + public int width { get; set; } + public int height { get; set; } + +#pragma warning restore IDE1006 // Naming Styles + } +}