using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; using Gallery.Illust; using Xamarin.Essentials; using Xamarin.Forms; namespace Gallery.Utils { public static class Stores { public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); public static readonly string CacheFolder = FileSystem.CacheDirectory; private const string favoriteFile = "favorites.json"; private const string globalFile = "global.json"; private const string imageFolder = "img-original"; private const string previewFolder = "img-master"; private const string ugoiraFolder = "img-zip-ugoira"; private const string illustFile = "illust.json"; private const string pagesFolder = "pages"; private const string preloadsFolder = "preloads"; private const string thumbFolder = "img-thumb"; private const string userFolder = "user-profile"; //private const string recommendsFolder = "recommends"; public static bool NetworkAvailable { get { try { return Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.Internet; } catch { return false; } } } public static FavoriteList Favorites => GetFavoriteObject().Illusts; public static string FavoritesPath => Path.Combine(PersonalFolder, favoriteFile); public static DateTime FavoritesLastUpdated { get; set; } = DateTime.Now; private static IllustFavorite favoriteObject; public static IllustFavorite GetFavoriteObject(bool force = false) { if (force || favoriteObject == null) { var favorites = LoadFavoritesIllusts(); if (favorites != null) { favoriteObject = favorites; } else { favoriteObject = new IllustFavorite { Illusts = new FavoriteList() }; } } return favoriteObject; } public static IllustFavorite LoadFavoritesIllusts(string file = null) { if (file == null) { file = FavoritesPath; } return ReadObject(file); } public static void SaveFavoritesIllusts() { var file = FavoritesPath; var data = GetFavoriteObject(); data.LastFavoriteUtc = DateTime.UtcNow; WriteObject(file, data); } public static string LoadUgoiraImage(string zip, string frame) { var file = Path.Combine(PersonalFolder, ugoiraFolder, zip, frame); if (File.Exists(file)) { return file; } return null; } public static string SaveUgoiraImage(string zip, string frame, byte[] data) { try { var directory = Path.Combine(PersonalFolder, ugoiraFolder, zip); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var file = Path.Combine(directory, frame); File.WriteAllBytes(file, data); return file; } catch (Exception ex) { App.DebugError("save.ugoira", $"failed to save ugoira frame: {zip}/{frame}, error: {ex.Message}"); return null; } } public static string GetUgoiraPath(string url, string ext) { return Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ext); } private static T ReadObject(string file) { string content = null; if (File.Exists(file)) { try { content = File.ReadAllText(file); } catch (Exception ex) { App.DebugError("read", $"failed to read file: {file}, error: {ex.Message}"); } } else { //App.DebugError("read", $"file not found: {file}"); return default; } try { return JsonConvert.DeserializeObject(content); } catch (Exception ex) { App.DebugError("read", $"failed to parse illust JSON object, error: {ex.Message}"); return default; } } private static void WriteObject(string file, object obj) { var dir = Path.GetDirectoryName(file); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } string content; try { content = JsonConvert.SerializeObject(obj, Formatting.None); } catch (Exception ex) { App.DebugError("write", $"failed to serialize object, error: {ex.Message}"); return; } try { File.WriteAllText(file, content, Encoding.UTF8); } catch (Exception ex) { App.DebugError("write", $"failed to write file: {file}, error: {ex.Message}"); } } public static IllustData LoadIllustData(bool force = false) { var file = Path.Combine(PersonalFolder, illustFile); var result = HttpUtility.LoadObject( file, Configs.UrlIllustList, Configs.Referer, out _, force: force); if (result == null || result.error) { App.DebugPrint($"error when load illust data: {result?.message}, force({force})"); } return result; } public static IllustRankingData LoadIllustRankingData(string mode, string date, int page, out string error, bool force = false) { var file = Path.Combine(CacheFolder, mode, $"{date}_{page}.json"); string query = $"mode={mode}"; if (mode != "male" && mode != "male_r18") { query += "&content=illust"; } if (date != null) { query += $"&date={date}"; } var referer = string.Format(Configs.RefererIllustRanking, query); if (page > 1) { query += $"&p={page}"; } query += "&format=json"; var result = HttpUtility.LoadObject( file, string.Format(Configs.UrlIllustRanking, query), referer, out error, namehandler: rst => { return Path.Combine(CacheFolder, mode, $"{rst.date}_{page}.json"); }, header: headers => { headers.Add("X-Requested-With", "XMLHttpRequest"); }, force: force); if (result == null) { App.DebugPrint($"error when load ranking data: mode({mode}), date({date}), page({page}), force({force})"); } return result; } public static IllustRecommendsData LoadIllustRecommendsInitData(string id) { //var file = Path.Combine(CacheFolder, recommendsFolder, $"{id}.json"); var result = HttpUtility.LoadObject( null, string.Format(Configs.UrlIllustRecommendsInit, id), string.Format(Configs.RefererIllust, id), out _); if (result == null || result.error) { App.DebugPrint($"error when load recommends init data: {result?.message}"); } return result; } public static IllustRecommendsData LoadIllustRecommendsListData(string id, string[] ids) { if (ids == null || ids.Length == 0) { return null; } var ps = string.Concat(ids.Select(i => $"illust_ids%5B%5D={i}&")); var result = HttpUtility.LoadObject( null, string.Format(Configs.UrlIllustRecommendsList, ps), string.Format(Configs.RefererIllust, id), out _); if (result == null || result.error) { App.DebugPrint($"error when load recommends list data: {result?.message}"); } return result; } public static IllustGlobalData LoadGlobalData(bool force = false) { var file = Path.Combine(PersonalFolder, globalFile); var result = HttpUtility.LoadObject( file, Configs.Prefix, null, out _, force: force, header: h => { }, action: content => { var index = content.IndexOf(Configs.SuffixGlobal); if (index > 0) { index += Configs.SuffixGlobalLength; var end = content.IndexOf('\'', index); if (end > index) { content = content.Substring(index, end - index); } } return content; }); if (result == null) { App.DebugPrint($"error when load global data, is null"); } else { #if LOG App.DebugPrint($"current csrf token: {result.token}"); #endif Configs.CsrfToken = result.token; } return result; } public static string AddBookmark(string id) { if (string.IsNullOrEmpty(Configs.CsrfToken)) { return null; } var content = new StringContent( "{\"illust_id\":\"" + id + "\",\"restrict\":0,\"comment\":\"\",\"tags\":[]}", Encoding.UTF8, Configs.AcceptJson); var result = HttpUtility.LoadObject>( null, Configs.BookmarkAdd, Configs.Referer, out string error, force: true, post: content); if (error != null) { App.DebugPrint($"failed to add bookmark, error: {error}"); } else if (result == null || result.error || result.body == null) { App.DebugPrint($"failed to add bookmark, message: {result?.message}"); } else { #if LOG App.DebugPrint($"successs, bookmark id: {result.body.last_bookmark_id}, status: {result.body.stacc_status_id}"); #endif return result.body.last_bookmark_id; } return null; } public static bool DeleteBookmark(string id) { if (string.IsNullOrEmpty(Configs.CsrfToken)) { return false; } var content = new StringContent( "mode=delete_illust_bookmark&bookmark_id=" + id, Encoding.UTF8, Configs.AcceptUrlEncoded); var result = HttpUtility.LoadObject( null, Configs.BookmarkRpc, Configs.Referer, out string error, force: true, nojson: true, post: content); if (error != null) { App.DebugPrint($"failed to delete bookmark, error: {error}"); return false; } else if (result == null) { App.DebugPrint("failed to delete bookmark, result is null"); return false; } #if LOG App.DebugPrint($"successs, delete bookmark"); #endif return true; } public static IllustPreloadBody LoadIllustPreloadData(string id, bool downloading, bool force = false) { var file = Path.Combine(CacheFolder, preloadsFolder, $"{id}.json"); IllustPreloadBody result; if (!force) { result = ReadObject(file); if (result != null) { return result; } else if (!downloading) { return null; } } if (downloading) { result = HttpUtility.LoadObject( file, string.Format(Configs.UrlIllust, id), null, out _, force: force, action: content => { var index = content.IndexOf(Configs.SuffixPreload); if (index > 0) { index += Configs.SuffixPreloadLength; var end = content.IndexOf('\'', index); if (end > index) { content = content.Substring(index, end - index); } } return content; }); } else { result = null; } if (result == null) { App.DebugPrint($"error when load preload data: force({force})"); } return result; } public static IllustPageData LoadIllustPageData(string id, out string error, bool force = false) { var file = Path.Combine(CacheFolder, pagesFolder, $"{id}.json"); var result = HttpUtility.LoadObject( file, string.Format(Configs.UrlIllustPage, id), string.Format(Configs.RefererIllust, id), out _, force: force); if (result == null || result.error) { error = result?.message ?? "result is null"; App.DebugPrint($"error when load page data: {error}, force({force})"); return null; } error = null; return result; } public static IllustUgoiraData LoadIllustUgoiraData(string id, bool force = false) { var file = Path.Combine(PersonalFolder, ugoiraFolder, $"{id}.json"); var result = HttpUtility.LoadObject( file, string.Format(Configs.UrlIllustUgoira, id), string.Format(Configs.RefererIllust, id), out _, force: force); if (result == null || result.error) { App.DebugPrint($"error when load ugoira data: {result?.message}, force({force})"); } return result; } public static IllustUserListData LoadIllustUserInitData(string userId) { var list = HttpUtility.LoadObject( null, string.Format(Configs.UrlIllustUserAll, userId), string.Format(Configs.RefererIllustUser, userId), out _); if (list == null || list.error) { App.DebugPrint($"error when load user data: {list?.message}"); } return list; } public static IllustUserData LoadIllustUserData(string userId, string[] ids, bool firstPage) { if (ids == null || ids.Length == 0) { return null; } var ps = string.Concat(ids.Select(i => $"ids%5B%5D={i}&")); var result = HttpUtility.LoadObject( null, string.Format(Configs.UrlIllustUserArtworks, userId, ps, firstPage ? 1 : 0), string.Format(Configs.RefererIllustUser, userId), out _); if (result == null || result.error) { App.DebugPrint($"error when load user illust data: {result?.message}"); } return result; } //private static readonly Regex regexIllust = new Regex( // @"book_id\[\]"" value=""([0-9]+)"".*data-src=""([^""]+)"".*data-id=""([0-9]+)"".*" + // @"data-tags=""([^""]+)"".*data-user-id=""([0-9]+)"".*" + // @"class=""title"" title=""([^""]+)"".*data-user_name=""([^""]+)"".*" + // @"_bookmark-icon-inline"">([0-9]+)", // RegexOptions.Compiled); public static IllustItem[] LoadOnlineFavorites() { var userId = Configs.UserId; var list = new List(); int offset = 0; while (offset >= 0) { var result = HttpUtility.LoadObject( null, string.Format(Configs.UrlFavoriteList, userId, offset, 48), string.Format(Configs.RefererFavorites, userId), out _); if (result == null || result.error) { App.DebugPrint($"error when load favorites data: {result?.message}"); } else { if (offset + 48 < result.body.total) { offset += 48; } else { offset = -1; } list.AddRange(result.body.works.Select(i => i.ConvertToItem())); } } return list.Where(l => l != null).ToArray(); } public static ImageSource LoadIllustImage(string url) { return LoadImage(url, PersonalFolder, imageFolder, true); } public static ImageSource LoadPreviewImage(string url, bool downloading, string id = null, bool force = false) { if (downloading && Configs.DownloadIllustThreads > 1) { return LoadImageAsync(url, id, PersonalFolder, previewFolder, force).Result; } return LoadImage(url, PersonalFolder, previewFolder, downloading, force); } public static ImageSource LoadThumbnailImage(string url, bool downloading, bool force = false) { return LoadImage(url, CacheFolder, thumbFolder, downloading, force); } public static ImageSource LoadUserProfileImage(string url, bool downloading, bool force = false) { return LoadImage(url, CacheFolder, userFolder, downloading, force); } public static bool CheckIllustImage(string url) { var file = Path.Combine(PersonalFolder, imageFolder, Path.GetFileName(url)); return File.Exists(file); } public static bool CheckUgoiraVideo(string url) { var file = Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ".mp4"); return File.Exists(file); } public static string GetPreviewImagePath(string url) { var file = Path.Combine(PersonalFolder, previewFolder, Path.GetFileName(url)); if (File.Exists(file)) { return file; } return null; } private static ImageSource LoadImage(string url, string working, string folder, bool downloading, bool force = false) { var file = Path.Combine(working, folder, Path.GetFileName(url)); ImageSource image; if (!force && File.Exists(file)) { try { image = ImageSource.FromFile(file); } catch (Exception ex) { App.DebugError("image.load", $"failed to load image from file: {file}, error: {ex.Message}"); image = null; } } else { image = null; } if (downloading && image == null) { file = HttpUtility.DownloadImage(url, working, folder); if (file != null) { return ImageSource.FromFile(file); } } return image; } private static Task LoadImageAsync(string url, string id, string working, string folder, bool force = false) { var file = Path.Combine(working, folder, Path.GetFileName(url)); ImageSource image; if (!force && File.Exists(file)) { try { image = ImageSource.FromFile(file); } catch (Exception ex) { App.DebugError("image.load", $"failed to load image from file: {file}, error: {ex.Message}"); image = null; } } else { image = null; } if (image == null) { file = HttpUtility.DownloadImageAsync(url, id, working, folder).Result; if (file != null) { image = ImageSource.FromFile(file); } } return Task.FromResult(image); } } public class IllustFavorite { public DateTime LastFavoriteUtc { get; set; } public FavoriteList Illusts { get; set; } } public class FavoriteList : List { public bool Changed { get; private set; } public FavoriteList() : base() { } public FavoriteList(IEnumerable collection) : base(collection) { } public new void Insert(int index, IllustItem item) { base.Insert(index, item); Changed = true; } public new void InsertRange(int index, IEnumerable collection) { base.InsertRange(index, collection); Changed = true; } public new void RemoveAt(int index) { base.RemoveAt(index); Changed = true; } public FavoriteList Reload() { Changed = false; return this; } } public enum SyncType { None = 0, Prompt, AutoSync } public static class Configs { public const string ProfileNameKey = "name"; public const string ProfileIdKey = "pixiv_id"; public const string ProfileImageKey = "profile_img"; public const string CookieKey = "cookies"; public const string UserIdKey = "user_id"; public const string DownloadIllustThreadsKey = "download_illust_threads"; public const string IsOnR18Key = "is_on_r18"; public const string SyncFavTypeKey = "sync_fav_type"; public const string IsProxiedKey = "is_proxied"; public const string HostKey = "host"; public const string PortKey = "port"; public const string QueryModeKey = "query_mode"; public const string QueryTypeKey = "query_type"; public const string QueryDateKey = "query_date"; public const string FavoriteTypeKey = "favorite_type"; public const int MaxPageThreads = 3; public const int MaxThreads = 8; public const string Referer = "https://www.pixiv.net/"; public const string RefererIllust = "https://www.pixiv.net/artworks/{0}"; public const string RefererIllustRanking = "https://www.pixiv.net/ranking.php?{0}"; public const string RefererIllustUser = "https://www.pixiv.net/users/{0}/illustrations"; public const string RefererFavorites = "https://www.pixiv.net/users/{0}/bookmarks/artworks"; public static int DownloadIllustThreads; public static bool IsOnR18; public static SyncType SyncFavType; public static WebProxy Proxy; public static string Prefix => Proxy == null ? "https://www.pixiv.net/" : // https://hk.tsanie.org/reverse/ "https://www.pixiv.net/"; public static string UserId { get; private set; } public static string Cookie { get; private set; } public static string CsrfToken; public static void SetUserId(string userId, bool save = false) { UserId = userId; if (!save) { return; } if (userId == null) { Preferences.Remove(UserIdKey); } else { Preferences.Set(UserIdKey, userId); } } public static void SetCookie(string cookie, bool save = false) { Cookie = cookie; if (!save) { return; } if (cookie == null) { Preferences.Remove(CookieKey); } else { Preferences.Set(CookieKey, cookie); } } #if __IOS__ public static Task RequestCookieContainer(WebKit.WKHttpCookieStore cookieStore) { var task = new TaskCompletionSource(); cookieStore.GetAllCookies(cookies => { var list = new List(); foreach (var c in cookies) { #if DEBUG App.DebugPrint($"domain: {c.Domain}, path: {c.Path}, {c.Name}={c.Value}, http only: {c.IsHttpOnly}, session only: {c.IsSessionOnly}"); #endif var domain = c.Domain; if (domain == null) { continue; } if (domain != "www.pixiv.net" && domain != ".pixiv.net") { continue; } list.Add($"{c.Name}={c.Value}"); } var cookie = string.Join("; ", list); Cookie = cookie; Preferences.Set(CookieKey, cookie); task.SetResult(true); }); return task.Task; } #endif public const string SuffixGlobal = " id=\"meta-global-data\" content='"; public const int SuffixGlobalLength = 32; public const string SuffixPreload = " id=\"meta-preload-data\" content='"; public const int SuffixPreloadLength = 33; // SuffixPreload.Length public static string UrlIllustList => Prefix + "ajax/top/illust?mode=all&lang=zh"; public static string UrlIllust => Prefix + "artworks/{0}"; public static string UrlIllustRanking => Prefix + "ranking.php?{0}"; public static string UrlIllustUserAll => Prefix + "ajax/user/{0}/profile/all?lang=zh"; public static string UrlIllustUserArtworks => Prefix + "ajax/user/{0}/profile/illusts?{1}work_category=illust&is_first_page={2}&lang=zh"; public static string UrlIllustPage => Prefix + "ajax/illust/{0}/pages?lang=zh"; public static string UrlIllustUgoira => Prefix + "ajax/illust/{0}/ugoira_meta?lang=zh"; public static string UrlIllustRecommendsInit => Prefix + "ajax/illust/{0}/recommend/init?limit=18&lang=zh"; public static string UrlIllustRecommendsList => Prefix + "ajax/illust/recommend/illusts?{0}lang=zh"; public static string UrlFavoriteList => Prefix + "ajax/user/{0}/illusts/bookmarks?tag=&offset={1}&limit={2}&rest=show&lang=zh"; public static string BookmarkAdd => Prefix + "ajax/illusts/bookmarks/add"; public static string BookmarkRpc => Prefix + "rpc/index.php"; 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 AcceptImage = "image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5"; public const string AcceptPureImage = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; public const string AcceptJson = "application/json"; public const string AcceptUrlEncoded = "application/x-www-form-urlencoded"; //public const string AcceptEncoding = "gzip, deflate"; public const string AcceptLanguage = "zh-cn"; private const string URL_PREVIEW = "https://i.pximg.net/c/360x360_70"; public static string GetThumbnailUrl(string url) { if (url == null) { return null; } url = url.ToLower().Replace("/custom-thumb/", "/img-master/"); var index = url.LastIndexOf("_square1200.jpg"); if (index < 0) { index = url.LastIndexOf("_custom1200.jpg"); } if (index > 0) { url = url.Substring(0, index) + "_master1200.jpg"; } var start = url.IndexOf("/img-master/"); if (start > 0) { url = URL_PREVIEW + url.Substring(start); } return url; } } public static class Routes { public const string Illust = "illust"; public const string Detail = "detail"; public const string Follow = "follow"; public const string Recommends = "recommends"; public const string ByUser = "byuser"; public const string Ranking = "ranking"; public const string Favorites = "favorites"; public const string Option = "option"; } }