qwen edit

This commit is contained in:
Курнат Андрей
2026-02-18 14:36:57 +03:00
parent 8cb459c832
commit 1d9224a025
13 changed files with 292 additions and 45 deletions

View File

@@ -31,6 +31,8 @@ public static class MauiProgram
builder.Services.AddSingleton<IBookParserService, BookParserService>(); builder.Services.AddSingleton<IBookParserService, BookParserService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>(); builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddSingleton<INavigationService, NavigationService>(); builder.Services.AddSingleton<INavigationService, NavigationService>();
builder.Services.AddSingleton<ICoverCacheService, LruCoverCacheService>();
builder.Services.AddSingleton<ICachedImageLoadingService, CachedImageLoadingService>();
// HTTP Client for Calibre // HTTP Client for Calibre
builder.Services.AddHttpClient<ICalibreWebService, CalibreWebService>(); builder.Services.AddHttpClient<ICalibreWebService, CalibreWebService>();

View File

@@ -36,6 +36,9 @@ public class Book
public string? CalibreId { get; set; } // ID from Calibre-Web if downloaded from there public string? CalibreId { get; set; } // ID from Calibre-Web if downloaded from there
[Ignore]
public string? CoverCacheKey => $"book_{Id}_cover";
[Ignore] [Ignore]
public ImageSource? CoverImageSource public ImageSource? CoverImageSource
{ {
@@ -45,7 +48,7 @@ public class Book
{ {
return ImageSource.FromStream(() => new MemoryStream(CoverImage)); return ImageSource.FromStream(() => new MemoryStream(CoverImage));
} }
return ImageSource.FromFile("default_cover.png"); return null;
} }
} }

View File

@@ -1,4 +1,6 @@
namespace BookReader.Models; using SQLite;
namespace BookReader.Models;
public class CalibreBook public class CalibreBook
{ {
@@ -10,6 +12,10 @@ public class CalibreBook
public string Format { get; set; } = string.Empty; public string Format { get; set; } = string.Empty;
public byte[]? CoverImage { get; set; } public byte[]? CoverImage { get; set; }
[Ignore]
public string CoverCacheKey => $"calibre_{Id}_cover";
[Ignore]
public ImageSource? CoverImageSource public ImageSource? CoverImageSource
{ {
get get

View File

@@ -27,13 +27,21 @@ public class BookParserService : IBookParserService
var bookId = Guid.NewGuid().ToString(); var bookId = Guid.NewGuid().ToString();
var destPath = Path.Combine(_booksDir, $"{bookId}{extension}"); var destPath = Path.Combine(_booksDir, $"{bookId}{extension}");
// Copy file to app storage // Перемещаем файл вместо копирования для экономии памяти
if (sourceFilePath != destPath) if (sourceFilePath != destPath)
{ {
try
{
File.Move(sourceFilePath, destPath, overwrite: true);
}
catch (IOException)
{
// Если перемещение не удалось (например, файл на внешнем носителе), копируем
using var sourceStream = File.OpenRead(sourceFilePath); using var sourceStream = File.OpenRead(sourceFilePath);
using var destStream = File.Create(destPath); using var destStream = File.Create(destPath);
await sourceStream.CopyToAsync(destStream); await sourceStream.CopyToAsync(destStream);
} }
}
var book = new Book var book = new Book
{ {

View File

@@ -8,13 +8,15 @@ namespace BookReader.Services;
public class CalibreWebService : ICalibreWebService public class CalibreWebService : ICalibreWebService
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly ICoverCacheService _coverCacheService;
private string _baseUrl = string.Empty; private string _baseUrl = string.Empty;
private string _username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
public CalibreWebService(HttpClient httpClient) public CalibreWebService(HttpClient httpClient, ICoverCacheService coverCacheService)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_coverCacheService = coverCacheService;
_httpClient.Timeout = TimeSpan.FromSeconds(Constants.Network.HttpClientTimeoutSeconds); _httpClient.Timeout = TimeSpan.FromSeconds(Constants.Network.HttpClientTimeoutSeconds);
} }
@@ -61,7 +63,33 @@ public class CalibreWebService : ICalibreWebService
var bookIds = json["book_ids"]?.ToObject<List<int>>() ?? new List<int>(); var bookIds = json["book_ids"]?.ToObject<List<int>>() ?? new List<int>();
foreach (var bookId in bookIds) // Параллельная загрузка данных книг с ограничением
var semaphore = new SemaphoreSlim(4); // Максимум 4 параллельных запроса
var tasks = bookIds.Select(async bookId =>
{
await semaphore.WaitAsync();
try
{
return await LoadBookDataAsync(bookId);
}
finally
{
semaphore.Release();
}
});
var results = await Task.WhenAll(tasks);
books.AddRange(results.Where(b => b != null)!);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error fetching Calibre books: {ex.Message}");
}
return books;
}
private async Task<CalibreBook?> LoadBookDataAsync(int bookId)
{ {
try try
{ {
@@ -74,7 +102,7 @@ public class CalibreWebService : ICalibreWebService
f.Equals("EPUB", StringComparison.OrdinalIgnoreCase) || f.Equals("EPUB", StringComparison.OrdinalIgnoreCase) ||
f.Equals("FB2", StringComparison.OrdinalIgnoreCase)); f.Equals("FB2", StringComparison.OrdinalIgnoreCase));
if (supportedFormat == null) continue; if (supportedFormat == null) return null;
var authors = bookJson["authors"]?.ToObject<List<string>>() ?? new List<string>(); var authors = bookJson["authors"]?.ToObject<List<string>>() ?? new List<string>();
@@ -88,24 +116,26 @@ public class CalibreWebService : ICalibreWebService
DownloadUrl = $"{_baseUrl}/get/{supportedFormat}/{bookId}" DownloadUrl = $"{_baseUrl}/get/{supportedFormat}/{bookId}"
}; };
// Try to load cover // Try to load cover from cache first
var cacheKey = $"cover_{bookId}";
calibreBook.CoverImage = await _coverCacheService.GetCoverAsync(cacheKey);
if (calibreBook.CoverImage == null)
{
try try
{ {
calibreBook.CoverImage = await _httpClient.GetByteArrayAsync(calibreBook.CoverUrl); calibreBook.CoverImage = await _httpClient.GetByteArrayAsync(calibreBook.CoverUrl);
await _coverCacheService.SetCoverAsync(cacheKey, calibreBook.CoverImage);
} }
catch { } catch { }
}
books.Add(calibreBook); return calibreBook;
} }
catch { continue; } catch
}
}
catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"Error fetching Calibre books: {ex.Message}"); return null;
} }
return books;
} }
public async Task<string> DownloadBookAsync(CalibreBook book, IProgress<double>? progress = null) public async Task<string> DownloadBookAsync(CalibreBook book, IProgress<double>? progress = null)

View File

@@ -0,0 +1,8 @@
namespace BookReader.Services;
public interface ICoverCacheService
{
Task<byte[]?> GetCoverAsync(string key);
Task SetCoverAsync(string key, byte[]? coverData);
Task ClearAsync();
}

View File

@@ -0,0 +1,10 @@
namespace BookReader.Services;
/// <summary>
/// Сервис для оптимизированной загрузки изображений с кэшированием
/// </summary>
public interface ICachedImageLoadingService
{
Task<ImageSource?> LoadImageAsync(byte[]? imageData, string cacheKey);
void ClearCache();
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Concurrent;
namespace BookReader.Services;
/// <summary>
/// Сервис для оптимизированной загрузки изображений с кэшированием ImageSource
/// </summary>
public class CachedImageLoadingService : ICachedImageLoadingService
{
private readonly ConcurrentDictionary<string, ImageSource> _memoryCache = new();
private readonly SemaphoreSlim _semaphore = new(3); // Максимум 3 параллельных загрузки
public async Task<ImageSource?> LoadImageAsync(byte[]? imageData, string cacheKey)
{
if (imageData == null || imageData.Length == 0)
return null;
// Try memory cache first
if (_memoryCache.TryGetValue(cacheKey, out var cachedSource))
{
return cachedSource;
}
// Limit concurrent loads
await _semaphore.WaitAsync();
try
{
// Double-check after acquiring semaphore
if (_memoryCache.TryGetValue(cacheKey, out cachedSource))
{
return cachedSource;
}
// Create new ImageSource
var imageSource = ImageSource.FromStream(() => new MemoryStream(imageData));
_memoryCache[cacheKey] = imageSource;
return imageSource;
}
finally
{
_semaphore.Release();
}
}
public void ClearCache()
{
_memoryCache.Clear();
}
}

View File

@@ -0,0 +1,123 @@
using System.Collections.Concurrent;
namespace BookReader.Services;
public class LruCoverCacheService : ICoverCacheService
{
private readonly ConcurrentDictionary<string, byte[]> _cache = new();
private readonly Queue<string> _accessOrder = new();
private readonly int _maxSize;
private readonly string _cacheDir;
public LruCoverCacheService(int maxSize = 100)
{
_maxSize = maxSize;
_cacheDir = Path.Combine(FileSystem.AppDataDirectory, "cover_cache");
Directory.CreateDirectory(_cacheDir);
}
public async Task<byte[]?> GetCoverAsync(string key)
{
// Try memory cache first
if (_cache.TryGetValue(key, out var cached))
{
UpdateAccessOrder(key);
return cached;
}
// Try disk cache
var filePath = GetCacheFilePath(key);
if (File.Exists(filePath))
{
var data = await File.ReadAllBytesAsync(filePath);
await SetCoverAsync(key, data); // Add to memory cache
return data;
}
return null;
}
public async Task SetCoverAsync(string key, byte[]? coverData)
{
if (coverData == null || coverData.Length == 0)
return;
// Add to memory cache
if (_cache.Count >= _maxSize)
{
EvictOldest();
}
_cache.AddOrUpdate(key, coverData, (_, _) => coverData);
UpdateAccessOrder(key);
// Save to disk cache
var filePath = GetCacheFilePath(key);
await File.WriteAllBytesAsync(filePath, coverData);
}
public async Task ClearAsync()
{
_cache.Clear();
_accessOrder.Clear();
try
{
if (Directory.Exists(_cacheDir))
{
foreach (var file in Directory.GetFiles(_cacheDir))
{
File.Delete(file);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[CoverCache] Clear error: {ex.Message}");
}
await Task.CompletedTask;
}
private void UpdateAccessOrder(string key)
{
lock (_accessOrder)
{
if (_accessOrder.Contains(key))
{
var list = _accessOrder.ToList();
list.Remove(key);
_accessOrder.Clear();
foreach (var item in list)
_accessOrder.Enqueue(item);
}
_accessOrder.Enqueue(key);
}
}
private void EvictOldest()
{
lock (_accessOrder)
{
while (_accessOrder.Count > 0 && _cache.Count >= _maxSize)
{
var oldest = _accessOrder.Dequeue();
if (_cache.TryRemove(oldest, out var data))
{
var filePath = GetCacheFilePath(oldest);
if (File.Exists(filePath))
{
try { File.Delete(filePath); } catch { }
}
}
}
}
}
private string GetCacheFilePath(string key)
{
// Sanitize key for filename
var sanitized = string.Concat(key.Split(Path.GetInvalidFileNameChars()));
return Path.Combine(_cacheDir, $"{sanitized}.cache");
}
}

View File

@@ -12,6 +12,7 @@ public partial class BookshelfViewModel : BaseViewModel
private readonly IBookParserService _bookParserService; private readonly IBookParserService _bookParserService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService; private readonly INavigationService _navigationService;
private readonly ICachedImageLoadingService _imageLoadingService;
public ObservableCollection<Book> Books { get; } = new(); public ObservableCollection<Book> Books { get; } = new();
@@ -22,12 +23,14 @@ public partial class BookshelfViewModel : BaseViewModel
IDatabaseService databaseService, IDatabaseService databaseService,
IBookParserService bookParserService, IBookParserService bookParserService,
ISettingsService settingsService, ISettingsService settingsService,
INavigationService navigationService) INavigationService navigationService,
ICachedImageLoadingService imageLoadingService)
{ {
_databaseService = databaseService; _databaseService = databaseService;
_bookParserService = bookParserService; _bookParserService = bookParserService;
_settingsService = settingsService; _settingsService = settingsService;
_navigationService = navigationService; _navigationService = navigationService;
_imageLoadingService = imageLoadingService;
Title = "My Library"; Title = "My Library";
} }

View File

@@ -13,6 +13,7 @@ public partial class CalibreLibraryViewModel : BaseViewModel
private readonly IDatabaseService _databaseService; private readonly IDatabaseService _databaseService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService; private readonly INavigationService _navigationService;
private readonly ICachedImageLoadingService _imageLoadingService;
public ObservableCollection<CalibreBook> Books { get; } = new(); public ObservableCollection<CalibreBook> Books { get; } = new();
@@ -32,13 +33,15 @@ public partial class CalibreLibraryViewModel : BaseViewModel
IBookParserService bookParserService, IBookParserService bookParserService,
IDatabaseService databaseService, IDatabaseService databaseService,
ISettingsService settingsService, ISettingsService settingsService,
INavigationService navigationService) INavigationService navigationService,
ICachedImageLoadingService imageLoadingService)
{ {
_calibreWebService = calibreWebService; _calibreWebService = calibreWebService;
_bookParserService = bookParserService; _bookParserService = bookParserService;
_databaseService = databaseService; _databaseService = databaseService;
_settingsService = settingsService; _settingsService = settingsService;
_navigationService = navigationService; _navigationService = navigationService;
_imageLoadingService = imageLoadingService;
Title = "Calibre Library"; Title = "Calibre Library";
} }

View File

@@ -8,7 +8,7 @@ public partial class BookshelfPage : ContentPage
private readonly BookshelfViewModel _viewModel; private readonly BookshelfViewModel _viewModel;
private readonly INavigationService _navigationService; private readonly INavigationService _navigationService;
public BookshelfPage(BookshelfViewModel viewModel, INavigationService navigationService) public BookshelfPage(BookshelfViewModel viewModel, INavigationService navigationService, ICachedImageLoadingService imageLoadingService)
{ {
InitializeComponent(); InitializeComponent();
_viewModel = viewModel; _viewModel = viewModel;

View File

@@ -1,3 +1,4 @@
using BookReader.Services;
using BookReader.ViewModels; using BookReader.ViewModels;
namespace BookReader.Views; namespace BookReader.Views;
@@ -6,7 +7,7 @@ public partial class CalibreLibraryPage : ContentPage
{ {
private readonly CalibreLibraryViewModel _viewModel; private readonly CalibreLibraryViewModel _viewModel;
public CalibreLibraryPage(CalibreLibraryViewModel viewModel) public CalibreLibraryPage(CalibreLibraryViewModel viewModel, ICachedImageLoadingService imageLoadingService)
{ {
InitializeComponent(); InitializeComponent();
_viewModel = viewModel; _viewModel = viewModel;