From 00c0e91a0246d1f54527d9c949075e112ef6de37 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Fri, 25 Apr 2025 00:02:11 +0200 Subject: [PATCH 001/106] feat: add download grid from channels chore: bump versions --- TelegramDownloader/Data/ITelegramService.cs | 3 +- TelegramDownloader/Data/TelegramService.cs | 52 +++++++- TelegramDownloader/Pages/Config.razor | 1 - TelegramDownloader/Pages/FileGrid.razor | 114 ++++++++++++++++++ .../Pages/Modals/DowloadFromTelegram.razor | 8 ++ TelegramDownloader/Pages/_Layout.cshtml | 2 +- TelegramDownloader/Program.cs | 2 +- TelegramDownloader/Shared/NavMenu.razor | 10 +- TelegramDownloader/TelegramDownloader.csproj | 12 +- TelegramDownloader/wwwroot/css/site.css | 3 + 10 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 TelegramDownloader/Pages/FileGrid.razor diff --git a/TelegramDownloader/Data/ITelegramService.cs b/TelegramDownloader/Data/ITelegramService.cs index f636d87..a825d7d 100644 --- a/TelegramDownloader/Data/ITelegramService.cs +++ b/TelegramDownloader/Data/ITelegramService.cs @@ -22,11 +22,12 @@ public interface ITelegramService Task RemoveFavouriteChannel(long id); Task> getAllChats(); Task> getAllSavedChats(); - Task> getAllMessages(long id); + Task> getAllMessages(long id, Boolean onlyFiles = false); Task> getChatHistory(long id, int limit = 30, int addOffset = 0); string getChatName(long id); Task getMessageFile(string chatId, int idMessage); Task getPhotoThumb(ChatBase chat); + Task downloadPhotoThumb(Photo thumb); Task logOff(); Task sendVerificationCode(string vc); Task uploadFile(string chatId, Stream file, string fileName, string mimeType = null, UploadModel um = null, string caption = null); diff --git a/TelegramDownloader/Data/TelegramService.cs b/TelegramDownloader/Data/TelegramService.cs index a500e8c..65430e5 100644 --- a/TelegramDownloader/Data/TelegramService.cs +++ b/TelegramDownloader/Data/TelegramService.cs @@ -317,7 +317,46 @@ public async Task getMessageFile(string chatId, int idMessage) //return m; } - public async Task> getAllMessages(long id) + public async Task> getAllMessages(long id, Boolean onlyFiles = false) + { + List cm = new List(); + InputPeer peer = chats.chats[id]; + for (int offset_id = 0; ;) + { + var messages = await client.Messages_GetHistory(peer, offset_id); + if (messages.Messages.Length == 0) break; + foreach (MessageBase msgBase in messages.Messages) + { + if (msgBase is Message msg) + { + ChatMessages cm2 = new ChatMessages(); + cm2.htmlMessage = client.EntitiesToHtml(msg.message, msg.entities); + cm2.message = msg; + + cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); + cm2.isDocument = false; + if (msg.media is MessageMediaDocument { document: Document document }) + { + cm2.isDocument = true; + } + if (onlyFiles) + { + if (cm2.isDocument) + cm.Add(cm2); + } else + { + cm.Add(cm2); + } + + + } + } + offset_id = messages.Messages[^1].ID; + } + return cm; + } + + public async Task> getAllFileMessages(long id) { List cm = new List(); InputPeer peer = chats.chats[id]; @@ -395,6 +434,17 @@ public async Task getPhotoThumb(ChatBase chat) } + public async Task downloadPhotoThumb(Photo thumb) + { + MemoryStream ms = new MemoryStream(); + if (await client.DownloadFileAsync(thumb, ms) != 0) + { + return $"data:image/jpeg;base64,{Convert.ToBase64String(ms.ToArray())}"; + }; + return ""; + + } + public async Task AddFavouriteChannel(long id) { if (!GeneralConfigStatic.config.FavouriteChannels.Contains(id)) diff --git a/TelegramDownloader/Pages/Config.razor b/TelegramDownloader/Pages/Config.razor index 1ee4a3b..98ca7c5 100644 --- a/TelegramDownloader/Pages/Config.razor +++ b/TelegramDownloader/Pages/Config.razor @@ -71,7 +71,6 @@ -
@if (TelegramService.isPremium) diff --git a/TelegramDownloader/Pages/FileGrid.razor b/TelegramDownloader/Pages/FileGrid.razor new file mode 100644 index 0000000..61bb5cf --- /dev/null +++ b/TelegramDownloader/Pages/FileGrid.razor @@ -0,0 +1,114 @@ +@page "/filegrid/{id}" + +@using TL +@using TelegramDownloader.Data; +@using TelegramDownloader.Models; +@using TelegramDownloader.Services; + +@inject ITelegramService ts; +@inject PreloadService PreloadService; + +

File Grid

+ +@if (SelectedItems.Any()) +{ + +} + + + + + + @context.message.id + + + @context.message.message + + + @context.message.Date.ToString("dd/MM/yyyy HH:mm:ss") + + + @(context.message.edit_date.Year > 1900 + ? context.message.edit_date.ToString("dd/MM/yyyy HH:mm:ss") + : "") + + + @if (context.message.media is MessageMediaDocument { document: Document document }) + { + @document.Filename + } + + + @if (context.message.media is MessageMediaDocument { document: Document document }) + { + @HelperService.SizeSuffix(document.size) + } + + + @if (context.message.media is MessageMediaDocument { document: Document document }) + { + @document.mime_type + } + + + @context.videoThumb + @if (context.message.media is MessageMediaPhoto { photo: Photo photo }) + { + + } else { + + } + + + + +
+ Selected Items Count: @SelectedItems.Count +
+ + + + + +@code { + [Parameter] + public string id { get; set; } + public List messages = default!; + + private Modals.DowloadFromTelegram Modal { get; set; } + private HashSet SelectedItems { get; set; } = new(); + + private async Task> ChatsDataProvider(GridDataProviderRequest request) + { + if (messages is null) { + PreloadService.Show(SpinnerColor.Light, "Loading data..."); + messages = await ts.getAllMessages(Convert.ToInt64(id), true); + PreloadService.Hide(); + } + + return await Task.FromResult(request.ApplyTo(messages)); + } + + private void GetSelectedItems() + { + Modal.chatMessages = SelectedItems.ToList(); + Modal.isDownloadingChatMessages = true; + Modal.Open(); + // Aquí puedes manejar los elementos seleccionados + // Console.WriteLine($"Elementos seleccionados: {SelectedItems.Count}"); + // foreach (var item in SelectedItems) + // { + // Console.WriteLine($"Id: {item.message.id}, Name: {item.message.message}"); + // } + } +} diff --git a/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor b/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor index 38938b6..03df9ac 100644 --- a/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor +++ b/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor @@ -45,7 +45,10 @@ [Parameter] public BsonSharedInfoModel bsi { get; set; } public string filePath { get; set; } + public List fileList { get; set; } + public List chatMessages { get; set; } + public bool isDownloadingChatMessages { get; set; } = false; public string filename = ""; public string size = ""; @@ -79,6 +82,11 @@ private async Task Submit() { + if (isDownloadingChatMessages) + { + chatMessages.ForEach(message => fs.DownloadFileFromChat(message, null, ddtree.selectedNode == null ? null : ddtree.selectedNode.FirstOrDefault(), null)); + return; + } // ts. if (isShare) fs.downloadFile(DbService.SHARED_DB_NAME, fileList.ToList(), ddtree.selectedNode == null ? null : ddtree.selectedNode.FirstOrDefault(), bsi.CollectionId, bsi.ChannelId); diff --git a/TelegramDownloader/Pages/_Layout.cshtml b/TelegramDownloader/Pages/_Layout.cshtml index 2c703e6..c31aa84 100644 --- a/TelegramDownloader/Pages/_Layout.cshtml +++ b/TelegramDownloader/Pages/_Layout.cshtml @@ -45,7 +45,7 @@ - + diff --git a/TelegramDownloader/Program.cs b/TelegramDownloader/Program.cs index c3c8d15..98433bc 100644 --- a/TelegramDownloader/Program.cs +++ b/TelegramDownloader/Program.cs @@ -33,7 +33,7 @@ var builder = WebApplication.CreateBuilder(args); -Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("MzY0NjIwOUAzMjM4MmUzMDJlMzBZdngxNkEzSjRTQTczaDdlWkxEd1BIeUovNjh1eHByNEg5SkRVZ1lmWHc4PQ=="); +Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("MzgzMTc5MUAzMjM5MmUzMDJlMzAzYjMyMzkzYmw1ZDhSMmlTeTVTMFl0Ky9YaHBpZlRhd0NZS2RuQlVvenpnRVduRHFtZkE9"); builder.Services.AddSyncfusionBlazor(); if (!Directory.Exists(FileService.IMGDIR)) diff --git a/TelegramDownloader/Shared/NavMenu.razor b/TelegramDownloader/Shared/NavMenu.razor index 6f5ba76..0efb2db 100644 --- a/TelegramDownloader/Shared/NavMenu.razor +++ b/TelegramDownloader/Shared/NavMenu.razor @@ -76,6 +76,9 @@
+ @@ -109,6 +112,9 @@
+ @@ -191,7 +197,9 @@
- + diff --git a/TelegramDownloader/TelegramDownloader.csproj b/TelegramDownloader/TelegramDownloader.csproj index 1ba72ce..de1dc77 100644 --- a/TelegramDownloader/TelegramDownloader.csproj +++ b/TelegramDownloader/TelegramDownloader.csproj @@ -15,13 +15,13 @@ - + - - - - - + + + + + diff --git a/TelegramDownloader/wwwroot/css/site.css b/TelegramDownloader/wwwroot/css/site.css index 1d7b996..47b4982 100644 --- a/TelegramDownloader/wwwroot/css/site.css +++ b/TelegramDownloader/wwwroot/css/site.css @@ -103,3 +103,6 @@ a, .btn-link { padding-bottom: 5%; } +.download-btn { + margin-bottom: 2% !important; +} \ No newline at end of file From 8bad38f07ab3a6b715ed87eb7fc86e321ee975b8 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 24 May 2025 18:27:48 +0200 Subject: [PATCH 002/106] feat: show channel files --- TelegramDownloader/Data/ITelegramService.cs | 5 +- TelegramDownloader/Data/TelegramService.cs | 145 +++++++++++++++++- TelegramDownloader/Models/GeneralConfig.cs | 1 + TelegramDownloader/Pages/Config.razor | 9 ++ TelegramDownloader/Pages/FileGrid.razor | 31 ++-- .../Pages/Modals/DowloadFromTelegram.razor | 1 + .../Services/TransactionInfoService.cs | 7 +- TelegramDownloader/Shared/NavMenu.razor | 32 ++-- 8 files changed, 197 insertions(+), 34 deletions(-) diff --git a/TelegramDownloader/Data/ITelegramService.cs b/TelegramDownloader/Data/ITelegramService.cs index a825d7d..33436be 100644 --- a/TelegramDownloader/Data/ITelegramService.cs +++ b/TelegramDownloader/Data/ITelegramService.cs @@ -1,4 +1,5 @@ -using TelegramDownloader.Models; +using BlazorBootstrap; +using TelegramDownloader.Models; using TL; using WTelegram; @@ -23,6 +24,8 @@ public interface ITelegramService Task> getAllChats(); Task> getAllSavedChats(); Task> getAllMessages(long id, Boolean onlyFiles = false); + Task> getPaginatedMessages(long id, int page, int size, Boolean onlyFiles = false); + Task> getAllMediaMessages(long id, Boolean onlyFiles = false); Task> getChatHistory(long id, int limit = 30, int addOffset = 0); string getChatName(long id); Task getMessageFile(string chatId, int idMessage); diff --git a/TelegramDownloader/Data/TelegramService.cs b/TelegramDownloader/Data/TelegramService.cs index 65430e5..dc52f42 100644 --- a/TelegramDownloader/Data/TelegramService.cs +++ b/TelegramDownloader/Data/TelegramService.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Components; +using BlazorBootstrap; +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; +using System.Threading.Tasks; using TelegramDownloader.Data.db; using TelegramDownloader.Models; using TelegramDownloader.Services; @@ -330,10 +333,7 @@ public async Task> getAllMessages(long id, Boolean onlyFiles if (msgBase is Message msg) { ChatMessages cm2 = new ChatMessages(); - cm2.htmlMessage = client.EntitiesToHtml(msg.message, msg.entities); cm2.message = msg; - - cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); cm2.isDocument = false; if (msg.media is MessageMediaDocument { document: Document document }) { @@ -345,6 +345,8 @@ public async Task> getAllMessages(long id, Boolean onlyFiles cm.Add(cm2); } else { + cm2.htmlMessage = client.EntitiesToHtml(msg.message, msg.entities); + cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); cm.Add(cm2); } @@ -356,6 +358,141 @@ public async Task> getAllMessages(long id, Boolean onlyFiles return cm; } + public async Task> getAllMediaMessages(long id, Boolean onlyFiles = false) + { + List cm = new List(); + InputPeer peer = chats.chats[id]; + int size = 100; + int page = 1; + + var messages = await client.Messages_GetHistory(peer, add_offset: page -1, limit: size); + if (messages.Messages.Length == 0) return await Task.FromResult(cm); + foreach (MessageBase msgBase in messages.Messages) + { + if (msgBase is Message msg) + { + ChatMessages cm2 = new ChatMessages(); + cm2.message = msg; + cm2.isDocument = false; + if (msg.media is MessageMediaDocument { document: Document document }) + { + cm2.isDocument = true; + } + if (onlyFiles) + { + if (cm2.isDocument) + cm.Add(cm2); + } + else + { + cm2.htmlMessage = client.EntitiesToHtml(msg.message, msg.entities); + cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); + cm.Add(cm2); + } + } + } + + int totalMessages = messages.Count; + int gettedMessages = messages.Messages.Length; + int maxParalellTasks = 50; + int totalTasks = 0; + page++; + List>> tasks = new List>>(); + + while (gettedMessages < totalMessages) + { + int sizeGetted = gettedMessages + size >= totalMessages ? totalMessages - gettedMessages : size; + + tasks.Add(getPaginatedMessagesAsync(peer, page, sizeGetted, onlyFiles)); + page++; + totalTasks++; + gettedMessages += sizeGetted; + + if (totalTasks == maxParalellTasks || gettedMessages == totalMessages) + { + var results = await Task.WhenAll(tasks); + totalTasks = 0; + + foreach (var result in results) + { + cm.AddRange(result); + } + } + } + + return await Task.FromResult(cm); + } + + private async Task> getPaginatedMessagesAsync(InputPeer peer, int page, int size, Boolean onlyFiles = false) + { + List cm = new List(); + var messages = await client.Messages_GetHistory(peer, add_offset: (page - 1) * 100, limit: size); + if (messages.Messages.Length == 0) return cm; + foreach (MessageBase msgBase in messages.Messages) + { + if (msgBase is Message msg) + { + ChatMessages cm2 = new ChatMessages(); + cm2.message = msg; + cm2.isDocument = false; + if (msg.media is MessageMediaDocument { document: Document document }) + { + cm2.isDocument = true; + } + if (onlyFiles) + { + if (cm2.isDocument) + cm.Add(cm2); + } + else + { + cm2.htmlMessage = client.EntitiesToHtml(msg.message, msg.entities); + cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); + cm.Add(cm2); + } + + + } + } + return cm; + } + + public async Task> getPaginatedMessages(long id, int page, int size, Boolean onlyFiles = false) + { + List cm = new List(); + InputPeer peer = chats.chats[id]; + + var messages = await client.Messages_GetHistory(peer, add_offset: (page - 1) * 100, limit: size); + if (messages.Messages.Length == 0) return await Task.FromResult(new GridDataProviderResult { Data = cm, TotalCount = messages.Count }); + foreach (MessageBase msgBase in messages.Messages) + { + if (msgBase is Message msg) + { + ChatMessages cm2 = new ChatMessages(); + cm2.message = msg; + cm2.isDocument = false; + if (msg.media is MessageMediaDocument { document: Document document }) + { + cm2.isDocument = true; + } + if (onlyFiles) + { + if (cm2.isDocument) + cm.Add(cm2); + } + else + { + cm2.htmlMessage = client.EntitiesToHtml(msg.message, msg.entities); + cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); + cm.Add(cm2); + } + + + } + } + return await Task.FromResult(new GridDataProviderResult { Data = cm, TotalCount = messages.Count }); + } + public async Task> getAllFileMessages(long id) { List cm = new List(); diff --git a/TelegramDownloader/Models/GeneralConfig.cs b/TelegramDownloader/Models/GeneralConfig.cs index 4bd4374..9b04aca 100644 --- a/TelegramDownloader/Models/GeneralConfig.cs +++ b/TelegramDownloader/Models/GeneralConfig.cs @@ -74,6 +74,7 @@ public class GeneralConfig public bool CheckHash { get; set; } = false; public bool ShouldShowCaptionPath { get; set; } = false; public bool ShouldShowLogInTerminal { get; set; } = false; + public bool ShouldShowPaginatedFileChannel { get; set; } = false; public List FavouriteChannels { get; set; } = new List(); } diff --git a/TelegramDownloader/Pages/Config.razor b/TelegramDownloader/Pages/Config.razor index 98ca7c5..0390990 100644 --- a/TelegramDownloader/Pages/Config.razor +++ b/TelegramDownloader/Pages/Config.razor @@ -53,6 +53,15 @@
+ + +
+
+ + +
+ +
diff --git a/TelegramDownloader/Pages/FileGrid.razor b/TelegramDownloader/Pages/FileGrid.razor index 61bb5cf..4f01531 100644 --- a/TelegramDownloader/Pages/FileGrid.razor +++ b/TelegramDownloader/Pages/FileGrid.razor @@ -19,48 +19,49 @@ Class="table table-hover table-bordered table-striped" DataProvider="ChatsDataProvider" AllowPaging="true" -PageSize="25" +PageSize="@pageSize" AllowSelection="true" SelectionMode="GridSelectionMode.Multiple" @bind-SelectedItems="@SelectedItems" -PageSizeSelectorVisible="true" +PageSizeSelectorVisible="false" PageSizeSelectorItems="@(new int[] { 25, 50, 100 })" +AllowFiltering="true" Responsive="true"> - + @context.message.id - + @context.message.message - + @context.message.Date.ToString("dd/MM/yyyy HH:mm:ss") - + @(context.message.edit_date.Year > 1900 - ? context.message.edit_date.ToString("dd/MM/yyyy HH:mm:ss") - : "") + ? context.message.edit_date.ToString("dd/MM/yyyy HH:mm:ss") + : "") - + @if (context.message.media is MessageMediaDocument { document: Document document }) { @document.Filename } - + @if (context.message.media is MessageMediaDocument { document: Document document }) { @HelperService.SizeSuffix(document.size) } - + @if (context.message.media is MessageMediaDocument { document: Document document }) { @document.mime_type } - + @context.videoThumb @if (context.message.media is MessageMediaPhoto { photo: Photo photo }) { @@ -84,15 +85,17 @@ Responsive="true"> [Parameter] public string id { get; set; } public List messages = default!; + private int pageSize = 100; private Modals.DowloadFromTelegram Modal { get; set; } private HashSet SelectedItems { get; set; } = new(); private async Task> ChatsDataProvider(GridDataProviderRequest request) { - if (messages is null) { + if (messages is null) + { PreloadService.Show(SpinnerColor.Light, "Loading data..."); - messages = await ts.getAllMessages(Convert.ToInt64(id), true); + messages = await ts.getAllMediaMessages(Convert.ToInt64(id), true); PreloadService.Hide(); } diff --git a/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor b/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor index 03df9ac..7a13315 100644 --- a/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor +++ b/TelegramDownloader/Pages/Modals/DowloadFromTelegram.razor @@ -85,6 +85,7 @@ if (isDownloadingChatMessages) { chatMessages.ForEach(message => fs.DownloadFileFromChat(message, null, ddtree.selectedNode == null ? null : ddtree.selectedNode.FirstOrDefault(), null)); + Close(); return; } // ts. diff --git a/TelegramDownloader/Services/TransactionInfoService.cs b/TelegramDownloader/Services/TransactionInfoService.cs index 73cede0..8f5a46e 100644 --- a/TelegramDownloader/Services/TransactionInfoService.cs +++ b/TelegramDownloader/Services/TransactionInfoService.cs @@ -85,8 +85,8 @@ public void stopTimer() private void OnTimedEvent(Object source, ElapsedEventArgs e) { - _logger.LogInformation("El evento Elapsed se disparó a las {0:HH:mm:ss.fff}", e.SignalTime); - _logger.LogInformation("Upload: {0}, download: {1}", bytesUploaded, bytesDownloaded); + //_logger.LogInformation("El evento Elapsed se disparó a las {0:HH:mm:ss.fff}", e.SignalTime); + //_logger.LogInformation("Upload: {0}, download: {1}", bytesUploaded, bytesDownloaded); if (bytesDownloaded > 0) setDownloadSpeed(HelperService.SizeSuffixPerTime(bytesDownloaded)); if (bytesUploaded > 0) @@ -104,7 +104,6 @@ public async Task addDownloadBytes(long bytes) public async Task addUploadBytes(long bytes) { - _logger.LogInformation("Add bytes to upload {0}", bytes); UploadBytesMutex.WaitOne(); bytesUploaded += bytes; UploadBytesMutex.ReleaseMutex(); @@ -132,7 +131,7 @@ public void setDownloadSpeed(String speed) public void setUploadSpeed(String speed) { - _logger.LogInformation("Set bytes speed upload {0}", speed); + //_logger.LogInformation("Set bytes speed upload {0}", speed); uploadSpeed = speed; TaskEventChanged.Invoke(null, EventArgs.Empty); } diff --git a/TelegramDownloader/Shared/NavMenu.razor b/TelegramDownloader/Shared/NavMenu.razor index 0efb2db..6993525 100644 --- a/TelegramDownloader/Shared/NavMenu.razor +++ b/TelegramDownloader/Shared/NavMenu.razor @@ -11,7 +11,7 @@ @inject IJSRuntime JS; - + @@ -76,9 +76,12 @@
- + @if (GeneralConfigStatic.config.ShouldShowPaginatedFileChannel) + { + + } @@ -112,9 +115,13 @@
- + @if(GeneralConfigStatic.config.ShouldShowPaginatedFileChannel) + { + + } + @@ -197,9 +204,12 @@
- + @if (GeneralConfigStatic.config.ShouldShowPaginatedFileChannel) + { + + } From b0b0cc639359f31136bb71c71619a4249be96f08 Mon Sep 17 00:00:00 2001 From: mateof Date: Sat, 6 Sep 2025 18:26:47 +0200 Subject: [PATCH 003/106] feat: add media streaming from telegram (#32) --- .../Controllers/FileController.cs | 234 +++++++++++++++++- TelegramDownloader/Data/FileService.cs | 2 +- TelegramDownloader/Data/ITelegramService.cs | 1 + TelegramDownloader/Data/TelegramService.cs | 106 +++++++- TelegramDownloader/Data/db/DbService.cs | 2 +- .../Pages/Partials/impl/FileManagerImpl.razor | 81 ++++-- TelegramDownloader/TelegramDownloader.csproj | 14 +- 7 files changed, 398 insertions(+), 42 deletions(-) diff --git a/TelegramDownloader/Controllers/FileController.cs b/TelegramDownloader/Controllers/FileController.cs index 560e883..59e0bfe 100644 --- a/TelegramDownloader/Controllers/FileController.cs +++ b/TelegramDownloader/Controllers/FileController.cs @@ -9,13 +9,16 @@ using Syncfusion.Blazor.Inputs; using Syncfusion.EJ2.FileManager.Base; using Syncfusion.EJ2.FileManager.PhysicalFileProvider; +using Syncfusion.EJ2.Notifications; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net; using System.Net.Http.Headers; using System.Runtime.Serialization.Formatters.Binary; +using System.Web; using System.Xml.Linq; using TelegramDownloader.Data; using TelegramDownloader.Data.db; @@ -234,7 +237,7 @@ public async Task GetImage(string path) } [Route("GetFile/{id}")] - public async Task GetFile(string id, string? idChannel, string? idFile ) + public async Task GetFile(string id, string? idChannel, string? idFile) { var fileName = id; var mimeType = FileService.getMimeType(id.Split(".").Last()); @@ -242,7 +245,7 @@ public async Task GetFile(string id, string? idChannel, string? i if ( file == null ) { // HttpResponseMessage fullResponse = new HttpResponseMessage(HttpStatusCode.OK); // Request.CreateResponse(HttpStatusCode.OK); - Message idM = await _ts.getMessageFile(idChannel, Convert.ToInt32(idFile)); + TL.Message idM = await _ts.getMessageFile(idChannel, Convert.ToInt32(idFile)); ChatMessages cm = new ChatMessages(); cm.message = idM; file = new FileStream(System.IO.Path.Combine(FileService.TEMPDIR, "_temp", $"{idChannel}-{idFile}-{id}"), FileMode.Create, FileAccess.ReadWrite); @@ -253,6 +256,8 @@ public async Task GetFile(string id, string? idChannel, string? i await _ts.DownloadFileAndReturn(cm, file, model: dm); file.Position = 0; } + var request = HttpContext.Request; + var rangeHeader = request.Headers["Range"].ToString(); return new FileStreamResult(file, mimeType) { @@ -260,6 +265,231 @@ public async Task GetFile(string id, string? idChannel, string? i EnableRangeProcessing = true }; + } + + [Route("GetFileStream/{idChannel}/{idFile}/{name}")] + public async Task GetFileStream(string idChannel, string idFile, string name) + { + var fileName = name; + var mimeType = FileService.getMimeType(name.Split(".").Last()); + + var request = HttpContext.Request; + var rangeHeader = request.Headers["Range"].ToString(); + + var file = await _fs.getItemById(idChannel, idFile); + + TL.Message idM = await _ts.getMessageFile(idChannel, file.MessageId ?? file.ListMessageId.FirstOrDefault()); + + long totalLength = 0; // (await _fs.getItemById(idChannel, idFile)).Size; + + if(idM.media is MessageMediaDocument { document: Document document }) + { + totalLength = document.size; + } + + long from = 0; + long initialFrom = 0; + long to = 0; + + if (!string.IsNullOrEmpty(rangeHeader) && rangeHeader.StartsWith("bytes=")) + { + var range = rangeHeader.Replace("bytes=", "").Split('-'); + from = long.Parse(range[0]); + initialFrom = from; + if (range.Length > 1 && !string.IsNullOrEmpty(range[1])) + to = long.Parse(range[1]); + } + + if (from > 0) + { + from = (from / 524288) * 524288; + } + + Console.WriteLine("Fom:" + from); + + if (to == 0) + if (string.IsNullOrEmpty(rangeHeader) || from == 0) + to = (12 * 524288) + from; + else + to = (5 * 524288) + from; + else + to = ((to + 524288) / 524288) * 524288; + + if (to > totalLength) + { + to = totalLength; + } + + if (totalLength == initialFrom) + { + Response.StatusCode = 416; // Range Not Satisfiable + Response.Headers["Content-Range"] = $"bytes */{totalLength}"; + return new EmptyResult(); + } + + long length = to - from; + //Console.WriteLine("To length: " + to); + //Console.WriteLine("future download length: " + length); + + byte[] data = await _ts.DownloadFileStream(idM, from, (int)length); + + //Console.WriteLine("Total download length: " + data.Length); + + long skipedBytes = initialFrom - from - (length - data.Length); + + if (skipedBytes < 0) skipedBytes = 0; + + if (skipedBytes > data.Length) + return StatusCode(500, "No hay suficientes bytes en el bloque descargado"); + + long dataLength = data.Length - skipedBytes; + Response.ContentLength = dataLength; //(dataLength + initialFrom) >= totalLength ? totalLength - initialFrom : dataLength; + // Response.StatusCode = (int)HttpStatusCode.PartialContent; // 206 + Response.StatusCode = StatusCodes.Status206PartialContent; // StatusCodes.Status206PartialContent; + Response.ContentType = mimeType; + Response.Headers["Content-Disposition"] = $"inline; filename=\"{HttpUtility.UrlEncode(fileName)}\""; + Response.Headers.Add("Content-Range", $"bytes {(initialFrom >= totalLength ? totalLength - 1 : initialFrom)}-{(initialFrom >= totalLength ? totalLength - 1 : initialFrom + Response.ContentLength - 1)}/{totalLength}"); + Response.Headers.Add("Accept-Ranges", "bytes"); + + //if (Request.Headers.ContainsKey("Range")) + //{ + // Console.WriteLine("Range header recibido:"); + // Console.WriteLine(Request.Headers["Range"]); + //} + + //foreach (var header in Response.Headers) + //{ + // Console.WriteLine($"{header.Key}: {header.Value}"); + //} + + + // Console.WriteLine("Skiped bytes: " + skipedBytes); + + var stream = new MemoryStream(data, (int)skipedBytes, (int)Response.ContentLength); + stream.Position = 0; + + // Console.WriteLine("Real Data length: " + stream.Length); + + // Console.WriteLine("---------------------------------------------------"); + + var cancellationToken = HttpContext.RequestAborted; + + try + { + await stream.CopyToAsync(Response.Body, 64 * 1024, cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + + return new EmptyResult(); + } + catch (OperationCanceledException) + { + Console.WriteLine("❌ El cliente cerró la conexión."); + } + + return new EmptyResult(); + + } + + [Route("GetFileStream2/{idChannel}/{idFile}/{name}")] + public async Task GetFileStream2(string idChannel, string idFile, string name) + { + var fileName = name; + var mimeType = FileService.getMimeType(name.Split(".").Last()); + + var request = HttpContext.Request; + var rangeHeader = request.Headers["Range"].ToString(); + + var file = await _fs.getItemById(idChannel, idFile); + + // HttpResponseMessage fullResponse = new HttpResponseMessage(HttpStatusCode.OK); // Request.CreateResponse(HttpStatusCode.OK); + TL.Message idM = await _ts.getMessageFile(idChannel, file.MessageId ?? file.ListMessageId.FirstOrDefault()); + //byte[] data = await _ts.DownloadFileStream(idM, 0, 512); + //var stream = new MemoryStream(data); + + // Supón que puedes obtener el tamaño total del archivo remoto + long totalLength = (await _fs.getItemById(idChannel, idFile)).Size; + + long from = 0; + long initialFrom = 0; + long to = 0; + + + + if (!string.IsNullOrEmpty(rangeHeader) && rangeHeader.StartsWith("bytes=")) + { + var range = rangeHeader.Replace("bytes=", "").Split('-'); + from = long.Parse(range[0]); + initialFrom = from; + if (range.Length > 1 && !string.IsNullOrEmpty(range[1])) + to = long.Parse(range[1]); + } + + if (from > 0) + { + from = (from / 524288) * 524288; + } + + if (to == 0) + to = (25 * 524288) + from; + else + to = ((to + 524288) / 524288) * 524288; + + if (to > totalLength) + { + to = totalLength - 1; + } + + long length = to - from; + + byte[] data = await _ts.DownloadFileStream(idM, from, (int)length); + + long skipedBytes = initialFrom - from; // (data.Length + initialFrom) >= totalLength ? data.Length - (totalLength - initialFrom) : initialFrom - from; + + if (skipedBytes < 0) skipedBytes = 0; + + if (skipedBytes >= data.Length) + return StatusCode(500, "No hay suficientes bytes en el bloque descargado"); + + long dataLength = data.Length - skipedBytes; + Response.ContentLength = (dataLength + initialFrom) >= totalLength ? totalLength - initialFrom : dataLength; + // Response.StatusCode = (int)HttpStatusCode.PartialContent; // 206 + Response.StatusCode = StatusCodes.Status206PartialContent; + Response.ContentType = mimeType; + Response.Headers.Add("Content-Range", $"bytes {initialFrom}-{(initialFrom + Response.ContentLength - 1)}/{totalLength}"); + Response.Headers.Add("Accept-Ranges", "bytes"); + + if (Request.Headers.ContainsKey("Range")) + { + Console.WriteLine("Range header recibido:"); + Console.WriteLine(Request.Headers["Range"]); + } + + foreach (var header in Response.Headers) + { + Console.WriteLine($"{header.Key}: {header.Value}"); + } + + + + + var stream = new MemoryStream(data, (int)skipedBytes, (int)Response.ContentLength); + + Console.WriteLine("Real Data length: " + stream.Length); + + Console.WriteLine("---------------------------------------------------"); + + //return new FileStreamResult(stream, mimeType); + var cancellationToken = HttpContext.RequestAborted; + + await stream.CopyToAsync(Response.Body, 64 * 1024, cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + + return new EmptyResult(); + // return File(stream, mimeType, enableRangeProcessing: false); + //return new FileStreamResult(stream, mimeType) + //{ + // EnableRangeProcessing = false + //}; } diff --git a/TelegramDownloader/Data/FileService.cs b/TelegramDownloader/Data/FileService.cs index eae4b4a..94c5b67 100644 --- a/TelegramDownloader/Data/FileService.cs +++ b/TelegramDownloader/Data/FileService.cs @@ -117,7 +117,7 @@ public class FileService : IFileService {"mid", "audio/midi"}, {"midi", "audio/midi"}, {"mif", "application/vnd.mif"}, - {"mkv", "video/webm" }, + {"mkv", "video/x-matroska" }, {"mov", "video/quicktime"}, {"movie", "video/x-sgi-movie"}, {"mp2", "audio/mpeg"}, diff --git a/TelegramDownloader/Data/ITelegramService.cs b/TelegramDownloader/Data/ITelegramService.cs index 33436be..71fbdf4 100644 --- a/TelegramDownloader/Data/ITelegramService.cs +++ b/TelegramDownloader/Data/ITelegramService.cs @@ -17,6 +17,7 @@ public interface ITelegramService Task joinChatInvitationHash(string? hash); Task CallQrGenerator(Action func, CancellationToken ct, bool logoutFirst = false); Task DownloadFile(ChatMessages message, string fileName = null, string folder = null, DownloadModel model = null, bool shouldAddToList = false); + Task DownloadFileStream(Message message, long offset, int limit); Task DownloadFileAndReturn(ChatMessages message, Stream ms = null, string fileName = null, string folder = null, DownloadModel model = null); Task> GetFouriteChannels(bool mustRefresh = true); Task AddFavouriteChannel(long id); diff --git a/TelegramDownloader/Data/TelegramService.cs b/TelegramDownloader/Data/TelegramService.cs index dc52f42..92f8c92 100644 --- a/TelegramDownloader/Data/TelegramService.cs +++ b/TelegramDownloader/Data/TelegramService.cs @@ -1,11 +1,19 @@ using BlazorBootstrap; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Syncfusion.Blazor.Inputs; +using Syncfusion.Blazor.Kanban.Internal; +using Syncfusion.Blazor.Sparkline.Internal; using System.Collections.Generic; +using System.Reflection.Metadata; using System.Threading.Tasks; using TelegramDownloader.Data.db; using TelegramDownloader.Models; using TelegramDownloader.Services; using TL; +using TL.Layer23; +using TL.Methods; +using WTelegram; namespace TelegramDownloader.Data { @@ -13,6 +21,7 @@ public class TelegramService : ITelegramService { public static bool isPremium = false; public static int splitSizeGB = 2; + private const int FILESPLITSIZE = 524288; // 512 * 1024; private TransactionInfoService _tis { get; set; } private IDbService _db { get; set; } private static WTelegram.Client client = null; @@ -335,7 +344,7 @@ public async Task> getAllMessages(long id, Boolean onlyFiles ChatMessages cm2 = new ChatMessages(); cm2.message = msg; cm2.isDocument = false; - if (msg.media is MessageMediaDocument { document: Document document }) + if (msg.media is MessageMediaDocument { document: TL.Document document }) { cm2.isDocument = true; } @@ -374,7 +383,7 @@ public async Task> getAllMediaMessages(long id, Boolean onlyF ChatMessages cm2 = new ChatMessages(); cm2.message = msg; cm2.isDocument = false; - if (msg.media is MessageMediaDocument { document: Document document }) + if (msg.media is MessageMediaDocument { document: TL.Document document }) { cm2.isDocument = true; } @@ -435,7 +444,7 @@ private async Task> getPaginatedMessagesAsync(InputPeer peer, ChatMessages cm2 = new ChatMessages(); cm2.message = msg; cm2.isDocument = false; - if (msg.media is MessageMediaDocument { document: Document document }) + if (msg.media is MessageMediaDocument { document: TL.Document document }) { cm2.isDocument = true; } @@ -471,7 +480,7 @@ public async Task> getPaginatedMessages(lon ChatMessages cm2 = new ChatMessages(); cm2.message = msg; cm2.isDocument = false; - if (msg.media is MessageMediaDocument { document: Document document }) + if (msg.media is MessageMediaDocument { document: TL.Document document }) { cm2.isDocument = true; } @@ -511,7 +520,7 @@ public async Task> getAllFileMessages(long id) cm2.user = messages.UserOrChat(msg.From ?? msg.Peer); cm2.isDocument = false; - if (msg.media is MessageMediaDocument { document: Document document }) + if (msg.media is MessageMediaDocument { document: TL.Document document }) { cm2.isDocument = true; } @@ -547,7 +556,7 @@ public async Task> getChatHistory(long id, int limit = 30, in cm2.user = mess.UserOrChat(mb.From ?? mb.Peer); cm2.isDocument = false; - if (msg.media is MessageMediaDocument { document: Document document }) + if (msg.media is MessageMediaDocument { document: TL.Document document }) { cm2.isDocument = true; } @@ -601,6 +610,84 @@ public async Task RemoveFavouriteChannel(long id) await GetFouriteChannels(); } } + public async Task DownloadFileStream(Message message, long offset, int limit) + { + int totalLimit = limit; + long currentOffset = offset; + Byte[] response; + if (message is Message msg && msg.media is MessageMediaDocument doc) + { + try + { + using (var memoryStream = new MemoryStream()) + { + while (totalLimit > 0) + { + int totalDownloadBytes = FILESPLITSIZE; + if (totalLimit >= FILESPLITSIZE) + { + totalDownloadBytes = FILESPLITSIZE; + + } + //else if (totalLimit >= 4096) + //{ + // // Busca el mayor múltiplo de 4096 menor o igual a totalLimit + // totalDownloadBytes = (totalLimit / 4096) * 4096; + //} + else + { + // Último trozo, menor de 4096 + totalDownloadBytes = FILESPLITSIZE; // totalLimit; + //totalDownloadBytes = totalLimit; + } + + InputDocument inputFile = doc.document; + var location = new InputDocumentFileLocation + { + id = inputFile.id, + access_hash = inputFile.access_hash, + file_reference = inputFile.file_reference, + thumb_size = "" + }; + + //var file = await client.Invoke(new Upload_GetFile + //{ + // location = new InputDocumentFileLocation + // { + // id = inputFile.id, + // access_hash = inputFile.access_hash, + // file_reference = inputFile.file_reference, + // thumb_size = "" + // }, + // offset = currentOffset, + // limit = totalDownloadBytes + //}); + + var file = await client.Upload_GetFile(location, currentOffset, limit: totalDownloadBytes); + + if (file is Upload_File uploadFile) + { + //uploadFile.WriteTL(new BinaryWriter(memoryStream)); + var fileBytes = uploadFile.bytes; + memoryStream.Write(fileBytes, 0, fileBytes.Length); + currentOffset += (totalDownloadBytes); + totalLimit -= (totalDownloadBytes); + continue; + } + throw new InvalidOperationException("Unexpected file type returned."); + } + + return memoryStream.ToArray(); + } + } + catch (Exception e) + { + throw new InvalidOperationException("Unexpected file type returned."); + } + } + + throw new ArgumentException("Invalid message or media type."); + } public async Task DownloadFileAndReturn(ChatMessages message, Stream ms = null, string fileName = null, string folder = null, DownloadModel model = null) { @@ -613,7 +700,7 @@ public async Task DownloadFileAndReturn(ChatMessages message, Stream ms model.m = message; model.channel = message.user; - if (message.message.media is MessageMediaDocument { document: Document document }) + if (message.message.media is MessageMediaDocument { document: TL.Document document }) { model._transmitted = 0; @@ -628,6 +715,7 @@ public async Task DownloadFileAndReturn(ChatMessages message, Stream ms MemoryStream dest = new MemoryStream(); // using var dest = new FileStream($"{Path.Combine(folder != null ? folder : Path.Combine(Environment.CurrentDirectory, "download"), filename)}", FileMode.Create, FileAccess.Write); await client.DownloadFileAsync(document, ms ?? dest, null, model.ProgressCallback); + return ms ?? dest; // fileStream.CopyTo(dest); Console.WriteLine("Download finished"); @@ -640,7 +728,7 @@ public async Task DownloadFileAndReturn(ChatMessages message, Stream ms Console.WriteLine("Downloading " + filename); MemoryStream dest = new MemoryStream(); // using var dest = new FileStream($"{Path.Combine(Environment.CurrentDirectory!, "wwwroot", "img", "telegram", filename)}", FileMode.Create, FileAccess.Write); - var type = await client.DownloadFileAsync(photo, ms ?? dest, null, model.ProgressCallback); + var type = await client.DownloadFileAsync(photo, ms ?? dest, (PhotoSizeBase)null, model.ProgressCallback); dest.Close(); // necessary for the renaming Console.WriteLine("Download finished"); //if (type is not Storage_FileType.unknown and not Storage_FileType.partial) @@ -666,7 +754,7 @@ public async Task DownloadFile(ChatMessages message, string fileName = n model.m = message; model.channel = message.user; - if (message.message.media is MessageMediaDocument { document: Document document }) + if (message.message.media is MessageMediaDocument { document: TL.Document document }) { model._transmitted = 0; diff --git a/TelegramDownloader/Data/db/DbService.cs b/TelegramDownloader/Data/db/DbService.cs index cc37686..85f6086 100644 --- a/TelegramDownloader/Data/db/DbService.cs +++ b/TelegramDownloader/Data/db/DbService.cs @@ -333,7 +333,7 @@ public async Task> getAllFilesInDirectoryPath2(string { if (collectionName == null) collectionName = "directory"; - var result = await (await getDatabase(dbName).GetCollection(collectionName).FindAsync(Builders.Filter.Where(x => x.FilePath + "/" == path || x.FilterPath == path))).ToListAsync(); + var result = await (await getDatabase(dbName).GetCollection(collectionName).FindAsync(Builders.Filter.Where(x => x.FilterId == path || x.FilePath + "/" == path || x.FilterPath == path))).ToListAsync(); return result; } diff --git a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor index e7504b8..4833273 100644 --- a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor @@ -6,6 +6,8 @@ @using TelegramDownloader.Data @using TelegramDownloader.Data.db @using TelegramDownloader.Models +@using TelegramDownloader.Pages.Modals.InfoModals +@using TelegramDownloader.Pages.Modals @using TelegramDownloader.Pages.Partials @inject IFileService fs @inject ITelegramService ts @@ -18,18 +20,18 @@ + ToolbarItemClicked="toolBarClicked" + ItemsMoving="ItemsMovingAsync" + ItemRenaming="ItemRenamingAsync" + Searching="SearchingAsync" + OnFileOpen="fileOpen" + FileSelection="FileSelectionAsync" + FileSelected="FileSelectedAsync" + OnRead="OnReadAsync" + FolderCreating="FolderCreatingAsync" + BeforeDownload="BeforeDownload" + ItemsDeleting="ItemsDeletingAsync" + ItemsUploaded="ItemsUploadedAsync"> @@ -37,6 +39,8 @@ + + @@ -53,6 +57,9 @@ public static string[] selectedItems = new string[] { "" }; public static bool downloadToServer = true; LocalFileManager lfm { get; set; } + private VideoPlayerModal videoPlayer { get; set; } = default!; + + private MediaUrlModal mediaUrlModal { get; set; } = default!; public string[] ContextItems = new string[] { "Open", "Delete", "Download", "Rename", "Details"}; @@ -68,6 +75,8 @@ new ToolBarItemModel() { Name = "Selection" }, new ToolBarItemModel() { Name = "View" }, new ToolBarItemModel() { Name = "Details" }, + new ToolBarItemModel() { Name = "UrlMedia", Text = "Url Media", TooltipText = "Url Media", PrefixIcon = "bi bi-collection-play-fill", Visible = false } + }; // protected override async Task OnParametersSetAsync() @@ -81,6 +90,7 @@ } + protected override async Task OnInitializedAsync() { if (!isShared) @@ -121,23 +131,24 @@ // args.Response = await FileManagerService.Rename(args.Path, args.File.Name, args.NewName, false, args.ShowFileExtension, args.File); } - public async Task FileOpen(FileOpenEventArgs args) + public async Task fileOpen(FileOpenEventArgs args) { - if (!isShared && args != null && args.FileDetails != null && args.FileDetails.IsFile && new List { ".mp3", ".ogg", ".flac", ".aac", ".wav" }.Contains(args.FileDetails.Type.ToLower())) + if (args != null && args.FileDetails != null && args.FileDetails.IsFile && new List { ".mp3", ".ogg", ".flac", ".aac", ".wav" }.Contains(args.FileDetails.Type.ToLower())) { await playAudio(args); - // string localdir = "/" + FileService.STATICRELATIVELOCALDIR.Replace("\\", "/"); - // string path = Path.Combine(localdir, args.FileDetails.FilterPath.Substring(1).Replace("\\", "/"), args.FileDetails.Name); - // await JSRuntime.InvokeVoidAsync("open", path, "_blank"); + } + else + { + if (args != null && args.FileDetails != null && args.FileDetails.IsFile && new List { ".mov", ".mp4", ".m4v", ".ogv", ".webm", ".flv", ".f4v" }.Contains(args.FileDetails.Type.ToLower())) + { + videoPlayer.ShowModal(getMediaUrl(args.FileDetails.Id, args.FileDetails.Name)); + } } } private async Task playAudio(FileOpenEventArgs args) { - var file = await fs.getItemById(id, args.FileDetails.Id); - string path = MyNavigationManager.BaseUri + "api/file/getfile/" + args.FileDetails.Name + $"?idChannel={id}&idFile={file.MessageId}"; - // string path = new System.Uri((Path.Combine(localdir, args.FileDetails.FilterPath.Substring(1).Replace("\\", "/"), args.FileDetails.Name)).Replace("\\", "/")).AbsoluteUri; - await JSRuntime.InvokeVoidAsync("openAudioPlayerModal", path, FileService.getMimeType(args.FileDetails.Name.Split(".").Last()), args.FileDetails.Name); + await JSRuntime.InvokeVoidAsync("openAudioPlayerModal", getMediaUrl(args.FileDetails.Id, args.FileDetails.Name), null, args.FileDetails.Name); } public async Task SearchingAsync(SearchEventArgs args) @@ -188,6 +199,18 @@ Console.WriteLine(""); } + private async Task ShowMediaURLModal(string idFile, String name) + { + mediaUrlModal.url = getMediaUrl(idFile, name); + await mediaUrlModal.OnShowModalClick(); + } + + private string getMediaUrl(string idFile, String name) + { + string localdir = MyNavigationManager.BaseUri; + return new System.Uri(Path.Combine(localdir, "api/file/GetFileStream", id, idFile, name).Replace("\\", "/")).AbsoluteUri; + } + public async Task toolBarClicked(ToolbarClickEventArgs args) { if (args.Item.Text == "Download To Local") @@ -207,6 +230,10 @@ { MyNavigationManager.NavigateTo("/api/file/share/" + id + $"?bsonId={args.FileDetails.FirstOrDefault().Id}&fileName={args.FileDetails.FirstOrDefault().Name}", true); } + if (args.Item.Text == "Url Media" && args.FileDetails.Count() > 0) + { + await ShowMediaURLModal(args.FileDetails[0].Id, args.FileDetails[0].Name); + } Console.WriteLine(""); } @@ -226,7 +253,17 @@ public async Task FileSelectionAsync(FileSelectionEventArgs args) { - Console.WriteLine(""); + List selectedList = fm.GetSelectedFiles(); + if ((selectedList.Count() + (args.Action == "UnSelect" ? -1 : 1)) == 1) + { + if (args.FileDetails.IsFile) + { + Items.Where(x => x.Name == "UrlMedia").FirstOrDefault().Visible = true; + return; + } + + } + Items.Where(x => x.Name == "UrlMedia").FirstOrDefault().Visible = false; } public async Task FileSelectedAsync(FileSelectEventArgs args) diff --git a/TelegramDownloader/TelegramDownloader.csproj b/TelegramDownloader/TelegramDownloader.csproj index de1dc77..f3d1bac 100644 --- a/TelegramDownloader/TelegramDownloader.csproj +++ b/TelegramDownloader/TelegramDownloader.csproj @@ -13,15 +13,15 @@ - + - + - - - - - + + + + + From 7f9345019312018c7c56126951870852c261d043 Mon Sep 17 00:00:00 2001 From: mateof Date: Tue, 9 Sep 2025 19:00:09 +0200 Subject: [PATCH 004/106] feat: media stream for huge files (#34) --- .../Controllers/FileController.cs | 35 +++++++++++++++---- TelegramDownloader/TelegramDownloader.csproj | 6 ++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/TelegramDownloader/Controllers/FileController.cs b/TelegramDownloader/Controllers/FileController.cs index 59e0bfe..d431d35 100644 --- a/TelegramDownloader/Controllers/FileController.cs +++ b/TelegramDownloader/Controllers/FileController.cs @@ -277,29 +277,50 @@ public async Task GetFileStream(string idChannel, string idFile, var rangeHeader = request.Headers["Range"].ToString(); var file = await _fs.getItemById(idChannel, idFile); + long totalLength = file.Size; TL.Message idM = await _ts.getMessageFile(idChannel, file.MessageId ?? file.ListMessageId.FirstOrDefault()); - long totalLength = 0; // (await _fs.getItemById(idChannel, idFile)).Size; + long totalPartialFileLength = 0; - if(idM.media is MessageMediaDocument { document: Document document }) + if (idM.media is MessageMediaDocument { document: Document document }) { - totalLength = document.size; + totalPartialFileLength = document.size; } long from = 0; long initialFrom = 0; + long initialPartFrom = 0; long to = 0; + long accumulatePart = 0; if (!string.IsNullOrEmpty(rangeHeader) && rangeHeader.StartsWith("bytes=")) { var range = rangeHeader.Replace("bytes=", "").Split('-'); from = long.Parse(range[0]); initialFrom = from; + initialPartFrom = from; if (range.Length > 1 && !string.IsNullOrEmpty(range[1])) to = long.Parse(range[1]); } + if (from >= totalPartialFileLength) + { + Console.WriteLine("Take a file part: from: " + from + ", totalPartialFileLength: " + totalPartialFileLength); + int filePart = 0; + while(from >= totalPartialFileLength) + { + filePart++; + from -= totalPartialFileLength; + } + idM = await _ts.getMessageFile(idChannel, file.ListMessageId[filePart]); + if (idM.media is MessageMediaDocument { document: Document document2 }) + { + totalPartialFileLength = document2.size; + } + initialPartFrom = from; + } + if (from > 0) { from = (from / 524288) * 524288; @@ -315,12 +336,12 @@ public async Task GetFileStream(string idChannel, string idFile, else to = ((to + 524288) / 524288) * 524288; - if (to > totalLength) + if (to > totalPartialFileLength) { - to = totalLength; + to = totalPartialFileLength; } - if (totalLength == initialFrom) + if (totalPartialFileLength == initialPartFrom) { Response.StatusCode = 416; // Range Not Satisfiable Response.Headers["Content-Range"] = $"bytes */{totalLength}"; @@ -335,7 +356,7 @@ public async Task GetFileStream(string idChannel, string idFile, //Console.WriteLine("Total download length: " + data.Length); - long skipedBytes = initialFrom - from - (length - data.Length); + long skipedBytes = initialPartFrom - from - (length - data.Length); if (skipedBytes < 0) skipedBytes = 0; diff --git a/TelegramDownloader/TelegramDownloader.csproj b/TelegramDownloader/TelegramDownloader.csproj index f3d1bac..39718bc 100644 --- a/TelegramDownloader/TelegramDownloader.csproj +++ b/TelegramDownloader/TelegramDownloader.csproj @@ -15,13 +15,13 @@ - + - - + + From d1836227d22b50015802b585b8518aa38e75968e Mon Sep 17 00:00:00 2001 From: mateof Date: Tue, 23 Sep 2025 20:09:31 +0200 Subject: [PATCH 005/106] feat: add webdav and export files to strm (#39) feat: improves fix: error on download root files fix: error on download splitted files --- .../Controllers/FileController.cs | 14 + .../Controllers/WebDavController.cs | 81 +++++ TelegramDownloader/Data/FileService.cs | 298 +++++++++--------- TelegramDownloader/Data/IFileService.cs | 3 + TelegramDownloader/Data/ITelegramService.cs | 2 + TelegramDownloader/Data/TelegramService.cs | 142 ++++++++- TelegramDownloader/Data/db/DbService.cs | 59 +++- TelegramDownloader/Data/db/IDbService.cs | 2 + TelegramDownloader/Dockerfile | 12 + TelegramDownloader/Models/DownloadModel.cs | 6 +- TelegramDownloader/Models/FileModel.cs | 38 +++ TelegramDownloader/Models/GeneralConfig.cs | 26 ++ TelegramDownloader/Models/LoginModel.cs | 19 ++ TelegramDownloader/Pages/Config.razor | 11 + TelegramDownloader/Pages/FileManager.razor | 43 ++- .../Pages/Modals/AudioPlayerModal.razor | 2 +- .../Modals/InfoModals/MediaUrlModal.razor | 4 +- .../Pages/Partials/impl/FileManagerImpl.razor | 65 +++- .../Partials/impl/LocalFileManagerImpl.razor | 25 +- TelegramDownloader/Pages/WebDavInfo.razor | 125 ++++++++ TelegramDownloader/Services/HelperService.cs | 45 +++ .../Services/TransactionInfoService.cs | 1 + TelegramDownloader/Services/WebDavService.cs | 51 +++ TelegramDownloader/Shared/ConfigLayout.razor | 2 +- TelegramDownloader/Shared/MainLayout.razor | 1 + TelegramDownloader/TelegramDownloader.csproj | 2 +- TelegramDownloader/WebDav/requirements.txt | 3 + TelegramDownloader/WebDav/webdav_api_proxy.py | 199 ++++++++++++ 28 files changed, 1077 insertions(+), 204 deletions(-) create mode 100644 TelegramDownloader/Controllers/WebDavController.cs create mode 100644 TelegramDownloader/Pages/WebDavInfo.razor create mode 100644 TelegramDownloader/Services/WebDavService.cs create mode 100644 TelegramDownloader/WebDav/requirements.txt create mode 100644 TelegramDownloader/WebDav/webdav_api_proxy.py diff --git a/TelegramDownloader/Controllers/FileController.cs b/TelegramDownloader/Controllers/FileController.cs index 8405d82..4300b1f 100644 --- a/TelegramDownloader/Controllers/FileController.cs +++ b/TelegramDownloader/Controllers/FileController.cs @@ -267,6 +267,19 @@ public async Task GetFile(string id, string? idChannel, string? i }; } + [Route("strm")] + public async Task GetStrm([FromQuery] string idChannel, [FromQuery] string path, [FromQuery] string host) + { + String zip = await _fs.CreateStrmFiles(path, idChannel, host); + + FileStream fs = new FileStream(zip, FileMode.Open); + + return new FileStreamResult(fs, "application/octet-stream") + { + FileDownloadName = Path.GetFileName(zip) + }; + } + [Route("GetFileStream/{idChannel}/{idFile}/{name}")] public async Task GetFileStream(string idChannel, string idFile, string name) { @@ -275,6 +288,7 @@ public async Task GetFileStream(string idChannel, string idFile, var request = HttpContext.Request; var rangeHeader = request.Headers["Range"].ToString(); + Console.WriteLine("range: ", rangeHeader); var file = await _fs.getItemById(idChannel, idFile); long totalLength = file.Size; diff --git a/TelegramDownloader/Controllers/WebDavController.cs b/TelegramDownloader/Controllers/WebDavController.cs new file mode 100644 index 0000000..3c7e708 --- /dev/null +++ b/TelegramDownloader/Controllers/WebDavController.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc; +using TelegramDownloader.Data.db; +using TelegramDownloader.Data; +using TelegramDownloader.Models; +using System.Text.Json; + +namespace TelegramDownloader.Controllers +{ + [Route("api/nodes")] + [ApiController] + public class WebDavController : ControllerBase + { + IDbService _db { get; set; } + + public WebDavController(IDbService db) + { + _db = db; + } + [HttpGet] + public async Task> webDavPaths([FromQuery] string path, [FromQuery] string depth) + { + Console.WriteLine("Peticion de webDavPaths: " + path); + var isFile = Path.HasExtension(path); + if (!path.EndsWith("/") && !isFile) + path = path + "/"; + if (String.IsNullOrEmpty(path)) + throw new BadHttpRequestException("Path is null or empty"); + string channel = path.Split("/")[1]; + if (String.IsNullOrEmpty(channel)) + throw new BadHttpRequestException("Channel is empty"); + path = path.Remove(0, path.IndexOf("/", 1)); + List files = new List(); + if (isFile) + { + var bsonFile = await _db.getFileByPath(channel, path); + if (!(bsonFile == null) && bsonFile.IsFile) + { + WebDavFileModel file = bsonFile.toWebDavFileModel(channel); + files = new List(); + files.Add(file); + } + } + if ((!isFile) || files.Count == 0) + { + if (depth == "0") + { + var bsonFile = await _db.getFileByPath(channel, path[..^1]); + if (bsonFile != null) + { + WebDavFileModel file = bsonFile.toWebDavFileModel(channel); + files = new List(); + files.Add(file); + } + + } + else + files = (await _db.getAllFilesInDirectoryPath(channel, path)).Select(file => file.toWebDavFileModel()).ToList(); + } + + // Console.WriteLine(JsonSerializer.Serialize(files)); + return files; + } + + [HttpGet("meta")] + public async Task webDavMetadata([FromQuery] string path) + { + Console.WriteLine("Peticion de webDavMetadata: " + path); + if (String.IsNullOrEmpty(path)) + throw new Exception("Path is null or empty"); + string channel = path.Split("/")[1]; + if (String.IsNullOrEmpty(channel)) + throw new Exception("Channel is empty"); + path = path.Remove(0, path.IndexOf("/", 1)); + var bsonFile = await _db.getFileByPath(channel, path); + if (bsonFile == null || !bsonFile.IsFile) + throw new FileNotFoundException(); + WebDavFileModel files = bsonFile.toWebDavFileModel(channel); + return files; + } + } +} diff --git a/TelegramDownloader/Data/FileService.cs b/TelegramDownloader/Data/FileService.cs index 94c5b67..5b039f8 100644 --- a/TelegramDownloader/Data/FileService.cs +++ b/TelegramDownloader/Data/FileService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using MongoDB.Driver; using Newtonsoft.Json; +using SharpCompress.Archives; using SharpCompress.Common; using Syncfusion.Blazor.FileManager; using Syncfusion.Blazor.Inputs; @@ -17,6 +18,8 @@ using System.IO.Hashing; using System.Security.Cryptography; using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Channels; using System.Xml.Linq; using TelegramDownloader.Data.db; using TelegramDownloader.Models; @@ -28,7 +31,6 @@ namespace TelegramDownloader.Data public class FileService : IFileService { public static readonly List ImageExtensions = new List { ".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG" }; - public static readonly long MAXIMAGESIZE = 1024 * 1024 * 10; public static string IMGDIR = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "img", "telegram"); public static string LOCALDIR = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "local"); public static string RELATIVELOCALDIR = System.IO.Path.Combine("local"); @@ -230,6 +232,7 @@ public class FileService : IFileService {"xyz", "chemical/x-xyz"}, {"zip", "application/zip"} }; + public static List refreshChannelList = new List(); private PhysicalFileProvider operation = new PhysicalFileProvider(); private ITelegramService _ts { get; set; } @@ -237,6 +240,7 @@ public class FileService : IFileService private ILogger _logger { get; set; } private TransactionInfoService _tis { get; set; } private ToastService _toastService { get; set; } + private static Mutex refreshMutex = new Mutex(); const int MaxSize = 1024 * 1024 * 1000; // 1GB @@ -321,6 +325,7 @@ public async Task> ShareFile(string dbName, string bs public async Task> itemDeleteAsync(string dbName, ItemsDeleteEventArgs args) { string[] names = args.Files.Select(x => x.Name).ToArray(); + bool isMyChannel = _ts.isMyChat(Convert.ToInt64(dbName)); // args.Response = await FileManagerService.Delete(args.Path, names, args.Files.ToArray()); foreach (Syncfusion.Blazor.FileManager.FileManagerDirectoryContent File in args.Files) { @@ -339,13 +344,13 @@ public async Task> ShareFile(string dbName, string bs { foreach (int id in child.ListMessageId) { - if (!await _db.existItemByTelegramId(dbName, id)) + if (isMyChannel && !await _db.existItemByTelegramId(dbName, id)) await _ts.deleteFile(dbName, id); } } else { - if (!await _db.existItemByTelegramId(dbName, (int)child.MessageId)) + if (isMyChannel && !await _db.existItemByTelegramId(dbName, (int)child.MessageId)) await _ts.deleteFile(dbName, (int)child.MessageId); } } @@ -364,13 +369,13 @@ public async Task> ShareFile(string dbName, string bs { foreach (int id in entry.ListMessageId) { - if (!await _db.existItemByTelegramId(dbName, id)) + if (isMyChannel && !await _db.existItemByTelegramId(dbName, id)) await _ts.deleteFile(dbName, id); } } else { - if (!await _db.existItemByTelegramId(dbName, (int)entry.MessageId)) + if (isMyChannel && !await _db.existItemByTelegramId(dbName, (int)entry.MessageId)) await _ts.deleteFile(dbName, (int)entry.MessageId); } } @@ -712,8 +717,8 @@ public async Task downloadFile(string dbName, List // if (targetPath == null) targetPath = "/"; //path.Replace("\\", "/"); foreach (var itemFile in files) { - - BsonFileManagerModel file = collectionId == null ? _db.getFileByPathSync(dbName, itemFile.FilterPath.Replace("\\", "/") + itemFile.Name) : _db.getFileByPathSync(dbName, itemFile.FilterPath.Replace("\\", "/") + itemFile.Name, collectionId); + var filterPath = itemFile.FilterPath == "Files/" ? "/" : itemFile.FilterPath; + BsonFileManagerModel file = collectionId == null ? _db.getFileByPathSync(dbName, filterPath.Replace("\\", "/") + itemFile.Name) : _db.getFileByPathSync(dbName, filterPath.Replace("\\", "/") + itemFile.Name, collectionId); string currentFilePath = currentTargetPath; if (!itemFile.IsFile) { @@ -732,20 +737,7 @@ public async Task downloadFile(string dbName, List throw new Exception($"{currentTargetPath} does not exist"); if (file.isSplit) { - int i = 1; - List splitPaths = new List(); - foreach (int messageId in file.ListMessageId) - { - string filePathPart = System.IO.Path.Combine(currentFilePath, $"({i})" + itemFile.Name); - await downloadFromTelegram(channelId == null ? dbName : channelId, messageId, filePathPart, file); - splitPaths.Add(filePathPart); - i++; - } - await mergeFileStreamAsync(splitPaths, System.IO.Path.Combine(currentFilePath, itemFile.Name)); - foreach (string filePath in splitPaths) - { - File.Delete(filePath); - } + downloadSplitFiles(itemFile, file, currentFilePath, channelId == null ? dbName : channelId); } else { @@ -768,6 +760,24 @@ public async Task downloadFile(string dbName, List } + private async Task downloadSplitFiles(FileManagerDirectoryContent itemFile, BsonFileManagerModel file, string currentFilePath, string dbName) + { + int i = 1; + List splitPaths = new List(); + foreach (int messageId in file.ListMessageId) + { + string filePathPart = System.IO.Path.Combine(currentFilePath, $"({i})" + itemFile.Name); + await downloadFromTelegram(dbName, messageId, filePathPart, file, true); + splitPaths.Add(filePathPart); + i++; + } + await mergeFileStreamAsync(splitPaths, System.IO.Path.Combine(currentFilePath, itemFile.Name)); + foreach (string filePath in splitPaths) + { + File.Delete(filePath); + } + } + private async Task downloadFromTelegramAndReturn(string dbName, int messageId, string destPath, MemoryStream ms = null) { TL.Message m = await _ts.getMessageFile(dbName, messageId); @@ -793,7 +803,7 @@ public static void functionCalll(string dbName, int messageId, string destPath) { } - private async Task downloadFromTelegram(string dbName, int messageId, string destPath, BsonFileManagerModel file = null) + private async Task downloadFromTelegram(string dbName, int messageId, string destPath, BsonFileManagerModel file = null, bool shouldWait = false) { DownloadModel model = new DownloadModel(); model.tis = _tis; @@ -810,9 +820,24 @@ private async Task downloadFromTelegram(string dbName, int messageId, string des } catch(Exception ex) { model.channelName = "Public or Shared"; } - + var tcs = new TaskCompletionSource(); + + EventHandler handler = null; + handler = (sender, args) => + { + if (model.state == StateTask.Completed) + { + model.EventStatechanged -= handler; + tcs.SetResult(true); + } + }; + + model.EventStatechanged += handler; model.callbacks.callback = async () => await DownloadFileNow(dbName, messageId, destPath, model); - _tis.addToPendingDownloadList(model); + _tis.addToPendingDownloadList(model, atFirst: shouldWait); + // Espera hasta que el estado sea "Completed" + if (shouldWait) + await tcs.Task; } public async Task DownloadFileFromChat(ChatMessages message, string fileName = null, string folder = null, DownloadModel model = null) @@ -828,7 +853,7 @@ public async Task DownloadFileFromChat(ChatMessages message, string fileName = n { model._size = document.size; } - if (message.user is Channel channel) + if (message.user is TL.Channel channel) { model.channelName = channel.Title; } @@ -1145,7 +1170,43 @@ public async Task AddUploadFileFromServer(string dbName, string currentPath, Lis _tis.CheckPendingUploadInfoTasks(); } + public async Task CreateStrmFiles(string path, string dbName, string host) + { + String basePath = Path.Combine(TEMPDIR, "Strm", dbName); + try + { + Directory.Delete(basePath, true); + } + catch (Exception ex) + { + } + + Directory.CreateDirectory(basePath); + List filesAndFolders = await _db.getAllChildFilesInDirectory(dbName, path); + foreach (BsonFileManagerModel file in filesAndFolders) + { + String filePath = Path.Combine(basePath, file.FilePath.Substring(path.Length)); + if (file.IsFile) + { + if (FileExtensionTypeTest.isVideoExtension(file.Type) || FileExtensionTypeTest.isAudioExtension(file.Type)) + { + string contenido = Path.Combine(host, "api/file/GetFileStream", dbName, file.Id, "file" + file.Type).Replace("\\", "/"); + string pattern = $@"\.({file.Type.Replace(".", "")})$"; + File.WriteAllText(Regex.Replace(filePath, pattern, ".strm"), contenido); + } + } else + { + Directory.CreateDirectory(filePath); + } + } + string zipPath = $"{basePath}.zip"; + + File.Delete(zipPath); + ZipFile.CreateFromDirectory(basePath, zipPath, CompressionLevel.Fastest, true); + Directory.Delete(basePath, true); + return zipPath; + } public async Task UploadFileFromServer(string dbName, string currentPath, List files, InfoDownloadTaksModel dm = null) // ItemsUploadedEventArgs args) { @@ -1155,44 +1216,6 @@ public async Task UploadFileFromServer(string dbName, string currentPath, List await UploadFileFromServer(dbName, currentPath, files); - // foreach (var file in files) - // { - // var filePath = file.IsFile ? file.FilterPath.Replace("\\", "/") + file.Name : file.FilterPath.Replace("\\", "/") + file.Name + "/"; - // string currentFilePath = System.IO.Path.Combine(LOCALDIR, filePath[0] == '/' ? filePath.Substring(1) : filePath).Replace("\\", "/"); - - // var fileInfo = new System.IO.FileInfo(currentFilePath); - // if (!file.IsFile) - // { - // var allFiles = new DirectoryInfo(currentFilePath).GetFiles("*.*", SearchOption.AllDirectories).Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden)); - // idt.total = allFiles.Count(); - // idt.totalSize += allFiles.Sum(x => x.Length); - // } else - // { - // idt.total++; - // idt.totalSize += fileInfo.Length; - // } - - // } - // TransactionInfoService ti = new TransactionInfoService(); - // ti.addToInfoDownloadTaskList(idt); - - - //} nm.sendEvent(new Notification($"Uploading files from folder {currentPath} to Telegram", "Telegram Upload", NotificationTypes.Info)); foreach (var file in files) { @@ -1203,24 +1226,6 @@ public async Task UploadFileFromServer(string dbName, string currentPath, List !x.Attributes.HasFlag(FileAttributes.Hidden)); - // idt.total = allFiles.Count(); - // idt.totalSize = allFiles.Sum(x => x.Length); - // idt.fromPath = currentFilePath; - // idt.toPath = Path.Combine(currentPath, file.Name); - // idt.executed = 0; - // idt.executedSize = 0; - // idt.isUpload = true; - // idt.file = file; - // idt.thread = Thread.CurrentThread; - // TransactionInfoService ti = new TransactionInfoService(); - // ti.addToInfoDownloadTaskList(idt); - //} BsonFileManagerModel model = new BsonFileManagerModel(); if (file.IsFile) @@ -1322,7 +1327,7 @@ public async Task UploadFileFromServer(string dbName, string currentPath, List file.Name.ToUpper().EndsWith(x)) && file.Size >= MAXIMAGESIZE) + if (ImageExtensions.Any(x => file.Name.ToUpper().EndsWith(x)) && file.Size >= (1024 * 1024 * GeneralConfigStatic.config.MaxImageUploadSizeInMb)) { m = await _ts.uploadFile(dbName, ms, file.Name, "application/octet-stream", um, caption: getCaption(filePath)); } @@ -1384,7 +1389,7 @@ public async Task UploadFileFromServer(string dbName, string currentPath, List args) + public async Task refreshChannelFIles(string channelId) { - // string currentPath = args.Path; - try + refreshMutex.WaitOne(); + refreshChannelList.Add(channelId); + refreshMutex.ReleaseMutex(); + List telegramChatDocuments = (await _ts.searchAllChannelFiles(Convert.ToInt64(channelId))).Where(x => x.name != null).ToList(); + var nameCount = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var doc in telegramChatDocuments) { - //foreach (var file in args.Files) - //{ + var baseName = doc.name; + var name = baseName; + int count = 0; - //using (var filestream = new FileStream(Path.Combine(Environment.CurrentDirectory, "download","assss.jpg"), FileMode.Create, FileAccess.Write)) - //{ - // await file.File.OpenReadStream(maxAllowedSize: long.MaxValue).CopyToAsync(filestream); - //} - //MemoryStream ms = new MemoryStream(file.File.OpenReadStream(maxAllowedSize: long.MaxValue)); - //await file.File.OpenReadStream().CopyToAsync(ms); + while (nameCount.ContainsKey(name)) + { + count++; + name = $"{baseName}({count})"; + } + doc.name = name; + nameCount[name] = 1; + if (count == 0) + nameCount[baseName] = 1; + } + List presentIds = await _db.getAllIdsFromChannel(channelId); + var rootFolder = await _db.getRootFolder(channelId); + foreach (TelegramChatDocuments tcd in telegramChatDocuments) + { + if (!presentIds.Contains(tcd.id)) + { + BsonFileManagerModel model = new BsonFileManagerModel(); + model.Size = tcd.fileSize; + model.MessageId = tcd.id; + model.Name = tcd.name; + model.IsFile = true; + model.HasChild = false; + model.DateCreated = tcd.creationDate; + model.DateModified = tcd.modifiedDate; + model.FilterPath = "/"; + model.FilterId = rootFolder.Id + "/"; + model.ParentId = rootFolder.Id; + model.FilePath = "/" + tcd.name; + model.Type = tcd.extension; + model.isSplit = false; + model.isEncrypted = false; + await _db.createEntry(channelId, model); + } + } + refreshMutex.WaitOne(); + refreshChannelList.Remove(channelId); + refreshMutex.ReleaseMutex(); + } + + public bool isChannelRefreshing(string channelId) + { + return refreshChannelList.Contains(channelId); + } + + public async Task UploadFile(string dbName, string currentPath, UploadFiles file) // ItemsUploadedEventArgs args) + { + try + { string currentFilePath = System.IO.Path.Combine(TEMPDIR, dbName + currentPath); await SaveToFile(file.File, currentFilePath); @@ -1547,33 +1600,6 @@ public async Task UploadFile(string dbName, string currentPath, UploadFiles file await _db.addBytesToFolder(dbName, model.ParentId, model.Size); GC.Collect(); - - - - - // var folders = (file.FileInfo.Name).Split('/'); - // if (folders.Length > 1) - // { - // for (var i = 0; i < folders.Length - 1; i++) - // { - // string newDirectoryPath = Path.Combine(FileManagerService.basePath + currentPath, folders[i]); - // if (Path.GetFullPath(newDirectoryPath) != (Path.GetDirectoryName(newDirectoryPath) + Path.DirectorySeparatorChar + folders[i])) - // { - // throw new UnauthorizedAccessException("Access denied for Directory-traversal"); - // } - // if (!Directory.Exists(newDirectoryPath)) - // { - // await FileManagerService.Create(currentPath, folders[i]); - // } - // currentPath += folders[i] + "/"; - // } - // } - // var fullName = Path.Combine((FileManagerService.contentRootPath + currentPath), file.File.Name); - // using (var filestream = new FileStream(fullName, FileMode.Create, FileAccess.Write)) - // { - // await file.File.OpenReadStream(long.MaxValue).CopyToAsync(filestream); - // } - //} } catch (Exception ex) { @@ -1646,15 +1672,6 @@ public async Task> getTelegramFolders(string dbName, } } return result; - //var root = listFolders.Where(x => string.IsNullOrEmpty(x.ParentId)).FirstOrDefault(); - //FolderModel folder = new FolderModel(); - //folder.FolderName = root.Name; - //folder.Id = "/"; - //folder.Expanded = true; - //folder.Folders = await getTelegramSubfolders(root.Id, listFolders.Where(x => !string.IsNullOrEmpty(x.ParentId)).ToList(), folder.Id); - //node.Add(folder); - //return node; - } private async Task> getTelegramSubfolders(string id, List listFolders, string prevPath) @@ -1712,27 +1729,6 @@ private async Task> AddChildRecords(string id, string parent if (fileDetails == null) fileDetails = new List(); FileManagerResponse response = new FileManagerResponse(); - //if (path == "/") - //{ - // List Data = await _db.getAllFiles(); - // string ParentId = Data - // .Where(x => string.IsNullOrEmpty(x.ParentId)) - // .Select(x => x.Id).First(); - // response.CWD = Data - // .Where(x => string.IsNullOrEmpty(x.ParentId)).First().toFileManagerContent(); - // response.Files = Data - // .Where(x => x.ParentId == ParentId).Select(x => x.toFileManagerContent()).ToList(); - //} - //else - //{ - //List Data = await _db.getAllFilesInDirectoryPath(path); - // var childItem = fileDetails.Count > 0 && fileDetails[0] != null ? fileDetails[0] : Data - // .Where(x => x.FilterPath == path).First().toFileManagerContent(); - // response.CWD = childItem; - // response.Files = Data - // .Where(x => x.ParentId == childItem.Id).Select(x => x.toFileManagerContent()).ToList(); - - //} if (path == "/") { List Data = collectionName == null ? await _db.getAllFiles(dbName) : await _db.getAllFiles(dbName, collectionName); diff --git a/TelegramDownloader/Data/IFileService.cs b/TelegramDownloader/Data/IFileService.cs index faeda77..e3455ce 100644 --- a/TelegramDownloader/Data/IFileService.cs +++ b/TelegramDownloader/Data/IFileService.cs @@ -25,6 +25,7 @@ public interface IFileService Task getItemById(string dbName, string id); Task> getTelegramFolders(string dbName, string? parentId = null); Task> GetTelegramFoldersExpando(string id, string parentId); + Task CreateStrmFiles(string path, string dbName, string host); Task importData(string dbName, string path, GenericNotificationProgressModel gnp); Task importSharedData(ShareFilesModel sfm, GenericNotificationProgressModel gnp); Task> itemDeleteAsync(string dbName, ItemsDeleteEventArgs args); @@ -34,5 +35,7 @@ public interface IFileService Task UploadFile(string dbName, string currentPath, UploadFiles file); Task UploadFileFromServer(string dbName, string currentPath, List files, InfoDownloadTaksModel dm = null); Task AddUploadFileFromServer(string dbName, string currentPath, List files, InfoDownloadTaksModel idt = null); + Task refreshChannelFIles(string channelId); + bool isChannelRefreshing(string channelId); } } \ No newline at end of file diff --git a/TelegramDownloader/Data/ITelegramService.cs b/TelegramDownloader/Data/ITelegramService.cs index 71fbdf4..cc83b81 100644 --- a/TelegramDownloader/Data/ITelegramService.cs +++ b/TelegramDownloader/Data/ITelegramService.cs @@ -35,6 +35,8 @@ public interface ITelegramService Task logOff(); Task sendVerificationCode(string vc); Task uploadFile(string chatId, Stream file, string fileName, string mimeType = null, UploadModel um = null, string caption = null); + Task> searchAllChannelFiles(long id); + bool isMyChat(long id); } } \ No newline at end of file diff --git a/TelegramDownloader/Data/TelegramService.cs b/TelegramDownloader/Data/TelegramService.cs index 92f8c92..9c2411c 100644 --- a/TelegramDownloader/Data/TelegramService.cs +++ b/TelegramDownloader/Data/TelegramService.cs @@ -3,9 +3,11 @@ using Microsoft.AspNetCore.Components.Forms; using Syncfusion.Blazor.Inputs; using Syncfusion.Blazor.Kanban.Internal; +using Syncfusion.Blazor.Schedule; using Syncfusion.Blazor.Sparkline.Internal; using System.Collections.Generic; using System.Reflection.Metadata; +using System.Threading.Channels; using System.Threading.Tasks; using TelegramDownloader.Data.db; using TelegramDownloader.Models; @@ -28,13 +30,14 @@ public class TelegramService : ITelegramService private static Messages_Chats chats = null; private static List favouriteChannels = new List(); private static Mutex mut = new Mutex(); + private ILogger _logger { get; set; } - - public TelegramService(TransactionInfoService tis, IDbService db) + public TelegramService(TransactionInfoService tis, IDbService db, ILogger logger) { _tis = tis; _db = db; + _logger = logger; // createDownloadFolder(); mut.WaitOne(); if (client == null) @@ -234,11 +237,26 @@ public bool isInChat(long id) public string getChatName(long id) { - List cm = new List(); + if (chats == null) + return ""; var peer = chats.chats[id]; return peer.Title; } + public bool isMyChat(long id) + { + if (chats == null) + return false; + var peer = chats.chats[id]; + if (peer is TL.Channel channel) + { + // Si es un canal, checkea la propiedad 'creator' + bool canPost = channel.admin_rights != null; + return canPost; + } + return true; + } + public async Task> GetFouriteChannels(bool mustRefresh = true) { @@ -650,20 +668,26 @@ public async Task DownloadFileStream(Message message, long offset, int l thumb_size = "" }; - //var file = await client.Invoke(new Upload_GetFile - //{ - // location = new InputDocumentFileLocation - // { - // id = inputFile.id, - // access_hash = inputFile.access_hash, - // file_reference = inputFile.file_reference, - // thumb_size = "" - // }, - // offset = currentOffset, - // limit = totalDownloadBytes - //}); - - var file = await client.Upload_GetFile(location, currentOffset, limit: totalDownloadBytes); + Upload_FileBase file = null; + try + { + file = await client.Upload_GetFile(location, currentOffset, limit: totalDownloadBytes); + } + catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X") + { + client = await client.GetClientForDC(-ex.X, true); + file = await client.Upload_GetFile(location, currentOffset, limit: totalDownloadBytes); + } + catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID") + { + _logger.LogError("OFFSET_INVALID", ex); + throw; + } + catch (Exception ex) + { + _logger.LogError("Download Error", ex); + throw; + } if (file is Upload_File uploadFile) { @@ -794,5 +818,89 @@ public async Task DownloadFile(ChatMessages message, string fileName = n return null; } + public async Task> searchAllChannelFiles(long id) + { + List telegramChatDocuments = new List(); + List result = await getAllFileMessages(id); + + foreach (var msg in result) + if (msg.message is Message msgBase) + { + if (msgBase.media is MessageMediaDocument mediaDoc && + mediaDoc.document is TL.Document doc) + { + TelegramChatDocuments tcd = new TelegramChatDocuments(); + tcd.id = msgBase.id; + tcd.documentType = DocumentType.Document; + tcd.name = doc.Filename; + tcd.fileSize = doc.size; + tcd.extension = Path.GetExtension(doc.Filename); + tcd.creationDate = msgBase.date; + tcd.modifiedDate = msgBase.edit_date < msgBase.date ? msgBase.date : msgBase.edit_date; + telegramChatDocuments.Add(tcd); + } + } + return telegramChatDocuments; + } + + //public async Task> searchAllChannelFiles(long id) + //{ + // InputPeer channel = chats.chats[id]; + // List telegramChatDocuments = new List(); + // for (int offset_id = 0; ;) + // { + // Messages_MessagesBase result = await client.Messages_Search( + // peer: channel, + // text: "", + // offset_id: offset_id, + // limit: 0); + // if (result.Messages.Length == 0) break; + // foreach (var msgBase in result.Messages.OfType()) + // { + // if (msgBase.media is MessageMediaDocument mediaDoc && + // mediaDoc.document is TL.Document doc) + // { + // TelegramChatDocuments tcd = new TelegramChatDocuments(); + // tcd.id = msgBase.id; + // tcd.documentType = DocumentType.Document; + // tcd.name = doc.Filename; + // tcd.extension = Path.GetExtension(doc.Filename); + // tcd.creationDate = msgBase.date; + // tcd.modifiedDate = msgBase.edit_date; + // telegramChatDocuments.Add(tcd); + // } + // } + // offset_id = result.Messages[^1].ID; + // } + + // for (int offset_id = 0; ;) + // { + // Messages_MessagesBase result = await client.Messages_Search( + // peer: channel, + // text: "", + // offset_id: offset_id, + // limit: 0); + // if (result.Messages.Length == 0) break; + // foreach (var msgBase in result.Messages.OfType()) + // { + // if (msgBase.media is MessageMediaPhoto mediaPhoto && + // mediaPhoto.photo is Photo photo) + // { + // TelegramChatDocuments tcd = new TelegramChatDocuments(); + // tcd.documentType = DocumentType.Photo; + // tcd.id = msgBase.id; + // // Nombre "falso" porque la foto no trae filename + // tcd.name = $"{photo.id}.jpg"; + // tcd.extension = ".jpg"; + // tcd.creationDate = msgBase.date; + // tcd.modifiedDate = msgBase.edit_date; + // telegramChatDocuments.Add(tcd); + // } + // } + // offset_id = result.Messages[^1].ID; + // } + + // return telegramChatDocuments; + //} } } diff --git a/TelegramDownloader/Data/db/DbService.cs b/TelegramDownloader/Data/db/DbService.cs index 85f6086..42370a7 100644 --- a/TelegramDownloader/Data/db/DbService.cs +++ b/TelegramDownloader/Data/db/DbService.cs @@ -6,6 +6,8 @@ using MongoDB.Driver.Linq; using System.Collections; using Syncfusion.Blazor.PivotView; +using Microsoft.Extensions.Options; +using Syncfusion.EJ2.Layouts; namespace TelegramDownloader.Data.db { @@ -34,15 +36,33 @@ public async Task getSession() public IMongoDatabase getDatabase(string dbName) { // currentDatabase = client.GetDatabase(dbName); + return client.GetDatabase(dbName); } public async Task createIndex(string dbName, string collectionName = "directory") { if (collectionName == null) collectionName = "directory"; - var options = new CreateIndexOptions() { Unique = true, Name = "uniquefile" }; - var indexKeysDefinition = Builders.IndexKeys.Ascending(x => x.FilePath); - await getDatabase(dbName).GetCollection(collectionName).Indexes.CreateOneAsync(indexKeysDefinition, options); + var collection = getDatabase(dbName).GetCollection(collectionName); + + var indexes = await collection.Indexes.ListAsync(); + var indexList = await indexes.ToListAsync(); + + bool existsFilePath = indexList.Any(i => i["name"] == "uniquefile"); + bool existsFilterPath = indexList.Any(i => i["name"] == "uniquefilterpath"); + + if (!existsFilePath) + { + var options = new CreateIndexOptions() { Unique = true, Name = "uniquefile" }; + var indexKeysDefinition = Builders.IndexKeys.Ascending(x => x.FilePath); + await collection.Indexes.CreateOneAsync(indexKeysDefinition, options); + } + if (!existsFilterPath) + { + var options2 = new CreateIndexOptions() { Unique = false, Name = "uniquefilterpath" }; + var indexKeysDefinition2 = Builders.IndexKeys.Ascending(x => x.FilterPath); + await collection.Indexes.CreateOneAsync(indexKeysDefinition2, options2); + } } public async Task CreateDatabase(string dbName = "default", string collection = "directory", bool CreateDefaultEntry = true) @@ -294,7 +314,7 @@ public async Task> getAllFiles(string dbName, string { if (collectionName == null) collectionName = "directory"; - return await (await getDatabase(dbName).GetCollection(collectionName).FindAsync(Builders.Filter.Eq(x => x.FilterPath, "/") | Builders.Filter.Eq(x => x.FilterId, ""))).ToListAsync(); + return await (await getDatabase(dbName).GetCollection(collectionName).FindAsync(Builders.Filter.Eq(x => x.FilterPath, "/") | Builders.Filter.Eq(x => x.FilterPath, "Files/") | Builders.Filter.Eq(x => x.FilterId, ""))).ToListAsync(); } public async Task> getAllFilesInDirectory(string dbName, string path, string collectionName = "directory") @@ -349,6 +369,30 @@ public async Task> getAllFolders(string dbName, strin //return await (await getDatabase(dbName).GetCollection(collectionName).FindAsync(Builders.Filter.Where(x => !x.IsFile && x.ParentId == parentId))).ToListAsync(); } + public async Task> getAllIdsFromChannel(string dbName, string collectionName = "directory") + { + if (collectionName == null) + collectionName = "directory"; + var collection = getDatabase(dbName).GetCollection(collectionName); + + var items = await collection + .Find(x => x.IsFile) + .Project(x => x.MessageId) + .ToListAsync(); + + var allMessageIds = new HashSet(); + foreach (var item in items) + { + if (item != null) + allMessageIds.Add(item.Value); + + //if (item != null) + // allMessageIds.UnionWith(item); + } + + return allMessageIds.OrderBy(x => x).ToList(); + } + public async Task> getAllFilesInDirectoryById(string dbName, string idFolder, string collectionName = "directory") { if (collectionName == null) @@ -486,6 +530,13 @@ public async Task> createEntry(string dbName, BsonFil return null; } + public async Task getRootFolder(string dbName, string collectionName = "directory") + { + if (collectionName == null) + collectionName = "directory"; + return await (await getDatabase(dbName).GetCollection(collectionName).FindAsync(Builders.Filter.Where(x => x.ParentId == "" && x.Name == "Files"))).FirstOrDefaultAsync(); + } + public async Task toBasonFile(string Path, string FolderName, FileManagerDirectoryContent ParentFolder) diff --git a/TelegramDownloader/Data/db/IDbService.cs b/TelegramDownloader/Data/db/IDbService.cs index 88db984..243657f 100644 --- a/TelegramDownloader/Data/db/IDbService.cs +++ b/TelegramDownloader/Data/db/IDbService.cs @@ -17,6 +17,7 @@ public interface IDbService Task copyItem(string dbName, string sourceId, FileManagerDirectoryContent target, string targetPath, bool isFile, string collectionName = "directory"); Task CreateDatabase(string dbName = "default", string collection = "directory", bool CreateDefaultEntry = true); Task> createEntry(string dbName, BsonFileManagerModel file, string collectionName = "directory", IClientSessionHandle? session = null); + Task getRootFolder(string dbName, string collectionName = "directory"); Task createIndex(string dbName, string collectionName = "directory"); Task deleteDatabase(string dbName = "default"); Task deleteEntry(string dbName, string id, string collectionName = "directory"); @@ -30,6 +31,7 @@ public interface IDbService Task> getAllFilesInDirectoryPath(string dbName, string path, string collectionName = "directory"); Task> getAllFilesInDirectoryPath2(string dbName, string path, string collectionName = "directory"); Task> getAllFolders(string dbName, string? parentId = null, string collectionName = "directory"); + Task> getAllIdsFromChannel(string dbName, string collectionName = "directory"); IMongoDatabase getDatabase(string dbName); Task getEntry(string dbName, string filterId, string name, string collectionName = "directory"); Task getFileById(string dbName, string id, string collectionName = "directory"); diff --git a/TelegramDownloader/Dockerfile b/TelegramDownloader/Dockerfile index fca714e..7aeebd8 100644 --- a/TelegramDownloader/Dockerfile +++ b/TelegramDownloader/Dockerfile @@ -21,5 +21,17 @@ RUN dotnet publish "./TelegramDownloader.csproj" -c $BUILD_CONFIGURATION -o /app FROM base AS final WORKDIR /app + +# Instala Python y pip +RUN apt-get update && \ + apt-get install -y python3 python3-pip && \ + ln -s /usr/bin/python3 /usr/bin/python && \ + rm -rf /var/lib/apt/lists/* + +# Copia y instala los requirements de WebDav +COPY WebDav /app/WebDav +RUN python3 -m pip install --upgrade pip && \ + python3 -m pip install -r /app/WebDav/requirements.txt + COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "TelegramDownloader.dll"] diff --git a/TelegramDownloader/Models/DownloadModel.cs b/TelegramDownloader/Models/DownloadModel.cs index c1dcd32..4ee5453 100644 --- a/TelegramDownloader/Models/DownloadModel.cs +++ b/TelegramDownloader/Models/DownloadModel.cs @@ -119,6 +119,7 @@ public class DownloadModel { public Mutex mutex = new Mutex(); public event EventHandler EventChanged; + public event EventHandler EventStatechanged; public string id = Guid.NewGuid().ToString(); public string action { get; set; } = "Download"; public StateTask state { get; set; } = StateTask.Working; @@ -145,13 +146,14 @@ public DownloadModel(string? name = null) id = Guid.NewGuid().ToString() + ":" + name; } + // Fix for CS7036: Ensure the required "sender" parameter is passed when invoking the EventHandler. + public void ProgressCallback(long transmitted, long totalSize) { if (state == StateTask.Canceled) throw new Exception($"Canceled {name}"); if (state == StateTask.Paused) { - state = StateTask.Working; tis.deleteDownloadInList(this); throw new Exception($"Paused {name}"); @@ -165,11 +167,11 @@ public void ProgressCallback(long transmitted, long totalSize) if (transmitted == totalSize) { state = StateTask.Completed; + EventStatechanged?.Invoke(this, EventArgs.Empty); NotificationModel nm = new NotificationModel(); nm.sendEvent(new Notification($"Download {name} completed", "Download Completed", NotificationTypes.Success)); tis.CheckPendingDownloads(); } - } public void Cancel() diff --git a/TelegramDownloader/Models/FileModel.cs b/TelegramDownloader/Models/FileModel.cs index 10f37b4..d74906c 100644 --- a/TelegramDownloader/Models/FileModel.cs +++ b/TelegramDownloader/Models/FileModel.cs @@ -4,6 +4,8 @@ using TelegramDownloader.Data.db; using Syncfusion.EJ2.Linq; using TL; +using System.Text.Json.Serialization; +using TelegramDownloader.Data; namespace TelegramDownloader.Models { @@ -168,6 +170,42 @@ public FileDetails toFileDetails() }; } + public WebDavFileModel toWebDavFileModel(String? channel = null) + { + return this.IsFile + ? new WebDavFileModel() + { + name = this.Name, + is_dir = false, + file_id = this.Id, + content_type = FileService.getMimeType(this.Type), + content_length = this.Size, + channel = channel, + last_modified = this.DateModified + } + : new WebDavFileModel() + { + name = this.Name, + is_dir = true, + last_modified = this.DateModified + }; + } + } + + public class WebDavFileModel + { + public string name { get; set; } + public bool is_dir { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string file_id { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string content_type { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long content_length { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? channel { get; set; } + public DateTime last_modified { get; set; } + } public class FileManagerModel diff --git a/TelegramDownloader/Models/GeneralConfig.cs b/TelegramDownloader/Models/GeneralConfig.cs index 9b04aca..9eb05f1 100644 --- a/TelegramDownloader/Models/GeneralConfig.cs +++ b/TelegramDownloader/Models/GeneralConfig.cs @@ -7,6 +7,8 @@ using Newtonsoft.Json; using Syncfusion.Blazor.Charts; using TelegramDownloader.Data; +using System.Diagnostics; +using TelegramDownloader.Services; namespace TelegramDownloader.Models { @@ -72,10 +74,15 @@ public class GeneralConfig public int SplitSize { get; set; } = 0; public int MaxSimultaneousDownloads = 1; public bool CheckHash { get; set; } = false; + /// + /// Maximum image upload size in megabytes. if 0, All images will be sent as a file + /// + public int MaxImageUploadSizeInMb { get; set; } = 10; public bool ShouldShowCaptionPath { get; set; } = false; public bool ShouldShowLogInTerminal { get; set; } = false; public bool ShouldShowPaginatedFileChannel { get; set; } = false; public List FavouriteChannels { get; set; } = new List(); + public WebDavModel webDav { get; set; } = new WebDavModel(); } @@ -86,4 +93,23 @@ public class TLConfig public string? mongo_connection_string { get; set; } public bool? avoid_checking_certificate { get; set; } } + + public class WebDavModel + { + public string Host { get; set; } = "127.0.0.1"; + public int PuertoEntrada { get; set; } = 8080; + public int PuertoSalida { get; set; } = 9081; + [BsonIgnore] + public WebbDavService? webDavService { get; set; } = new WebbDavService(); + + public void start() + { + webDavService.Start(port: PuertoEntrada, externalPort: PuertoSalida, host: Host); + } + + public void stop() + { + webDavService.Stop(); + } + } } diff --git a/TelegramDownloader/Models/LoginModel.cs b/TelegramDownloader/Models/LoginModel.cs index cad6ee7..7afeda8 100644 --- a/TelegramDownloader/Models/LoginModel.cs +++ b/TelegramDownloader/Models/LoginModel.cs @@ -32,4 +32,23 @@ public class ChatMessages public string videoThumb { get; set; } public bool isDocument { get; set; } } + + public class TelegramChatDocuments + { + public int id { get; set; } + public string name { get; set; } + public string extension { get; set; } + public DateTime creationDate { get; set; } + public DateTime modifiedDate { get; set; } + public long fileSize { get; set; } + public DocumentType documentType { get; set; } + } + + public enum DocumentType + { + Photo, + Video, + Audio, + Document + } } diff --git a/TelegramDownloader/Pages/Config.razor b/TelegramDownloader/Pages/Config.razor index 0390990..75eb010 100644 --- a/TelegramDownloader/Pages/Config.razor +++ b/TelegramDownloader/Pages/Config.razor @@ -80,6 +80,17 @@ +
+
+ +
+ + +
+ +
+
+
@if (TelegramService.isPremium) diff --git a/TelegramDownloader/Pages/FileManager.razor b/TelegramDownloader/Pages/FileManager.razor index 9bc58ef..a0b3f85 100644 --- a/TelegramDownloader/Pages/FileManager.razor +++ b/TelegramDownloader/Pages/FileManager.razor @@ -3,7 +3,9 @@ @using TelegramDownloader.Data @using TelegramDownloader.Data.db @using TelegramDownloader.Models +@using TelegramDownloader.Pages.Modals.InfoModals @using TelegramDownloader.Pages.Partials +@using TelegramDownloader.Services @inject IFileService fs @inject ITelegramService ts @inject ILogger Logger @@ -12,11 +14,11 @@ @@ -29,6 +31,16 @@ Import + @if (webDavService.IsRunning) + { + + } + + @if (!isMyChannel && showRefresh) + { + + } + - + @code { private Modals.ImportDataModal ImportModal { get; set; } + private TelegramDownloader.Pages.Partials.impl.FileManagerImpl fileManagerImpl { get; set; } = default!; [Parameter] public string id { get; set; } private MediaUrlModal mediaUrlModal { get; set; } = default!; @@ -141,6 +142,12 @@ try { await fs.refreshChannelFIles(id); + + // Refresh the file manager to show updated data + if (fileManagerImpl != null) + { + await fileManagerImpl.RefreshFileManager(); + } } finally { diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.css b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.css index c082903..cf7c889 100644 --- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.css +++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.css @@ -259,6 +259,14 @@ -webkit-overflow-scrolling: touch; scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE/Edge */ + transition: left 0.3s ease; +} + +/* On wide screens where sidebar is on the side, account for sidebar width */ +@media (min-width: 2049px) { + .mfm-bottom-bar { + left: max(280px, 20%); /* Match sidebar min-width and width */ + } } .mfm-bottom-bar::-webkit-scrollbar { @@ -794,7 +802,14 @@ left: 0; right: 0; z-index: 99; - transition: bottom 0.2s ease; + transition: bottom 0.2s ease, left 0.3s ease; +} + +/* On wide screens where sidebar is on the side, account for sidebar width */ +@media (min-width: 2049px) { + .mfm-pagination { + left: max(280px, 20%); /* Match sidebar min-width and width */ + } } /* Move pagination up when action bar is visible */ From 59d1939aea22464033bdfad16db113aa3519cd78 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Mon, 15 Dec 2025 16:26:13 +0100 Subject: [PATCH 084/106] feat: improve split files --- TelegramDownloader/Data/FileService.cs | 125 +++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 10 deletions(-) diff --git a/TelegramDownloader/Data/FileService.cs b/TelegramDownloader/Data/FileService.cs index 091f4f3..f1babbb 100644 --- a/TelegramDownloader/Data/FileService.cs +++ b/TelegramDownloader/Data/FileService.cs @@ -386,7 +386,11 @@ public async Task> ShareFile(string dbName, string bs await _db.subBytesToFolder(dbName, entry.ParentId, entry.Size); } - await _db.checkAndSetDirectoryHasChild(dbName, args.Files.FirstOrDefault().ParentId); + var firstFile = args.Files.FirstOrDefault(); + if (firstFile != null) + { + await _db.checkAndSetDirectoryHasChild(dbName, firstFile.ParentId); + } return await GetFilesPath(dbName, args.Path); } @@ -1903,11 +1907,25 @@ private async Task> AddChildRecords(string id, string parent if (path == "/") { List Data = collectionName == null ? await _db.getAllFiles(dbName) : await _db.getAllFiles(dbName, collectionName); - string ParentId = Data - .Where(x => string.IsNullOrEmpty(x.ParentId)) - .Select(x => x.Id).First(); - response.CWD = Data - .Where(x => string.IsNullOrEmpty(x.ParentId)).First().toFileManagerContent(); + + // Find root element (element with no ParentId) + var rootElement = Data.FirstOrDefault(x => string.IsNullOrEmpty(x.ParentId)); + + if (rootElement == null) + { + // No root element found, return empty response + response.CWD = new Syncfusion.Blazor.FileManager.FileManagerDirectoryContent + { + Name = "Root", + FilterPath = "/", + IsFile = false + }; + response.Files = new List(); + return response; + } + + string ParentId = rootElement.Id; + response.CWD = rootElement.toFileManagerContent(); response.Files = Data .Where(x => x.ParentId == ParentId).Select(x => x.toFileManagerContent()).ToList(); } @@ -2038,11 +2056,22 @@ private async Task SaveToFile(IBrowserFile file, string path) private async Task> splitFileStreamAsync(string path, string name, int splitSize) { + var originalFileInfo = new System.IO.FileInfo(System.IO.Path.Combine(path, name)); + long originalFileSize = originalFileInfo.Length; + _logger.LogInformation("Starting file split - FileName: {FileName}, SizeMB: {SizeMB:F2}, SplitSizeMB: {SplitSizeMB}", - name, new System.IO.FileInfo(System.IO.Path.Combine(path, name)).Length / (1024.0 * 1024.0), splitSize / (1024 * 1024)); + name, originalFileSize / (1024.0 * 1024.0), splitSize / (1024 * 1024)); + int _gb = TelegramService.splitSizeGB; + long partSize = (long)splitSize * _gb; // Size of each part in bytes + int expectedParts = (int)Math.Ceiling((double)originalFileSize / partSize); + + // Check for existing split parts from a previous failed run + var existingParts = CheckExistingSplitParts(path, name, originalFileSize, partSize, expectedParts); + List listPath = new List(); byte[] buffer = new byte[splitSize]; + using (FileStream fs = new FileStream(System.IO.Path.Combine(path, name), FileMode.Open, FileAccess.Read, FileShare.Read)) { SplitModel sm = new SplitModel(); @@ -2051,13 +2080,33 @@ private async Task> splitFileStreamAsync(string path, string name, sm._transmitted = 0; _tis.addToUploadList(sm); int index = 1; + while (fs.Position < fs.Length) { string partPath = System.IO.Path.Combine(path, $"({index})" + name); + + // Check if this part already exists and is valid + if (existingParts.TryGetValue(index, out var existingPartInfo) && existingPartInfo.isValid) + { + _logger.LogInformation("Reusing existing split part {PartNumber}/{TotalParts} - Size: {SizeMB:F2} MB", + index, expectedParts, existingPartInfo.size / (1024.0 * 1024.0)); + + // Skip this part in the source file + fs.Position += existingPartInfo.size; + listPath.Add(partPath); + sm.ProgressCallback(fs.Position, fs.Length); + index++; + continue; + } + + // Delete existing invalid part if exists if (File.Exists(partPath)) { + _logger.LogDebug("Deleting invalid existing part: {PartPath}", partPath); File.Delete(partPath); } + + // Create new part int gb = _gb; while (fs.Position < fs.Length && gb > 0) { @@ -2070,16 +2119,72 @@ private async Task> splitFileStreamAsync(string path, string name, sm.ProgressCallback(fs.Position, fs.Length); } - - // await fs.CopyToAsync(fsPart, Convert.ToInt32(Math.Min(splitSize, fs.Length - fs.Position))); listPath.Add(partPath); index++; } } - _logger.LogInformation("File split completed - FileName: {FileName}, Parts: {PartsCount}", name, listPath.Count); + + int reusedParts = existingParts.Count(p => p.Value.isValid); + _logger.LogInformation("File split completed - FileName: {FileName}, Parts: {PartsCount}, ReusedParts: {ReusedParts}", + name, listPath.Count, reusedParts); return listPath; } + /// + /// Checks for existing split parts from a previous failed run and validates their sizes + /// + private Dictionary CheckExistingSplitParts(string path, string name, long originalFileSize, long partSize, int expectedParts) + { + var existingParts = new Dictionary(); + + for (int i = 1; i <= expectedParts; i++) + { + string partPath = System.IO.Path.Combine(path, $"({i})" + name); + if (File.Exists(partPath)) + { + var partInfo = new System.IO.FileInfo(partPath); + long actualSize = partInfo.Length; + + // Calculate expected size for this part + long expectedSize; + if (i < expectedParts) + { + // Not the last part - should be full partSize + expectedSize = partSize; + } + else + { + // Last part - calculate remaining size + expectedSize = originalFileSize - (partSize * (expectedParts - 1)); + } + + bool isValid = actualSize == expectedSize; + + existingParts[i] = (actualSize, isValid); + + if (isValid) + { + _logger.LogDebug("Found valid existing part {PartNumber}/{TotalParts} - Size: {SizeMB:F2} MB", + i, expectedParts, actualSize / (1024.0 * 1024.0)); + } + else + { + _logger.LogWarning("Found invalid existing part {PartNumber}/{TotalParts} - Expected: {ExpectedMB:F2} MB, Actual: {ActualMB:F2} MB", + i, expectedParts, expectedSize / (1024.0 * 1024.0), actualSize / (1024.0 * 1024.0)); + } + } + } + + if (existingParts.Any()) + { + int validCount = existingParts.Count(p => p.Value.isValid); + _logger.LogInformation("Found {TotalExisting} existing split parts, {ValidCount} are valid and will be reused", + existingParts.Count, validCount); + } + + return existingParts; + } + private async Task mergeFileStreamAsync(List pathList, string destName) { _logger.LogInformation("Starting file merge - DestName: {DestName}, PartsCount: {PartsCount}", destName, pathList.Count); From d4f4a99b0b83c2c6b997d1b97a952add005bdb62 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Mon, 15 Dec 2025 16:42:18 +0100 Subject: [PATCH 085/106] feat: add system restart --- TelegramDownloader/Pages/Config.razor | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/TelegramDownloader/Pages/Config.razor b/TelegramDownloader/Pages/Config.razor index ab6d011..2261b5b 100644 --- a/TelegramDownloader/Pages/Config.razor +++ b/TelegramDownloader/Pages/Config.razor @@ -3,11 +3,13 @@ @using TelegramDownloader.Data.db @using TelegramDownloader.Models @using TelegramDownloader.Services +@using Microsoft.Extensions.Hosting @implements IDisposable @inject IDbService db @inject ToastService toastService @inject TransactionInfoService tis +@inject IHostApplicationLifetime applicationLifetime + + +Login - Telegram File Manager + + @@ -229,7 +229,7 @@ @(metrics?.AppMemory ?? "0 B")
-
+
@@ -238,7 +238,7 @@ @(metrics?.UsedMemory ?? "0 B") / @(metrics?.TotalMemory ?? "0 B")
-
+
@@ -262,7 +262,7 @@ @(metrics?.TempFolderSize ?? "0 B")
-
+
@@ -271,7 +271,7 @@ @(metrics?.DiskUsed ?? "0 B") / @(metrics?.DiskTotal ?? "0 B")
-
75 ? "warning" : "")" style="width: @(metrics?.DiskUsagePercent ?? 0)%">
+
75 ? "warning" : "")" style="width: @(metrics?.DiskUsagePercentCss ?? "0")%">
diff --git a/TelegramDownloader/wwwroot/js/phonecodes.js b/TelegramDownloader/wwwroot/js/phonecodes.js index 6bada16..45ecfb0 100644 --- a/TelegramDownloader/wwwroot/js/phonecodes.js +++ b/TelegramDownloader/wwwroot/js/phonecodes.js @@ -9,4 +9,15 @@ function loadCountries() { function getNumber() { return iti.getNumber(); +} + +function focusElement(elementId) { + const element = document.getElementById(elementId); + if (element) { + element.focus(); + // Also select all text if it's an input + if (element.select) { + element.select(); + } + } } \ No newline at end of file From f1c377368c0b62aaa079971da44bab091b7ee252 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Wed, 17 Dec 2025 09:59:16 +0100 Subject: [PATCH 100/106] feat: improve webdav info --- TelegramDownloader/Pages/WebDavInfo.razor | 176 +++++---- TelegramDownloader/Pages/WebDavInfo.razor.css | 369 ++++++++++++++++++ 2 files changed, 474 insertions(+), 71 deletions(-) create mode 100644 TelegramDownloader/Pages/WebDavInfo.razor.css diff --git a/TelegramDownloader/Pages/WebDavInfo.razor b/TelegramDownloader/Pages/WebDavInfo.razor index fad0444..f61ea58 100644 --- a/TelegramDownloader/Pages/WebDavInfo.razor +++ b/TelegramDownloader/Pages/WebDavInfo.razor @@ -1,5 +1,4 @@ -@page "/webdavinfo" -@* @model TelegramDownloader.Pages.WebDavInfoModel *@ +@page "/webdavinfo" @using Microsoft.AspNetCore.Components.Forms @using TelegramDownloader.Data.db @using TelegramDownloader.Models @@ -7,42 +6,110 @@ @inject IDbService db @inject ToastService toastService +WebDAV Configuration + @if(model != null) { - -
-
-
WebDAV
-
-
- - -
-
- - -
-
- - +
+ +
+

WebDAV Server

+

Configure your WebDAV server to access Telegram files from any file manager

+
+ + +
+ +
+
+ +

Configuration

+
+
+ +
+ + +
Use 0.0.0.0 to allow external connections
+
+ +
+ + +
Telegram File Manager internal port
+
+ +
+ + +
Port exposed for WebDAV connections
+
+ + +
+ +
+
+ + How to connect +
+

+ Use any WebDAV client (Windows Explorer, macOS Finder, Cyberduck, etc.) + and connect to the WebDAV URL shown in the status panel. +

-
-
- -
-
-
- -
-
-

@(status == StatusColor.Rojo ? "Stopped" : "Active")

-
-
- - - + +
+
+ +

Server Status

+
+
+ +
+
+ +
+
+ + +
+

@(IsRunning ? "Running" : "Stopped")

+

@(IsRunning ? "WebDAV server is accepting connections" : "Server is not running")

+
+ + +
+ + + +
+ + + @if(IsRunning) + { +
+
Connection URL
+
+ http://@(model.webDav.Host):@(model.webDav.PuertoSalida)/ +
+
+ } +
@@ -51,75 +118,42 @@ @code { private GeneralConfig model { get; set; } - - private StatusColor status = StatusColor.Rojo; + private bool IsRunning => model?.webDav?.webDavService?.IsRunning ?? false; protected override async Task OnInitializedAsync() { - model = null; await loadModel(); - } private async Task loadModel() { model ??= await GeneralConfigStatic.Load(db); - checkStatusColor(); await InvokeAsync(StateHasChanged); } private async Task OnSave() { await GeneralConfigStatic.SaveChanges(db, model); - toastService.Notify(new(ToastType.Success, $"The webdav configuration has been saved") { Title = "Success", AutoHide = true }); + toastService.Notify(new(ToastType.Success, $"WebDAV configuration has been saved") { Title = "Success", AutoHide = true }); } private async Task Start() { model.webDav.start(); - checkStatusColor(); await InvokeAsync(StateHasChanged); } + private async Task Stop() { model.webDav.stop(); - checkStatusColor(); await InvokeAsync(StateHasChanged); } + private async Task Reset() { model.webDav.stop(); - checkStatusColor(); - await InvokeAsync(StateHasChanged); + await Task.Delay(500); model.webDav.start(); - checkStatusColor(); await InvokeAsync(StateHasChanged); } - - private string GetStatusColor() => status switch - { - StatusColor.Verde => "green", - StatusColor.Amarillo => "yellow", - StatusColor.Rojo => "red", - _ => "gray" - }; - - private void checkStatusColor() - { - if (model.webDav.webDavService.IsRunning) - { - status = StatusColor.Verde; - } else - { - status = StatusColor.Rojo; - } - - } - - private enum StatusColor - { - Verde, - Amarillo, - Rojo - } } diff --git a/TelegramDownloader/Pages/WebDavInfo.razor.css b/TelegramDownloader/Pages/WebDavInfo.razor.css new file mode 100644 index 0000000..a17cfca --- /dev/null +++ b/TelegramDownloader/Pages/WebDavInfo.razor.css @@ -0,0 +1,369 @@ +/* ===== WebDAV Configuration Page Styles ===== */ + +.webdav-page { + max-width: 800px; + margin: 0 auto; + padding: 2rem 1rem; +} + +/* Page Header */ +.webdav-header { + text-align: center; + margin-bottom: 2rem; +} + +.webdav-header h1 { + color: #fff; + font-size: 1.75rem; + font-weight: 600; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.webdav-header h1 i { + color: #0088cc; +} + +.webdav-header p { + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; +} + +/* Cards Grid */ +.webdav-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +@media (max-width: 768px) { + .webdav-grid { + grid-template-columns: 1fr; + } +} + +/* Card Base */ +.webdav-card { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + overflow: hidden; +} + +.webdav-card-header { + background: linear-gradient(135deg, rgba(0, 136, 204, 0.2) 0%, rgba(0, 136, 204, 0.05) 100%); + padding: 1rem 1.25rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.webdav-card-header i { + font-size: 1.25rem; + color: #0088cc; +} + +.webdav-card-header h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #fff; +} + +.webdav-card-body { + padding: 1.5rem; +} + +/* Form Styles */ +.form-group { + margin-bottom: 1.25rem; +} + +.form-group:last-of-type { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + color: rgba(255, 255, 255, 0.8); + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.form-group label i { + margin-right: 0.5rem; + color: rgba(255, 255, 255, 0.5); +} + +.form-group ::deep input, +.form-group ::deep .form-control { + width: 100%; + background: rgba(255, 255, 255, 0.05) !important; + border: 1px solid rgba(255, 255, 255, 0.15) !important; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + color: #fff !important; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.form-group ::deep input:focus, +.form-group ::deep .form-control:focus { + outline: none; + border-color: #0088cc !important; + background: rgba(255, 255, 255, 0.08) !important; + box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.2); +} + +.form-group ::deep input::placeholder { + color: rgba(255, 255, 255, 0.4); +} + +/* Input hint */ +.input-hint { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.4); + margin-top: 0.375rem; +} + +/* Save Button */ +.btn-save { + width: 100%; + background: linear-gradient(135deg, #0088cc 0%, #0066aa 100%); + border: none; + border-radius: 0.5rem; + padding: 0.75rem 1.5rem; + color: #fff; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.btn-save:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 136, 204, 0.3); +} + +.btn-save:active { + transform: translateY(0); +} + +/* Status Card */ +.status-card { + display: flex; + flex-direction: column; + height: 100%; +} + +.status-card .webdav-card-body { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +/* Status Indicator */ +.status-indicator { + position: relative; + margin-bottom: 1.5rem; +} + +.status-circle { + width: 80px; + height: 80px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + transition: all 0.3s ease; +} + +.status-circle.active { + background: rgba(16, 185, 129, 0.2); + border: 3px solid #10b981; + color: #10b981; + box-shadow: 0 0 30px rgba(16, 185, 129, 0.3); +} + +.status-circle.stopped { + background: rgba(239, 68, 68, 0.2); + border: 3px solid #ef4444; + color: #ef4444; + box-shadow: 0 0 30px rgba(239, 68, 68, 0.2); +} + +.status-circle.active::after { + content: ''; + position: absolute; + inset: -5px; + border-radius: 50%; + border: 2px solid rgba(16, 185, 129, 0.3); + animation: pulse-ring 2s ease-out infinite; +} + +@keyframes pulse-ring { + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(1.3); + opacity: 0; + } +} + +/* Status Text */ +.status-text { + margin-bottom: 1.5rem; +} + +.status-text h4 { + margin: 0 0 0.25rem 0; + font-size: 1.25rem; + font-weight: 600; +} + +.status-text.active h4 { + color: #10b981; +} + +.status-text.stopped h4 { + color: #ef4444; +} + +.status-text p { + margin: 0; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); +} + +/* Control Buttons */ +.control-buttons { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + justify-content: center; +} + +.btn-control { + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; + border: none; +} + +.btn-control.start { + background: rgba(16, 185, 129, 0.2); + border: 1px solid rgba(16, 185, 129, 0.3); + color: #10b981; +} + +.btn-control.start:hover { + background: rgba(16, 185, 129, 0.3); + border-color: rgba(16, 185, 129, 0.5); + transform: translateY(-2px); +} + +.btn-control.stop { + background: rgba(245, 158, 11, 0.2); + border: 1px solid rgba(245, 158, 11, 0.3); + color: #f59e0b; +} + +.btn-control.stop:hover { + background: rgba(245, 158, 11, 0.3); + border-color: rgba(245, 158, 11, 0.5); + transform: translateY(-2px); +} + +.btn-control.reset { + background: rgba(239, 68, 68, 0.2); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +.btn-control.reset:hover { + background: rgba(239, 68, 68, 0.3); + border-color: rgba(239, 68, 68, 0.5); + transform: translateY(-2px); +} + +/* Connection Info */ +.connection-info { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + width: 100%; +} + +.connection-info h5 { + color: rgba(255, 255, 255, 0.6); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.75rem; +} + +.connection-url { + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0.5rem; + padding: 0.75rem 1rem; + font-family: monospace; + font-size: 0.85rem; + color: #0088cc; + word-break: break-all; +} + +/* Info Box */ +.info-box { + background: rgba(0, 136, 204, 0.1); + border: 1px solid rgba(0, 136, 204, 0.2); + border-radius: 0.5rem; + padding: 1rem; + margin-top: 1.5rem; +} + +.info-box-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.info-box-header i { + color: #0088cc; +} + +.info-box-header span { + color: #fff; + font-weight: 500; + font-size: 0.9rem; +} + +.info-box p { + margin: 0; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.6); + line-height: 1.5; +} From 4ce7fb61680458ec57fba008e014becd5a909310 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Wed, 17 Dec 2025 11:46:44 +0100 Subject: [PATCH 101/106] feat: improve chat messages --- TelegramDownloader/Pages/FetchData.razor | 61 +++- TelegramDownloader/Pages/FetchData.razor.css | 148 ++++++++ .../Pages/Modals/DownloadModal.razor | 99 ++++-- .../Pages/Modals/DownloadModal.razor.css | 322 ++++++++++++++++++ .../Pages/Partials/ChatMessage.razor | 168 ++++++--- .../Pages/Partials/ChatMessage.razor.css | 255 ++++++++++++++ .../Shared/CustomDropDownTree.razor | 4 +- .../Shared/CustomDropDownTree.razor.css | 71 +++- .../Shared/CustomTreeNode.razor.css | 51 ++- 9 files changed, 1056 insertions(+), 123 deletions(-) create mode 100644 TelegramDownloader/Pages/FetchData.razor.css create mode 100644 TelegramDownloader/Pages/Modals/DownloadModal.razor.css create mode 100644 TelegramDownloader/Pages/Partials/ChatMessage.razor.css diff --git a/TelegramDownloader/Pages/FetchData.razor b/TelegramDownloader/Pages/FetchData.razor index 379c11d..acfd196 100644 --- a/TelegramDownloader/Pages/FetchData.razor +++ b/TelegramDownloader/Pages/FetchData.razor @@ -9,34 +9,65 @@ @inject NavigationManager NavManager; @inject PreloadService PreloadService; -Chat +Chat Messages @if (m == null) { -

Seleciona un canal

+
+
+ +

Select a channel

+

Choose a channel from the sidebar to view messages

+
+
} else { - -
- @if(m != null) +
+ +
+
+

Messages

+ @if(m.Count > 0) + { + @m.Count loaded + } +
+
+ +
+
+ + + @if(m.Count > 0) { - - @foreach (var mess in m) - { - - +
+ @foreach (var mess in m) + { + + } +
- } @if (!showAll) { - +
+ +
} - } - + else + { +
+ +

No messages

+

This channel doesn't have any messages yet

+
+ }
- + } diff --git a/TelegramDownloader/Pages/FetchData.razor.css b/TelegramDownloader/Pages/FetchData.razor.css new file mode 100644 index 0000000..aefe05a --- /dev/null +++ b/TelegramDownloader/Pages/FetchData.razor.css @@ -0,0 +1,148 @@ +/* ===== Chat Messages Page Styles ===== */ + +.chat-page { + max-width: 900px; + margin: 0 auto; + padding: 1rem; +} + +/* Header */ +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: #1e1e2e; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + margin-bottom: 1.5rem; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); +} + +.chat-header-left { + display: flex; + align-items: center; + gap: 1rem; +} + +.chat-header h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #fff; +} + +.message-count { + background: rgba(0, 136, 204, 0.2); + color: #0088cc; + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.8rem; + font-weight: 500; +} + +/* Toggle Switch Container */ +.toggle-container { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.toggle-container ::deep .form-check-label { + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; +} + +/* Messages List */ +.messages-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 4rem 2rem; + background: #1e1e2e; + border: 1px dashed rgba(255, 255, 255, 0.2); + border-radius: 1rem; +} + +.empty-state i { + font-size: 4rem; + color: rgba(255, 255, 255, 0.2); + margin-bottom: 1rem; +} + +.empty-state h3 { + color: rgba(255, 255, 255, 0.6); + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.empty-state p { + color: rgba(255, 255, 255, 0.4); + font-size: 0.9rem; + margin: 0; +} + +/* Load More Button */ +.load-more-container { + display: flex; + justify-content: center; + padding: 1.5rem 0; +} + +.btn-load-more { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 2rem; + background: linear-gradient(135deg, #0088cc 0%, #0066aa 100%); + border: none; + border-radius: 2rem; + color: #fff; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3); +} + +.btn-load-more:hover { + background: linear-gradient(135deg, #0099dd 0%, #0077bb 100%); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 136, 204, 0.4); +} + +.btn-load-more i { + font-size: 1.1rem; +} + +/* Scroll to top button */ +.scroll-top-btn { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, #0088cc 0%, #0066aa 100%); + border: none; + color: #fff; + font-size: 1.25rem; + cursor: pointer; + box-shadow: 0 4px 15px rgba(0, 136, 204, 0.4); + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.scroll-top-btn:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(0, 136, 204, 0.5); +} diff --git a/TelegramDownloader/Pages/Modals/DownloadModal.razor b/TelegramDownloader/Pages/Modals/DownloadModal.razor index 38acf03..a7109f1 100644 --- a/TelegramDownloader/Pages/Modals/DownloadModal.razor +++ b/TelegramDownloader/Pages/Modals/DownloadModal.razor @@ -4,49 +4,76 @@ @using TelegramDownloader.Services @using TelegramDownloader.Shared - @inject ITelegramService ts @inject IFileService fs