using DotBased.Monads; using Manager.App.Constants; using Manager.App.Models.Library; using Manager.App.Models.Settings; using Manager.App.Models.System; using Manager.Data.Contexts; using Manager.Data.Entities.LibraryContext; using Manager.YouTube; using Manager.YouTube.Models.Innertube; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace Manager.App.Services; public class LibraryService : ILibraryService { private readonly ILogger _logger; private readonly LibrarySettings _librarySettings; private readonly IDbContextFactory _dbContextFactory; private readonly DirectoryInfo _libraryDirectory; public LibraryService(ILogger logger, IOptions librarySettings, IDbContextFactory contextFactory) { _logger = logger; _librarySettings = librarySettings.Value; _dbContextFactory = contextFactory; _libraryDirectory = Directory.CreateDirectory(_librarySettings.Path); logger.LogDebug("Working dir for library: {LibraryWorkingDir}", _libraryDirectory.FullName); Directory.CreateDirectory(Path.Combine(_librarySettings.Path, LibraryConstants.Directories.SubDirMedia)); Directory.CreateDirectory(Path.Combine(_librarySettings.Path, LibraryConstants.Directories.SubDirChannels)); } public async Task FetchChannelImagesAsync(Channel channel) { try { await using var context = await _dbContextFactory.CreateDbContextAsync(); await AddWebImagesAsync(context, channel.AvatarImages, channel.Id, "avatars", LibraryConstants.FileTypes.ChannelAvatar, LibraryConstants.Directories.SubDirChannels); await AddWebImagesAsync(context, channel.BannerImages, channel.Id, "banners", LibraryConstants.FileTypes.ChannelBanner, LibraryConstants.Directories.SubDirChannels); if (!context.ChangeTracker.HasChanges()) { _logger.LogInformation("No changes detected. Skipping."); return Result.Success(); } await context.SaveChangesAsync(); } catch (Exception e) { return ResultError.Error(e); } return Result.Success(); } private async Task AddWebImagesAsync(LibraryDbContext context, List images, string foreignKey, string libSubDir, string fileType, string subDir) { foreach (var image in images) { if (context.Files.Any(f => image.Url.Equals(f.OriginalUrl, StringComparison.OrdinalIgnoreCase))) { continue; } var downloadResult = await NetworkService.DownloadBytesAsync(new HttpRequestMessage(HttpMethod.Get, image.Url)); if (!downloadResult.IsSuccess) { continue; } var download = downloadResult.Value; var fileId = Guid.NewGuid(); var fileName = download.FileName ?? $"{fileId}.{download.ContentType?.Split('/').Last() ?? "unknown"}"; var relativePath = Path.Combine(foreignKey, libSubDir, $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{fileName}"); var savePath = Path.Combine(_libraryDirectory.FullName, subDir, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(savePath) ?? savePath); await using var fileStream = File.Create(savePath); await fileStream.WriteAsync(download.Data.AsMemory(0, download.Data.Length)); var file = new FileEntity { Id = fileId, OriginalUrl = image.Url, OriginalFileName = download.FileName, ForeignKey = foreignKey, FileType = fileType, RelativePath = relativePath.Replace('\\', '/'), MimeType = download.ContentType, SizeBytes = download.ContentLength, Height = image.Height, Width = image.Width }; await context.Files.AddAsync(file); } } public async Task> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(id)) { return ResultError.Fail("Channel id cannot be null or empty!"); } try { await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); var channel = await context.Channels.Include(c => c.ClientAccount).FirstOrDefaultAsync(c => c.Id == id, cancellationToken); if (channel == null) { return ResultError.Fail("Channel not found!"); } return channel; } catch (Exception e) { return HandleException(e); } } public async Task SaveChannelAsync(ChannelEntity channel, CancellationToken cancellationToken = default) { try { await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); if (context.Channels.Any(c => c.Id == channel.Id)) { context.Channels.Update(channel); } else { context.Channels.Add(channel); } var changed = await context.SaveChangesAsync(cancellationToken); return changed <= 0 ? ResultError.Fail("Failed to save channel!") : Result.Success(); } catch (Exception e) { return ResultError.Error(e); } } public async Task> GetLibraryInfoAsync(CancellationToken cancellationToken = default) { try { await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); var libInfo = new LibraryInformation { LibraryPath = _libraryDirectory.FullName, CreatedAtUtc = _libraryDirectory.CreationTimeUtc, LastModifiedUtc = _libraryDirectory.LastWriteTimeUtc, TotalChannels = await context.Channels.CountAsync(cancellationToken: cancellationToken), TotalMedia = await context.Media.CountAsync(cancellationToken: cancellationToken), TotalSizeBytes = GetDirectorySize(_libraryDirectory) }; return libInfo; } catch (Exception e) { return HandleException(e); } } public async Task> GetChannelsAsync(int total = 20, int offset = 0, CancellationToken cancellationToken = default) { try { await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); var orderedAccounts = context.Channels.Include(x => x.ClientAccount).Where(x => x.ClientAccount != null).OrderBy(x => x.Id); return new ListResultReturn(orderedAccounts.Skip(offset).Take(total).ToList(),orderedAccounts.Count()); } catch (Exception e) { return HandleException(e); } } private long GetDirectorySize(DirectoryInfo dir) { try { var size = dir.EnumerateFiles("*", SearchOption.AllDirectories).Select(f => f.Length).Sum(); return size; } catch (Exception e) { _logger.LogError(e, "Error while getting directory size."); throw; } } private ResultError HandleException(Exception exception) { if (exception is OperationCanceledException) { return ResultError.Fail("Library service operation cancelled"); } _logger.LogError(exception, "Failed to get library information"); return ResultError.Fail("Failed to get library information"); } }