Compare commits
6 Commits
88e724099c
...
2c125c24ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c125c24ae | ||
|
|
646e0a814a | ||
|
|
a478943792 | ||
|
|
a7baeb0d73 | ||
|
|
1903cb2938 | ||
|
|
9e81e221c6 |
@@ -1,9 +1,14 @@
|
||||
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/" Icon="@Icons.Material.Filled.Home" Match="NavLinkMatch.All">Home</MudNavLink>
|
||||
<MudNavLink Href="/Channels" Icon="@Icons.Material.Filled.SupervisorAccount" Match="NavLinkMatch.All">Channels</MudNavLink>
|
||||
<MudNavLink Href="/Library" Icon="@Icons.Material.Filled.LocalLibrary" Match="NavLinkMatch.All">Library</MudNavLink>
|
||||
<MudNavLink Href="/Playlists" Icon="@Icons.Material.Filled.ViewList" Match="NavLinkMatch.All">Playlists</MudNavLink>
|
||||
<MudNavLink Href="/Development" Icon="@Icons.Material.Filled.DeveloperMode" Match="NavLinkMatch.All">Development</MudNavLink>
|
||||
<MudNavLink Href="/Services" Icon="@Icons.Material.Filled.MiscellaneousServices" Match="NavLinkMatch.All">Services</MudNavLink>
|
||||
<MudNavGroup Title="Library" Expanded Icon="@Icons.Custom.Brands.YouTube" IconColor="Color.Error">
|
||||
<MudNavLink Href="/Accounts" Icon="@Icons.Material.Filled.AccountBox" Match="NavLinkMatch.All">Accounts</MudNavLink>
|
||||
<MudNavLink Href="/Channels" Icon="@Icons.Material.Filled.AccountCircle" Match="NavLinkMatch.All">Channels</MudNavLink>
|
||||
<MudNavLink Href="/Playlists" Icon="@Icons.Material.Filled.ViewList" Match="NavLinkMatch.All">Playlists</MudNavLink>
|
||||
<MudNavLink Href="/Library" Icon="@Icons.Material.Filled.Info" Match="NavLinkMatch.All" IconColor="Color.Info">Info</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="Application" Expanded Icon="@Icons.Material.Filled.SettingsSystemDaydream" IconColor="Color.Primary">
|
||||
<MudNavLink Href="/Development" Icon="@Icons.Material.Filled.DeveloperMode" Match="NavLinkMatch.All">Development</MudNavLink>
|
||||
<MudNavLink Href="/Services" Icon="@Icons.Material.Filled.MiscellaneousServices" Match="NavLinkMatch.All">Services</MudNavLink>
|
||||
</MudNavGroup>
|
||||
</MudNavMenu>
|
||||
53
Manager.App/Components/Pages/Accounts.razor
Normal file
53
Manager.App/Components/Pages/Accounts.razor
Normal file
@@ -0,0 +1,53 @@
|
||||
@page "/Accounts"
|
||||
@using Manager.App.Controllers
|
||||
@using Manager.App.Models.Settings
|
||||
@using Manager.App.Services.System
|
||||
@using Microsoft.Extensions.Options
|
||||
|
||||
@inject ILibraryService LibraryService
|
||||
@inject IDialogService DialogService
|
||||
@inject IOptions<LibrarySettings> LibraryOptions
|
||||
@inject ClientService ClientService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Accounts</PageTitle>
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudPaper Elevation="0" Outlined>
|
||||
<MudStack Row Class="ma-2">
|
||||
<MudButton IconSize="Size.Small" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Outlined" OnClick="OnAddAccountDialogAsync">Add account</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable @ref="@_table" ServerData="ServerReload">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">Accounts</MudText>
|
||||
<MudSpacer />
|
||||
<MudTextField T="string" ValueChanged="@(s=>OnSearch(s))" Placeholder="Search" Adornment="Adornment.Start" DebounceInterval="300"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh></MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Handle</MudTh>
|
||||
<MudTh>ID</MudTh>
|
||||
<MudTh>Cookies</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd><MudImage Src="@(FileController.CreateProvideUrl(context.AvatarFileId))" Height="40"/></MudTd>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>@context.Handle</MudTd>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@context.HasCookies</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>No channels found</MudText>
|
||||
</NoRecordsContent>
|
||||
<LoadingContent>
|
||||
<MudText>Loading...</MudText>
|
||||
</LoadingContent>
|
||||
<PagerContent>
|
||||
<MudTablePager/>
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
</MudStack>
|
||||
73
Manager.App/Components/Pages/Accounts.razor.cs
Normal file
73
Manager.App/Components/Pages/Accounts.razor.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Manager.App.Components.Dialogs;
|
||||
using Manager.App.Models.Library;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Manager.App.Components.Pages;
|
||||
|
||||
public partial class Accounts : ComponentBase
|
||||
{
|
||||
private MudTable<AccountListView>? _table;
|
||||
private readonly DialogOptions _dialogOptions = new() { BackdropClick = false, CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.ExtraLarge };
|
||||
private string _search = "";
|
||||
|
||||
private async Task<TableData<AccountListView>> ServerReload(TableState state, CancellationToken token)
|
||||
{
|
||||
var results = await LibraryService.GetAccountsAsync(_search, state.Page * state.PageSize, state.PageSize, token);
|
||||
return !results.IsSuccess ? new TableData<AccountListView>() : new TableData<AccountListView> { Items = results.Value, TotalItems = results.Total };
|
||||
}
|
||||
|
||||
private void OnSearch(string text)
|
||||
{
|
||||
_search = text;
|
||||
_table?.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnAddAccountDialogAsync()
|
||||
{
|
||||
var libSettings = LibraryOptions.Value;
|
||||
var parameters = new DialogParameters<AccountDialog> { { x => x.DefaultUserAgent, libSettings.DefaultUserAgent } };
|
||||
var dialog = await DialogService.ShowAsync<AccountDialog>("Add account", parameters, _dialogOptions);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result == null || result.Canceled || result.Data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var clientChannel = (ClientChannel)result.Data;
|
||||
if (clientChannel?.YouTubeClient == null)
|
||||
{
|
||||
Snackbar.Add("No YouTube client received.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var savedClientResult = await ClientService.SaveClientAsync(clientChannel.YouTubeClient);
|
||||
if (savedClientResult.IsSuccess)
|
||||
{
|
||||
if (_table != null)
|
||||
{
|
||||
await _table.ReloadServerData();
|
||||
}
|
||||
Snackbar.Add($"Client {clientChannel.Channel?.Handle ?? clientChannel.YouTubeClient.Id} saved!", Severity.Success);
|
||||
ClientService.AddClient(clientChannel.YouTubeClient);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add($"Failed to store client: {savedClientResult.Error?.Description ?? "Unknown!"}", Severity.Error);
|
||||
}
|
||||
|
||||
if (clientChannel.Channel == null)
|
||||
{
|
||||
Snackbar.Add("No channel information received!", Severity.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
var saveChannelResult = await LibraryService.SaveChannelAsync(clientChannel.Channel);
|
||||
if (!saveChannelResult.IsSuccess)
|
||||
{
|
||||
Snackbar.Add("Failed to save channel information", Severity.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,29 @@
|
||||
@page "/Channels"
|
||||
@using Manager.App.Models.Settings
|
||||
@using Manager.App.Services.System
|
||||
@using Microsoft.Extensions.Options
|
||||
@using Manager.App.Controllers
|
||||
|
||||
@inject ILibraryService LibraryService
|
||||
@inject IDialogService DialogService
|
||||
@inject IOptions<LibrarySettings> LibraryOptions
|
||||
@inject ClientService ClientService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Channels</PageTitle>
|
||||
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudPaper Elevation="0" Outlined>
|
||||
<MudStack Row Class="ma-2">
|
||||
<MudButton IconSize="Size.Small" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Outlined" OnClick="OnAddAccountDialogAsync">Add account</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable @ref="@_table" ServerData="ServerReload">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">Channels</MudText>
|
||||
<MudText Typo="Typo.h6">Channels stored in the library</MudText>
|
||||
<MudSpacer />
|
||||
<MudTextField T="string" ValueChanged="@(s=>OnSearch(s))" Placeholder="Search" Adornment="Adornment.Start" DebounceInterval="300"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh></MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Handle</MudTh>
|
||||
<MudTh>Channel id</MudTh>
|
||||
<MudTh>Has login</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd><MudImage Src="@(FileController.CreateProvideUrl(context.AvatarFileId))" Height="40"/></MudTd>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>@context.Handle</MudTd>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@(context.ClientAccount != null)</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>No channels found</MudText>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Manager.App.Components.Dialogs;
|
||||
using Manager.App.Models.Library;
|
||||
using Manager.Data.Entities.LibraryContext;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -8,60 +6,18 @@ namespace Manager.App.Components.Pages;
|
||||
|
||||
public partial class Channels : ComponentBase
|
||||
{
|
||||
private readonly DialogOptions _dialogOptions = new() { BackdropClick = false, CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.ExtraLarge };
|
||||
private MudTable<ChannelEntity>? _table;
|
||||
private MudTable<ChannelListView>? _table;
|
||||
private string _search = "";
|
||||
|
||||
private async Task<TableData<ChannelEntity>> ServerReload(TableState state, CancellationToken token)
|
||||
private async Task<TableData<ChannelListView>> ServerReload(TableState state, CancellationToken token)
|
||||
{
|
||||
var results = await LibraryService.GetChannelsAsync(state.PageSize, state.Page * state.PageSize, token);
|
||||
return !results.IsSuccess ? new TableData<ChannelEntity>() : new TableData<ChannelEntity> { Items = results.Value, TotalItems = results.Total };
|
||||
var results = await LibraryService.GetChannelsAsync(_search, state.Page * state.PageSize, state.PageSize, token);
|
||||
return !results.IsSuccess ? new TableData<ChannelListView>() : new TableData<ChannelListView> { Items = results.Value, TotalItems = results.Total };
|
||||
}
|
||||
|
||||
private async Task OnAddAccountDialogAsync()
|
||||
private void OnSearch(string text)
|
||||
{
|
||||
var libSettings = LibraryOptions.Value;
|
||||
var parameters = new DialogParameters<AccountDialog> { { x => x.DefaultUserAgent, libSettings.DefaultUserAgent } };
|
||||
var dialog = await DialogService.ShowAsync<AccountDialog>("Add account", parameters, _dialogOptions);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result == null || result.Canceled || result.Data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var clientChannel = (ClientChannel)result.Data;
|
||||
if (clientChannel?.YouTubeClient == null)
|
||||
{
|
||||
Snackbar.Add("No YouTube client received.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var savedClientResult = await ClientService.SaveClientAsync(clientChannel.YouTubeClient);
|
||||
if (savedClientResult.IsSuccess)
|
||||
{
|
||||
if (_table != null)
|
||||
{
|
||||
await _table.ReloadServerData();
|
||||
}
|
||||
Snackbar.Add($"Client {clientChannel.Channel?.Handle ?? clientChannel.YouTubeClient.Id} saved!", Severity.Success);
|
||||
ClientService.AddClient(clientChannel.YouTubeClient);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add($"Failed to store client: {savedClientResult.Error?.Description ?? "Unknown!"}", Severity.Error);
|
||||
}
|
||||
|
||||
if (clientChannel.Channel == null)
|
||||
{
|
||||
Snackbar.Add("No channel information received!", Severity.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
var saveChannelResult = await LibraryService.SaveChannelAsync(clientChannel.Channel);
|
||||
if (!saveChannelResult.IsSuccess)
|
||||
{
|
||||
Snackbar.Add("Failed to save channel information", Severity.Warning);
|
||||
}
|
||||
}
|
||||
_search = text;
|
||||
_table?.ReloadServerData();
|
||||
}
|
||||
}
|
||||
24
Manager.App/Controllers/FileController.cs
Normal file
24
Manager.App/Controllers/FileController.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Manager.App.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Manager.App.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
public class FileController(ILibraryService libraryService) : ControllerBase
|
||||
{
|
||||
public static string CreateProvideUrl(Guid? id) => id == null ? "" : $"/api/v1/file/provide?id={id}";
|
||||
|
||||
[HttpGet("provide")]
|
||||
public async Task<IActionResult> ProvideFile([FromQuery(Name = "id")] Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var fileResult = await libraryService.GetFileByIdAsync(id, cancellationToken);
|
||||
if (!fileResult.IsSuccess)
|
||||
{
|
||||
return BadRequest(fileResult.Error);
|
||||
}
|
||||
|
||||
var libFile = fileResult.Value;
|
||||
return File(libFile.DataStream, libFile.MimeType, libFile.FileName);
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,10 @@ public interface ILibraryService
|
||||
{
|
||||
public Task<Result> FetchChannelImagesAsync(InnertubeChannel innertubeChannel);
|
||||
public Task<Result> SaveClientAsync(ClientAccountEntity client, CancellationToken cancellationToken = default);
|
||||
public Task<Result<LibraryFile>> GetFileByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
public Task<Result<ChannelEntity>> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default);
|
||||
|
||||
public Task<Result> SaveChannelAsync(InnertubeChannel innertubeChannel, CancellationToken cancellationToken = default);
|
||||
public Task<Result<LibraryInformation>> GetLibraryInfoAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
public Task<ListResult<ChannelEntity>> GetChannelsAsync(int total = 20, int offset = 0, CancellationToken cancellationToken = default);
|
||||
public Task<ListResult<AccountListView>> GetAccountsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default);
|
||||
public Task<ListResult<ChannelListView>> GetChannelsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Mime;
|
||||
using DotBased.Monads;
|
||||
using Manager.App.Constants;
|
||||
using Manager.App.Models.Library;
|
||||
@@ -51,7 +52,7 @@ public class LibraryService : ILibraryService
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ResultError.Error(e);
|
||||
return HandleException(e);
|
||||
}
|
||||
|
||||
return Result.Success();
|
||||
@@ -61,7 +62,7 @@ public class LibraryService : ILibraryService
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (context.Files.Any(f => image.Url.Equals(f.OriginalUrl, StringComparison.OrdinalIgnoreCase)))
|
||||
if (context.Files.Any(f => image.Url.Equals(f.OriginalUrl)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -108,7 +109,7 @@ public class LibraryService : ILibraryService
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var updateEntity = false;
|
||||
var dbClient = context.ClientAccounts.FirstOrDefault(c => c.Id == client.Id);
|
||||
var dbClient = context.ClientAccounts.Include(ca => ca.HttpCookies).FirstOrDefault(c => c.Id == client.Id);
|
||||
if (dbClient == null)
|
||||
{
|
||||
dbClient = client;
|
||||
@@ -134,7 +135,27 @@ public class LibraryService : ILibraryService
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ResultError.Error(e);
|
||||
return HandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<LibraryFile>> GetFileByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var file = context.Files.FirstOrDefault(f => f.Id == id);
|
||||
if (file == null)
|
||||
{
|
||||
return ResultError.Fail($"File with id {id} not found.");
|
||||
}
|
||||
|
||||
var fs = new FileStream(Path.Combine(_libraryDirectory.FullName, LibraryConstants.Directories.SubDirChannels, file.RelativePath), FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return new LibraryFile { DataStream = fs, SizeBytes = file.SizeBytes, FileName = file.OriginalFileName ?? file.Id.ToString(), MimeType = file.MimeType ?? MediaTypeNames.Application.Octet };
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return HandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +172,7 @@ public class LibraryService : ILibraryService
|
||||
var channel = await context.Channels
|
||||
.Include(c => c.ClientAccount)
|
||||
.ThenInclude(p => p!.HttpCookies)
|
||||
.Include(f => f.Files)
|
||||
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
|
||||
|
||||
if (channel == null)
|
||||
@@ -246,13 +268,97 @@ public class LibraryService : ILibraryService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ListResult<ChannelEntity>> GetChannelsAsync(int total = 20, int offset = 0, CancellationToken cancellationToken = default)
|
||||
public async Task<ListResult<AccountListView>> GetAccountsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (total == 0)
|
||||
{
|
||||
total = 20;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var orderedAccounts = context.Channels.Include(x => x.ClientAccount).Where(x => x.ClientAccount != null).OrderBy(x => x.Id);
|
||||
return new ListResultReturn<ChannelEntity>(orderedAccounts.Skip(offset).Take(total).ToList(),orderedAccounts.Count());
|
||||
var accountsQuery = context.ClientAccounts
|
||||
.Include(ca => ca.Channel)
|
||||
.Include(ca => ca.HttpCookies)
|
||||
.OrderByDescending(ca => ca.Id).AsQueryable();
|
||||
var totalAccounts = accountsQuery.Count();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search) && totalAccounts != 0)
|
||||
{
|
||||
var normalizedSearch = $"%{search.ToLower()}%";
|
||||
accountsQuery = accountsQuery
|
||||
.Where(ca =>
|
||||
EF.Functions.Like(
|
||||
(
|
||||
ca.Id.ToString() + " " +
|
||||
(ca.Channel != null ? ca.Channel.Name : "") + " " +
|
||||
(ca.Channel != null ? ca.Channel.Handle : "")
|
||||
).ToLower(),
|
||||
normalizedSearch
|
||||
)
|
||||
);
|
||||
totalAccounts = accountsQuery.Count();
|
||||
}
|
||||
|
||||
var accountViews = accountsQuery.Skip(offset).Take(total).Select(account => new AccountListView
|
||||
{
|
||||
Id = account.Id,
|
||||
Name = account.Channel != null ? account.Channel.Name : "",
|
||||
Handle = account.Channel != null ? account.Channel.Handle : "",
|
||||
HasCookies = account.HttpCookies.Count != 0,
|
||||
AvatarFileId = account.Files == null ? null
|
||||
: account.Files.Where(f => f.FileType == LibraryConstants.FileTypes.ChannelAvatar).OrderBy(x => x.Id).Select(f => f.Id).FirstOrDefault()
|
||||
});
|
||||
|
||||
return new ListResultReturn<AccountListView>(totalAccounts == 0 ? [] : accountViews.ToList(), totalAccounts);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return HandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ListResult<ChannelListView>> GetChannelsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (total == 0)
|
||||
{
|
||||
total = 20;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var channelQuery = context.Channels.OrderByDescending(c => c.Id).AsQueryable();
|
||||
|
||||
var totalChannels = channelQuery.Count();
|
||||
if (!string.IsNullOrWhiteSpace(search) && totalChannels != 0)
|
||||
{
|
||||
var normalizedSearch = $"%{search.ToLower()}%";
|
||||
channelQuery = channelQuery
|
||||
.Where(ca =>
|
||||
EF.Functions.Like((
|
||||
ca.Id.ToString() + " " +
|
||||
ca.Name + " " +
|
||||
ca.Handle
|
||||
).ToLower(),
|
||||
normalizedSearch
|
||||
)
|
||||
);
|
||||
totalChannels = channelQuery.Count();
|
||||
}
|
||||
|
||||
var channelViews = channelQuery.Skip(offset).Take(total).Select(channel => new ChannelListView
|
||||
{
|
||||
Id = channel.Id,
|
||||
Name = channel.Name,
|
||||
Handle = channel.Handle,
|
||||
AvatarFileId = channel.Files == null ? null
|
||||
: channel.Files.Where(f => f.FileType == LibraryConstants.FileTypes.ChannelAvatar).OrderBy(x => x.Id).Select(f => f.Id).FirstOrDefault()
|
||||
});
|
||||
|
||||
return new ListResultReturn<ChannelListView>(totalChannels == 0 ? [] : channelViews.ToList(), totalChannels);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -281,7 +387,7 @@ public class LibraryService : ILibraryService
|
||||
return ResultError.Fail("Library service operation cancelled");
|
||||
}
|
||||
|
||||
_logger.LogError(exception, "Failed to get library information");
|
||||
return ResultError.Fail("Failed to get library information");
|
||||
_logger.LogError(exception, "Service error");
|
||||
return ResultError.Error(exception);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,22 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientServ
|
||||
return ResultError.Fail("Client does not have an ID, cannot save to library database!");
|
||||
}
|
||||
|
||||
var saveResult = await _libraryService.SaveClientAsync(new ClientAccountEntity { Id = client.Id, UserAgent = client.UserAgent }, cancellationToken);
|
||||
List<HttpCookieEntity> httpCookies = [];
|
||||
httpCookies.AddRange(client.CookieContainer.GetAllCookies()
|
||||
.ToList()
|
||||
.Select(cookie => new HttpCookieEntity
|
||||
{
|
||||
ClientId = client.Id,
|
||||
Name = cookie.Name,
|
||||
Value = cookie.Value,
|
||||
Domain = cookie.Domain,
|
||||
Path = cookie.Path,
|
||||
Secure = cookie.Secure,
|
||||
HttpOnly = cookie.HttpOnly,
|
||||
ExpiresUtc = cookie.Expires
|
||||
}));
|
||||
|
||||
var saveResult = await _libraryService.SaveClientAsync(new ClientAccountEntity { Id = client.Id, UserAgent = client.UserAgent, HttpCookies = httpCookies }, cancellationToken);
|
||||
return saveResult;
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,12 @@ public sealed class LibraryDbContext : DbContext
|
||||
.WithOne()
|
||||
.HasForeignKey(x => x.ChannelId);
|
||||
channel.HasOne(x => x.ClientAccount)
|
||||
.WithOne(x => x.Channel)
|
||||
.HasForeignKey<ClientAccountEntity>(e => e.Id)
|
||||
.IsRequired(false);
|
||||
channel.HasMany(x => x.Files)
|
||||
.WithOne()
|
||||
.HasForeignKey<ClientAccountEntity>(e => e.Id);
|
||||
.HasForeignKey(f => f.ForeignKey);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ClientAccountEntity>(cae =>
|
||||
@@ -66,6 +70,13 @@ public sealed class LibraryDbContext : DbContext
|
||||
cae.HasMany(x => x.HttpCookies)
|
||||
.WithOne()
|
||||
.HasForeignKey(x => x.ClientId);
|
||||
cae.HasOne(x => x.Channel)
|
||||
.WithOne(ca => ca.ClientAccount)
|
||||
.HasForeignKey<ChannelEntity>(ce => ce.Id)
|
||||
.IsRequired(false);
|
||||
cae.HasMany(x => x.Files)
|
||||
.WithOne()
|
||||
.HasForeignKey(f => f.ForeignKey);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<HttpCookieEntity>(httpce =>
|
||||
|
||||
@@ -17,4 +17,5 @@ public class ChannelEntity : DateTimeBase
|
||||
public List<MediaEntity> Media { get; set; } = [];
|
||||
public List<PlaylistEntity> Playlists { get; set; } = [];
|
||||
public ClientAccountEntity? ClientAccount { get; set; }
|
||||
public List<FileEntity>? Files { get; set; }
|
||||
}
|
||||
@@ -11,4 +11,6 @@ public class ClientAccountEntity : DateTimeBase
|
||||
public List<HttpCookieEntity> HttpCookies { get; set; } = [];
|
||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||
public string? UserAgent { get; set; }
|
||||
public ChannelEntity? Channel { get; set; }
|
||||
public List<FileEntity>? Files { get; set; }
|
||||
}
|
||||
@@ -31,7 +31,17 @@ public static class AuthenticationUtilities
|
||||
throw new ArgumentNullException(nameof(origin));
|
||||
}
|
||||
|
||||
datasyncId = datasyncId.Replace("||", "");
|
||||
if (datasyncId.Contains("||", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var ids = datasyncId.Split("||", StringSplitOptions.RemoveEmptyEntries);
|
||||
datasyncId = ids.Length switch
|
||||
{
|
||||
1 => ids[0],
|
||||
2 => ids[1],
|
||||
_ => datasyncId
|
||||
};
|
||||
}
|
||||
|
||||
sapisid = Uri.UnescapeDataString(sapisid);
|
||||
if (string.IsNullOrWhiteSpace(time))
|
||||
{
|
||||
@@ -52,7 +62,7 @@ public static class AuthenticationUtilities
|
||||
private static string GetTime()
|
||||
{
|
||||
var st = new DateTime(1970, 1, 1);
|
||||
var t = DateTime.Now.ToUniversalTime() - st;
|
||||
var t = DateTime.UtcNow - st;
|
||||
var time = (t.TotalMilliseconds + 0.5).ToString(CultureInfo.InvariantCulture);
|
||||
return time[..10];
|
||||
}
|
||||
|
||||
@@ -21,30 +21,33 @@ public sealed class YouTubeClient : IDisposable
|
||||
public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"];
|
||||
public HttpClient HttpClient { get; }
|
||||
|
||||
private YouTubeClient(CookieCollection cookies, string userAgent)
|
||||
private YouTubeClient(CookieCollection? cookies, string userAgent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userAgent))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userAgent));
|
||||
}
|
||||
UserAgent = userAgent;
|
||||
if (cookies.Count == 0)
|
||||
if (cookies == null || cookies.Count == 0)
|
||||
{
|
||||
Id = $"anon_{Guid.NewGuid()}";
|
||||
IsAnonymous = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CookieContainer.Add(cookies);
|
||||
}
|
||||
|
||||
CookieContainer.Add(cookies);
|
||||
HttpClient = new HttpClient(GetHttpClientHandler());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the given cookies and fetch client state.
|
||||
/// </summary>
|
||||
/// <param name="cookies">The cookies to use for making requests. Empty collection for anonymous requests.</param>
|
||||
/// <param name="cookies">The cookies to use for making requests. Empty collection or null for anonymous requests.</param>
|
||||
/// <param name="userAgent">The user agent to use for the requests. Only WEB client is supported.</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Result<YouTubeClient>> CreateAsync(CookieCollection cookies, string userAgent)
|
||||
public static async Task<Result<YouTubeClient>> CreateAsync(CookieCollection? cookies, string userAgent)
|
||||
{
|
||||
var client = new YouTubeClient(cookies, userAgent);
|
||||
var clientInitializeResult = await client.FetchClientDataAsync();
|
||||
@@ -202,6 +205,22 @@ public sealed class YouTubeClient : IDisposable
|
||||
return tempDatasyncId;
|
||||
}
|
||||
|
||||
public async Task<Result> RotateCookiesPageAsync(string origin = NetworkService.Origin, int ytPid = 1)
|
||||
{
|
||||
if (IsAnonymous)
|
||||
{
|
||||
return ResultError.Fail("Anonymous clients cannot rotate cookies!");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(origin))
|
||||
{
|
||||
return ResultError.Fail("Origin is empty!");
|
||||
}
|
||||
|
||||
var rotatePageCookiesRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://accounts.youtube.com/RotateCookiesPage?origin={origin}&yt_pid={ytPid}"));
|
||||
return await NetworkService.MakeRequestAsync(rotatePageCookiesRequest, this, true);
|
||||
}
|
||||
|
||||
public async Task<Result> RotateCookiesAsync()
|
||||
{
|
||||
if (IsAnonymous)
|
||||
@@ -209,16 +228,8 @@ public sealed class YouTubeClient : IDisposable
|
||||
return ResultError.Fail("Anonymous clients cannot rotate cookies!");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var rotateRequest = new HttpRequestMessage(HttpMethod.Post, new Uri("https://accounts.youtube.com/RotateCookies"));
|
||||
var response = await HttpClient.SendAsync(rotateRequest);
|
||||
return !response.IsSuccessStatusCode ? ResultError.Fail($"Cookies rotation failed, status code: {response.StatusCode}") : Result.Success();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ResultError.Error(e);
|
||||
}
|
||||
var rotateRequest = new HttpRequestMessage(HttpMethod.Post, new Uri("https://accounts.youtube.com/RotateCookies"));
|
||||
return await NetworkService.MakeRequestAsync(rotateRequest, this, true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
Reference in New Issue
Block a user