Refactor authentication (#348)
This commit is contained in:
parent
eb1bca6271
commit
6641d7d484
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Gress;
|
using Gress;
|
||||||
@ -14,7 +15,10 @@ namespace YoutubeDownloader.Core.Downloading;
|
|||||||
|
|
||||||
public class VideoDownloader
|
public class VideoDownloader
|
||||||
{
|
{
|
||||||
private readonly YoutubeClient _youtube = new(Http.Client);
|
private readonly YoutubeClient _youtube;
|
||||||
|
|
||||||
|
public VideoDownloader(HttpClient http) =>
|
||||||
|
_youtube = new YoutubeClient(http);
|
||||||
|
|
||||||
public async Task<IReadOnlyList<VideoDownloadOption>> GetDownloadOptionsAsync(
|
public async Task<IReadOnlyList<VideoDownloadOption>> GetDownloadOptionsAsync(
|
||||||
VideoId videoId,
|
VideoId videoId,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Gress;
|
using Gress;
|
||||||
@ -15,14 +16,17 @@ namespace YoutubeDownloader.Core.Resolving;
|
|||||||
|
|
||||||
public class QueryResolver
|
public class QueryResolver
|
||||||
{
|
{
|
||||||
private readonly YoutubeClient _youtube = new(Http.Client);
|
private readonly YoutubeClient _youtube;
|
||||||
|
|
||||||
|
public QueryResolver(HttpClient http) =>
|
||||||
|
_youtube = new YoutubeClient(http);
|
||||||
|
|
||||||
public async Task<QueryResult> ResolveAsync(
|
public async Task<QueryResult> ResolveAsync(
|
||||||
string query,
|
string query,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Only consider URLs when parsing IDs.
|
// Only consider URLs for parsing IDs.
|
||||||
// All other queries should be treated as search queries.
|
// All other queries should be treated as search keywords.
|
||||||
var isUrl = Uri.IsWellFormedUriString(query, UriKind.Absolute);
|
var isUrl = Uri.IsWellFormedUriString(query, UriKind.Absolute);
|
||||||
|
|
||||||
// Playlist
|
// Playlist
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace YoutubeDownloader.Core.Utils;
|
|
||||||
|
|
||||||
public class AuthHandler : DelegatingHandler
|
|
||||||
{
|
|
||||||
private const string Origin = "https://www.youtube.com";
|
|
||||||
private readonly Uri _baseUri = new(Origin);
|
|
||||||
private readonly HttpClientHandler _innerHandler = new()
|
|
||||||
{
|
|
||||||
UseCookies = true,
|
|
||||||
CookieContainer = new CookieContainer()
|
|
||||||
};
|
|
||||||
|
|
||||||
public AuthHandler() => InnerHandler = _innerHandler;
|
|
||||||
|
|
||||||
public string? PageId { get; set; }
|
|
||||||
|
|
||||||
public void SetCookies(string cookies)
|
|
||||||
{
|
|
||||||
foreach (Cookie cookie in _innerHandler.CookieContainer.GetCookies(_baseUri))
|
|
||||||
cookie.Expired = true;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(cookies))
|
|
||||||
_innerHandler.CookieContainer.SetCookies(_baseUri, cookies);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var sapisid = _innerHandler.CookieContainer.GetCookies(_baseUri)["__Secure-3PAPISID"] ?? _innerHandler.CookieContainer.GetCookies(_baseUri)["SAPISID"];
|
|
||||||
|
|
||||||
if (sapisid is null)
|
|
||||||
return base.SendAsync(request, cancellationToken);
|
|
||||||
|
|
||||||
if(_innerHandler.CookieContainer.GetCookies(_baseUri)["SAPISID"] is null)
|
|
||||||
_innerHandler.CookieContainer.Add(_baseUri, new Cookie("SAPISID", sapisid.Value));
|
|
||||||
|
|
||||||
request.Headers.Remove("Authorization");
|
|
||||||
request.Headers.Remove("Origin");
|
|
||||||
request.Headers.Remove("X-Origin");
|
|
||||||
|
|
||||||
request.Headers.Add("Authorization", $"SAPISIDHASH {GenerateSidBasedAuth(sapisid.Value, Origin)}");
|
|
||||||
request.Headers.Add("Origin", Origin);
|
|
||||||
request.Headers.Add("X-Origin", Origin);
|
|
||||||
//Set to 0 as it is only allowed to be logged in with one account
|
|
||||||
request.Headers.Add("X-Goog-AuthUser", "0");
|
|
||||||
|
|
||||||
//Needed if there are brand accounts (Secondary channels)
|
|
||||||
if (PageId is not null)
|
|
||||||
request.Headers.Add("X-Goog-PageId", PageId);
|
|
||||||
|
|
||||||
return base.SendAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenerateSidBasedAuth(string sid, string origin)
|
|
||||||
{
|
|
||||||
var date = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
||||||
|
|
||||||
var timestamp = date / 1000;
|
|
||||||
var sidHash = Hash($"{timestamp} {sid} {origin}");
|
|
||||||
return $"{timestamp}_{sidHash}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Hash(string input)
|
|
||||||
{
|
|
||||||
var hash = SHA1.HashData(Encoding.UTF8.GetBytes(input));
|
|
||||||
return string.Concat(hash.Select(b => b.ToString("x2")));
|
|
||||||
}
|
|
||||||
}
|
|
16
YoutubeDownloader.Core/Utils/Extensions/BinaryExtensions.cs
Normal file
16
YoutubeDownloader.Core/Utils/Extensions/BinaryExtensions.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace YoutubeDownloader.Core.Utils.Extensions;
|
||||||
|
|
||||||
|
internal static class BinaryExtensions
|
||||||
|
{
|
||||||
|
public static string ToHex(this byte[] data)
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder(2 * data.Length);
|
||||||
|
|
||||||
|
foreach (var b in data)
|
||||||
|
buffer.Append(b.ToString("x2"));
|
||||||
|
|
||||||
|
return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
namespace YoutubeDownloader.Core.Utils.Extensions;
|
namespace YoutubeDownloader.Core.Utils.Extensions;
|
||||||
|
|
||||||
public static class CollectionExtensions
|
internal static class CollectionExtensions
|
||||||
{
|
{
|
||||||
public static void AddRange<T>(this ICollection<T> source, IEnumerable<T> items)
|
public static void AddRange<T>(this ICollection<T> source, IEnumerable<T> items)
|
||||||
{
|
{
|
||||||
foreach (var i in items)
|
foreach (var i in items)
|
||||||
source.Add(i);
|
source.Add(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string Join<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
|
|
||||||
}
|
}
|
@ -6,19 +6,21 @@ namespace YoutubeDownloader.Core.Utils;
|
|||||||
|
|
||||||
public static class Http
|
public static class Http
|
||||||
{
|
{
|
||||||
public static AuthHandler AuthHandler { get; } = new();
|
public static HttpClient Client { get; } = CreateClient();
|
||||||
public static HttpClient Client { get; } = new(AuthHandler, true)
|
|
||||||
{
|
public static HttpClient CreateClient(HttpMessageHandler? handler = null) =>
|
||||||
DefaultRequestHeaders =
|
new(handler ?? new SocketsHttpHandler(), true)
|
||||||
{
|
{
|
||||||
// Required by some of the services we're using
|
DefaultRequestHeaders =
|
||||||
UserAgent =
|
|
||||||
{
|
{
|
||||||
new ProductInfoHeaderValue(
|
// Required by some of the services we're using
|
||||||
"YoutubeDownloader",
|
UserAgent =
|
||||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)
|
{
|
||||||
)
|
new ProductInfoHeaderValue(
|
||||||
|
"YoutubeDownloader",
|
||||||
|
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
}
|
}
|
85
YoutubeDownloader.Core/YoutubeAuthHttpHandler.cs
Normal file
85
YoutubeDownloader.Core/YoutubeAuthHttpHandler.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using YoutubeDownloader.Core.Utils.Extensions;
|
||||||
|
|
||||||
|
namespace YoutubeDownloader.Core;
|
||||||
|
|
||||||
|
public partial class YoutubeAuthHttpHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly CookieContainer _cookieContainer = new();
|
||||||
|
|
||||||
|
public YoutubeAuthHttpHandler(IReadOnlyDictionary<string, string> cookies)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in cookies)
|
||||||
|
_cookieContainer.Add(YoutubeDomainUri, new Cookie(key, value));
|
||||||
|
|
||||||
|
InnerHandler = new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
UseCookies = true,
|
||||||
|
// Need to use a cookie container because YouTube sets additional cookies
|
||||||
|
// even after the initial authentication.
|
||||||
|
CookieContainer = _cookieContainer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cookies = _cookieContainer
|
||||||
|
.GetCookies(YoutubeDomainUri)
|
||||||
|
// Most specific cookies first
|
||||||
|
.OrderByDescending(c => string.Equals(c.Domain, YoutubeDomain, StringComparison.OrdinalIgnoreCase))
|
||||||
|
// Discard less specific cookies
|
||||||
|
.DistinctBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(c => c.Name, c => c.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var sessionId =
|
||||||
|
cookies.GetValueOrDefault("__Secure-3PAPISID") ??
|
||||||
|
cookies.GetValueOrDefault("SAPISID");
|
||||||
|
|
||||||
|
if (sessionId is not null)
|
||||||
|
{
|
||||||
|
// If only __Secure-3PAPISID is present, add SAPISID manually
|
||||||
|
if (!cookies.ContainsKey("SAPISID"))
|
||||||
|
_cookieContainer.Add(YoutubeDomainUri, new Cookie("SAPISID", sessionId));
|
||||||
|
|
||||||
|
request.Headers.Remove("Authorization");
|
||||||
|
request.Headers.Add("Authorization", $"SAPISIDHASH {GenerateAuthHash(sessionId)}");
|
||||||
|
|
||||||
|
request.Headers.Remove("Origin");
|
||||||
|
request.Headers.Add("Origin", YoutubeDomain);
|
||||||
|
|
||||||
|
request.Headers.Remove("X-Origin");
|
||||||
|
request.Headers.Add("X-Origin", YoutubeDomain);
|
||||||
|
|
||||||
|
// Set to 0 as it is only allowed to be logged in with one account
|
||||||
|
request.Headers.Add("X-Goog-AuthUser", "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class YoutubeAuthHttpHandler
|
||||||
|
{
|
||||||
|
private const string YoutubeDomain = "https://www.youtube.com";
|
||||||
|
private static readonly Uri YoutubeDomainUri = new(YoutubeDomain);
|
||||||
|
|
||||||
|
private static string GenerateAuthHash(string sessionId)
|
||||||
|
{
|
||||||
|
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000;
|
||||||
|
|
||||||
|
var token = $"{timestamp} {sessionId} {YoutubeDomain}";
|
||||||
|
var tokenHash = SHA1.HashData(Encoding.UTF8.GetBytes(token)).ToHex();
|
||||||
|
|
||||||
|
return timestamp + "_" + tokenHash;
|
||||||
|
}
|
||||||
|
}
|
@ -35,12 +35,12 @@ public class Bootstrapper : Bootstrapper<RootViewModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e)
|
protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs args)
|
||||||
{
|
{
|
||||||
base.OnUnhandledException(e);
|
base.OnUnhandledException(args);
|
||||||
|
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
e.Exception.ToString(),
|
args.Exception.ToString(),
|
||||||
"Error occured",
|
"Error occured",
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Error
|
MessageBoxImage.Error
|
||||||
|
@ -21,6 +21,8 @@ public partial class SettingsService : SettingsBase, INotifyPropertyChanged
|
|||||||
|
|
||||||
public bool IsDarkModeEnabled { get; set; } = IsDarkModeEnabledByDefault();
|
public bool IsDarkModeEnabled { get; set; } = IsDarkModeEnabledByDefault();
|
||||||
|
|
||||||
|
public bool IsAuthPersisted { get; set; } = true;
|
||||||
|
|
||||||
public bool ShouldInjectTags { get; set; } = true;
|
public bool ShouldInjectTags { get; set; } = true;
|
||||||
|
|
||||||
public bool ShouldSkipExistingFiles { get; set; }
|
public bool ShouldSkipExistingFiles { get; set; }
|
||||||
@ -28,12 +30,11 @@ public partial class SettingsService : SettingsBase, INotifyPropertyChanged
|
|||||||
public string FileNameTemplate { get; set; } = "$title";
|
public string FileNameTemplate { get; set; } = "$title";
|
||||||
|
|
||||||
public int ParallelLimit { get; set; } = 2;
|
public int ParallelLimit { get; set; } = 2;
|
||||||
|
|
||||||
public Dictionary<string,string> Cookies { get; set; } = new();
|
|
||||||
public string? PageId { get; set; }
|
|
||||||
|
|
||||||
public Version? LastAppVersion { get; set; }
|
public Version? LastAppVersion { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string>? LastAuthCookies { get; set; }
|
||||||
|
|
||||||
// STJ cannot properly serialize immutable structs
|
// STJ cannot properly serialize immutable structs
|
||||||
[JsonConverter(typeof(ContainerJsonConverter))]
|
[JsonConverter(typeof(ContainerJsonConverter))]
|
||||||
public Container LastContainer { get; set; } = Container.Mp4;
|
public Container LastContainer { get; set; } = Container.Mp4;
|
||||||
@ -44,6 +45,18 @@ public partial class SettingsService : SettingsBase, INotifyPropertyChanged
|
|||||||
: base(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat"))
|
: base(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat"))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Save()
|
||||||
|
{
|
||||||
|
// Clear the cookies if they are not supposed to be persisted
|
||||||
|
var lastAuthCookies = LastAuthCookies;
|
||||||
|
if (!IsAuthPersisted)
|
||||||
|
LastAuthCookies = null;
|
||||||
|
|
||||||
|
base.Save();
|
||||||
|
|
||||||
|
LastAuthCookies = lastAuthCookies;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class SettingsService
|
public partial class SettingsService
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Gress;
|
using Gress;
|
||||||
using Gress.Completable;
|
using Gress.Completable;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
using YoutubeDownloader.Core;
|
||||||
using YoutubeDownloader.Core.Downloading;
|
using YoutubeDownloader.Core.Downloading;
|
||||||
using YoutubeDownloader.Core.Resolving;
|
using YoutubeDownloader.Core.Resolving;
|
||||||
using YoutubeDownloader.Core.Tagging;
|
using YoutubeDownloader.Core.Tagging;
|
||||||
using YoutubeDownloader.Core.Utils;
|
using YoutubeDownloader.Core.Utils;
|
||||||
using YoutubeDownloader.Core.Utils.Extensions;
|
|
||||||
using YoutubeDownloader.Services;
|
using YoutubeDownloader.Services;
|
||||||
using YoutubeDownloader.Utils;
|
using YoutubeDownloader.Utils;
|
||||||
using YoutubeDownloader.ViewModels.Dialogs;
|
using YoutubeDownloader.ViewModels.Dialogs;
|
||||||
@ -28,10 +29,6 @@ public class DashboardViewModel : PropertyChangedBase, IDisposable
|
|||||||
private readonly AutoResetProgressMuxer _progressMuxer;
|
private readonly AutoResetProgressMuxer _progressMuxer;
|
||||||
private readonly ResizableSemaphore _downloadSemaphore = new();
|
private readonly ResizableSemaphore _downloadSemaphore = new();
|
||||||
|
|
||||||
private readonly QueryResolver _queryResolver = new();
|
|
||||||
private readonly VideoDownloader _videoDownloader = new();
|
|
||||||
private readonly MediaTagInjector _mediaTagInjector = new();
|
|
||||||
|
|
||||||
public bool IsBusy { get; private set; }
|
public bool IsBusy { get; private set; }
|
||||||
|
|
||||||
public ProgressContainer<Percentage> Progress { get; } = new();
|
public ProgressContainer<Percentage> Progress { get; } = new();
|
||||||
@ -55,13 +52,32 @@ public class DashboardViewModel : PropertyChangedBase, IDisposable
|
|||||||
|
|
||||||
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
||||||
|
|
||||||
_settingsService.BindAndInvoke(o => o.ParallelLimit, (_, e) => _downloadSemaphore.MaxCount = e.NewValue);
|
_settingsService.BindAndInvoke(
|
||||||
_settingsService.BindAndInvoke(o => o.Cookies, (_ ,e ) => Http.AuthHandler.SetCookies(e.NewValue.Select(i => $"{i.Key}={i.Value}").Join(",")));
|
o => o.ParallelLimit,
|
||||||
_settingsService.BindAndInvoke(o => o.PageId, (_, e) => Http.AuthHandler.PageId = e.NewValue);
|
(_, e) => _downloadSemaphore.MaxCount = e.NewValue
|
||||||
Progress.Bind(o => o.Current, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
);
|
||||||
Downloads.Bind(o => o.Count, (_, _) => NotifyOfPropertyChange(() => IsDownloadsAvailable));
|
|
||||||
|
Progress.Bind(
|
||||||
|
o => o.Current,
|
||||||
|
(_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate)
|
||||||
|
);
|
||||||
|
|
||||||
|
Downloads.Bind(
|
||||||
|
o => o.Count,
|
||||||
|
(_, _) => NotifyOfPropertyChange(() => IsDownloadsAvailable)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HttpClient CreateHttpClient() => Http.CreateClient(
|
||||||
|
new YoutubeAuthHttpHandler(_settingsService.LastAuthCookies ?? new Dictionary<string, string>())
|
||||||
|
);
|
||||||
|
|
||||||
|
public bool CanShowAuthSetup => !IsBusy;
|
||||||
|
|
||||||
|
public async void ShowAuthSetup() => await _dialogManager.ShowDialogAsync(
|
||||||
|
_viewModelFactory.CreateAuthSetupViewModel()
|
||||||
|
);
|
||||||
|
|
||||||
public bool CanShowSettings => !IsBusy;
|
public bool CanShowSettings => !IsBusy;
|
||||||
|
|
||||||
public async void ShowSettings() => await _dialogManager.ShowDialogAsync(
|
public async void ShowSettings() => await _dialogManager.ShowDialogAsync(
|
||||||
@ -76,19 +92,22 @@ public class DashboardViewModel : PropertyChangedBase, IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
using var http = CreateHttpClient();
|
||||||
|
var downloader = new VideoDownloader(http);
|
||||||
|
|
||||||
using var access = await _downloadSemaphore.AcquireAsync(download.CancellationToken);
|
using var access = await _downloadSemaphore.AcquireAsync(download.CancellationToken);
|
||||||
|
|
||||||
download.Status = DownloadStatus.Started;
|
download.Status = DownloadStatus.Started;
|
||||||
|
|
||||||
var downloadOption =
|
var downloadOption =
|
||||||
download.DownloadOption ??
|
download.DownloadOption ??
|
||||||
await _videoDownloader.GetBestDownloadOptionAsync(
|
await downloader.GetBestDownloadOptionAsync(
|
||||||
download.Video!.Id,
|
download.Video!.Id,
|
||||||
download.DownloadPreference!,
|
download.DownloadPreference!,
|
||||||
download.CancellationToken
|
download.CancellationToken
|
||||||
);
|
);
|
||||||
|
|
||||||
await _videoDownloader.DownloadVideoAsync(
|
await downloader.DownloadVideoAsync(
|
||||||
download.FilePath!,
|
download.FilePath!,
|
||||||
download.Video!,
|
download.Video!,
|
||||||
downloadOption,
|
downloadOption,
|
||||||
@ -100,7 +119,7 @@ public class DashboardViewModel : PropertyChangedBase, IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _mediaTagInjector.InjectTagsAsync(
|
await new MediaTagInjector().InjectTagsAsync(
|
||||||
download.FilePath!,
|
download.FilePath!,
|
||||||
download.Video!,
|
download.Video!,
|
||||||
download.CancellationToken
|
download.CancellationToken
|
||||||
@ -159,16 +178,21 @@ public class DashboardViewModel : PropertyChangedBase, IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _queryResolver.ResolveAsync(
|
using var http = CreateHttpClient();
|
||||||
|
var resolver = new QueryResolver(http);
|
||||||
|
|
||||||
|
var result = await resolver.ResolveAsync(
|
||||||
Query.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
Query.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
||||||
progress
|
progress
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var downloader = new VideoDownloader(http);
|
||||||
|
|
||||||
// Single video
|
// Single video
|
||||||
if (result.Videos.Count == 1)
|
if (result.Videos.Count == 1)
|
||||||
{
|
{
|
||||||
var video = result.Videos.Single();
|
var video = result.Videos.Single();
|
||||||
var downloadOptions = await _videoDownloader.GetDownloadOptionsAsync(video.Id);
|
var downloadOptions = await downloader.GetDownloadOptionsAsync(video.Id);
|
||||||
|
|
||||||
var download = await _dialogManager.ShowDialogAsync(
|
var download = await _dialogManager.ShowDialogAsync(
|
||||||
_viewModelFactory.CreateDownloadSingleSetupViewModel(video, downloadOptions)
|
_viewModelFactory.CreateDownloadSingleSetupViewModel(video, downloadOptions)
|
||||||
|
@ -46,7 +46,10 @@ public class DownloadViewModel : PropertyChangedBase, IDisposable
|
|||||||
_viewModelFactory = viewModelFactory;
|
_viewModelFactory = viewModelFactory;
|
||||||
_dialogManager = dialogManager;
|
_dialogManager = dialogManager;
|
||||||
|
|
||||||
Progress.Bind(o => o.Current, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
Progress.Bind(
|
||||||
|
o => o.Current,
|
||||||
|
(_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanCancel => Status is DownloadStatus.Enqueued or DownloadStatus.Started;
|
public bool CanCancel => Status is DownloadStatus.Enqueued or DownloadStatus.Started;
|
||||||
|
35
YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs
Normal file
35
YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Stylet;
|
||||||
|
using YoutubeDownloader.Services;
|
||||||
|
using YoutubeDownloader.ViewModels.Framework;
|
||||||
|
|
||||||
|
namespace YoutubeDownloader.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
public class AuthSetupViewModel : DialogScreen
|
||||||
|
{
|
||||||
|
private readonly SettingsService _settingsService;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string>? Cookies
|
||||||
|
{
|
||||||
|
get => _settingsService.LastAuthCookies;
|
||||||
|
set => _settingsService.LastAuthCookies = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAuthenticated => Cookies?.Any() == true;
|
||||||
|
|
||||||
|
public AuthSetupViewModel(SettingsService settingsService)
|
||||||
|
{
|
||||||
|
_settingsService = settingsService;
|
||||||
|
|
||||||
|
_settingsService.BindAndInvoke(
|
||||||
|
o => o.LastAuthCookies,
|
||||||
|
(_, _) => NotifyOfPropertyChange(() => Cookies)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.BindAndInvoke(
|
||||||
|
o => o.Cookies,
|
||||||
|
(_, _) => NotifyOfPropertyChange(() => IsAuthenticated)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using YoutubeDownloader.Services;
|
|
||||||
using YoutubeDownloader.ViewModels.Framework;
|
|
||||||
|
|
||||||
namespace YoutubeDownloader.ViewModels.Dialogs;
|
|
||||||
|
|
||||||
public class BrowserViewModel : DialogScreen
|
|
||||||
{
|
|
||||||
private readonly SettingsService _settingsService;
|
|
||||||
|
|
||||||
public BrowserViewModel(SettingsService settingsService) => _settingsService = settingsService;
|
|
||||||
|
|
||||||
public Dictionary<string, string> Cookies
|
|
||||||
{
|
|
||||||
get => _settingsService.Cookies;
|
|
||||||
set => _settingsService.Cookies = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? PageId
|
|
||||||
{
|
|
||||||
get => _settingsService.PageId;
|
|
||||||
set => _settingsService.PageId = value;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using YoutubeDownloader.Services;
|
using YoutubeDownloader.Services;
|
||||||
using YoutubeDownloader.ViewModels.Framework;
|
using YoutubeDownloader.ViewModels.Framework;
|
||||||
|
|
||||||
@ -10,15 +7,6 @@ namespace YoutubeDownloader.ViewModels.Dialogs;
|
|||||||
public class SettingsViewModel : DialogScreen
|
public class SettingsViewModel : DialogScreen
|
||||||
{
|
{
|
||||||
private readonly SettingsService _settingsService;
|
private readonly SettingsService _settingsService;
|
||||||
private readonly IViewModelFactory _viewModelFactory;
|
|
||||||
private readonly DialogManager _dialogManager;
|
|
||||||
|
|
||||||
public SettingsViewModel(SettingsService settingsService, IViewModelFactory viewModelFactory, DialogManager dialogManager)
|
|
||||||
{
|
|
||||||
_settingsService = settingsService;
|
|
||||||
_viewModelFactory = viewModelFactory;
|
|
||||||
_dialogManager = dialogManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsAutoUpdateEnabled
|
public bool IsAutoUpdateEnabled
|
||||||
{
|
{
|
||||||
@ -32,6 +20,12 @@ public class SettingsViewModel : DialogScreen
|
|||||||
set => _settingsService.IsDarkModeEnabled = value;
|
set => _settingsService.IsDarkModeEnabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsAuthPersisted
|
||||||
|
{
|
||||||
|
get => _settingsService.IsAuthPersisted;
|
||||||
|
set => _settingsService.IsAuthPersisted = value;
|
||||||
|
}
|
||||||
|
|
||||||
public bool ShouldInjectTags
|
public bool ShouldInjectTags
|
||||||
{
|
{
|
||||||
get => _settingsService.ShouldInjectTags;
|
get => _settingsService.ShouldInjectTags;
|
||||||
@ -55,22 +49,7 @@ public class SettingsViewModel : DialogScreen
|
|||||||
get => _settingsService.ParallelLimit;
|
get => _settingsService.ParallelLimit;
|
||||||
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Login()
|
public SettingsViewModel(SettingsService settingsService) =>
|
||||||
{
|
_settingsService = settingsService;
|
||||||
Close();
|
|
||||||
await _dialogManager.ShowDialogAsync(
|
|
||||||
_viewModelFactory.CreateBrowserSettingsViewModel()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Logout()
|
|
||||||
{
|
|
||||||
_settingsService.Cookies = new Dictionary<string, string>();
|
|
||||||
_settingsService.PageId = null;
|
|
||||||
Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsLogged => _settingsService.Cookies.Any();
|
|
||||||
public bool IsNotLogged => !IsLogged;
|
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ public interface IViewModelFactory
|
|||||||
|
|
||||||
MessageBoxViewModel CreateMessageBoxViewModel();
|
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||||
|
|
||||||
|
AuthSetupViewModel CreateAuthSetupViewModel();
|
||||||
|
|
||||||
SettingsViewModel CreateSettingsViewModel();
|
SettingsViewModel CreateSettingsViewModel();
|
||||||
|
|
||||||
BrowserViewModel CreateBrowserSettingsViewModel();
|
|
||||||
}
|
}
|
@ -23,6 +23,7 @@
|
|||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Query and process query button -->
|
<!-- Query and process query button -->
|
||||||
@ -77,11 +78,29 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</materialDesign:Card>
|
</materialDesign:Card>
|
||||||
|
|
||||||
<!-- Settings button -->
|
<!-- Auth button -->
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="6"
|
Margin="6"
|
||||||
Padding="4"
|
Padding="4"
|
||||||
|
Command="{s:Action ShowAuthSetup}"
|
||||||
|
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||||
|
ToolTip="Authentication">
|
||||||
|
<Button.Resources>
|
||||||
|
<SolidColorBrush x:Key="MaterialDesignFlatButtonClick" Color="#4C4C4C" />
|
||||||
|
</Button.Resources>
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Kind="AccountKey" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Settings button -->
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="6"
|
||||||
|
Padding="4"
|
||||||
Command="{s:Action ShowSettings}"
|
Command="{s:Action ShowSettings}"
|
||||||
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
||||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||||
@ -111,8 +130,8 @@
|
|||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Visibility="{Binding IsDownloadsAvailable, Converter={x:Static converters:BoolToVisibilityConverter.CollapsedOrVisible}}">
|
Visibility="{Binding IsDownloadsAvailable, Converter={x:Static converters:BoolToVisibilityConverter.CollapsedOrVisible}}">
|
||||||
<materialDesign:PackIcon
|
<materialDesign:PackIcon
|
||||||
Width="258"
|
Width="256"
|
||||||
Height="258"
|
Height="256"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Foreground="{DynamicResource MaterialDesignDivider}"
|
Foreground="{DynamicResource MaterialDesignDivider}"
|
||||||
Kind="Youtube" />
|
Kind="Youtube" />
|
||||||
|
99
YoutubeDownloader/Views/Dialogs/AuthSetupView.xaml
Normal file
99
YoutubeDownloader/Views/Dialogs/AuthSetupView.xaml
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="YoutubeDownloader.Views.Dialogs.AuthSetupView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:dialogs="clr-namespace:YoutubeDownloader.ViewModels.Dialogs"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
|
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||||
|
Width="450"
|
||||||
|
Height="450"
|
||||||
|
d:DataContext="{d:DesignInstance Type=dialogs:AuthSetupViewModel}"
|
||||||
|
Style="{DynamicResource MaterialDesignRoot}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="16"
|
||||||
|
FontSize="19"
|
||||||
|
FontWeight="Light"
|
||||||
|
Text="Authentication" />
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="1"
|
||||||
|
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||||
|
BorderThickness="0,1">
|
||||||
|
<Grid>
|
||||||
|
<!-- Current auth info & logout -->
|
||||||
|
<StackPanel
|
||||||
|
Margin="16"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{Binding IsAuthenticated, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="196"
|
||||||
|
Height="196"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MaterialDesignDivider}"
|
||||||
|
Kind="AccountCheck" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="18"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
<Run FontWeight="Light" Text="You are currently authenticated" />
|
||||||
|
<LineBreak />
|
||||||
|
<Hyperlink x:Name="LogoutHyperlink" Click="LogoutHyperlink_OnClick">
|
||||||
|
Log out
|
||||||
|
</Hyperlink>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Placeholder -->
|
||||||
|
<TextBlock
|
||||||
|
Margin="16"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="18"
|
||||||
|
Text="Loading..."
|
||||||
|
Visibility="{Binding IsAuthenticated, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}" />
|
||||||
|
|
||||||
|
<!-- Browser -->
|
||||||
|
<wpf:WebView2
|
||||||
|
x:Name="WebBrowser"
|
||||||
|
CoreWebView2InitializationCompleted="WebBrowser_OnCoreWebView2InitializationCompleted"
|
||||||
|
Loaded="WebBrowser_OnLoaded"
|
||||||
|
NavigationCompleted="WebBrowser_OnNavigationCompleted"
|
||||||
|
NavigationStarting="WebBrowser_OnNavigationStarting"
|
||||||
|
Unloaded="WebBrowser_OnUnloaded"
|
||||||
|
Visibility="{Binding IsAuthenticated, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||||
|
<wpf:WebView2.CreationProperties>
|
||||||
|
<wpf:CoreWebView2CreationProperties IsInPrivateModeEnabled="True" />
|
||||||
|
</wpf:WebView2.CreationProperties>
|
||||||
|
</wpf:WebView2>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Close button -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="16"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{s:Action Close}"
|
||||||
|
Content="CLOSE"
|
||||||
|
IsCancel="True"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
75
YoutubeDownloader/Views/Dialogs/AuthSetupView.xaml.cs
Normal file
75
YoutubeDownloader/Views/Dialogs/AuthSetupView.xaml.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Windows;
|
||||||
|
using Microsoft.Web.WebView2.Core;
|
||||||
|
using YoutubeDownloader.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace YoutubeDownloader.Views.Dialogs;
|
||||||
|
|
||||||
|
public partial class AuthSetupView
|
||||||
|
{
|
||||||
|
private const string HomePageUrl = "https://www.youtube.com";
|
||||||
|
private static readonly string LoginPageUrl =
|
||||||
|
$"https://accounts.google.com/ServiceLogin?continue={WebUtility.UrlEncode(HomePageUrl)}";
|
||||||
|
|
||||||
|
private AuthSetupViewModel ViewModel => (AuthSetupViewModel) DataContext;
|
||||||
|
|
||||||
|
public AuthSetupView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToLoginPage() => WebBrowser.Source = new Uri(LoginPageUrl);
|
||||||
|
|
||||||
|
private void LogoutHyperlink_OnClick(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
ViewModel.Cookies = null;
|
||||||
|
NavigateToLoginPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WebBrowser_OnLoaded(object sender, RoutedEventArgs args) => NavigateToLoginPage();
|
||||||
|
|
||||||
|
private void WebBrowser_OnCoreWebView2InitializationCompleted(
|
||||||
|
object? sender,
|
||||||
|
CoreWebView2InitializationCompletedEventArgs args)
|
||||||
|
{
|
||||||
|
if (!args.IsSuccess)
|
||||||
|
return;
|
||||||
|
|
||||||
|
WebBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
|
||||||
|
WebBrowser.CoreWebView2.Settings.AreDevToolsEnabled = false;
|
||||||
|
WebBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
|
||||||
|
WebBrowser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false;
|
||||||
|
WebBrowser.CoreWebView2.Settings.IsStatusBarEnabled = false;
|
||||||
|
WebBrowser.CoreWebView2.Settings.IsSwipeNavigationEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WebBrowser_OnNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args)
|
||||||
|
{
|
||||||
|
// Reset existing browser cookies if the user is attempting to log in (again)
|
||||||
|
if (string.Equals(args.Uri, LoginPageUrl, StringComparison.OrdinalIgnoreCase))
|
||||||
|
WebBrowser.CoreWebView2.CookieManager.DeleteAllCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void WebBrowser_OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs args)
|
||||||
|
{
|
||||||
|
var url = WebBrowser.Source.AbsoluteUri.TrimEnd('/');
|
||||||
|
|
||||||
|
// Navigated to the home page (presumably after a successful login)
|
||||||
|
if (string.Equals(url, HomePageUrl, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Extract the cookies that the browser received after logging in
|
||||||
|
var cookies = await WebBrowser.CoreWebView2.CookieManager.GetCookiesAsync("");
|
||||||
|
ViewModel.Cookies = cookies.ToDictionary(i => i.Name, i => i.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void WebBrowser_OnUnloaded(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
// This will most likely not work because WebView2 would still be running at this point,
|
||||||
|
// and there doesn't seem to be any way to shut it down using the .NET API.
|
||||||
|
if (WebBrowser.CoreWebView2?.Profile is not null)
|
||||||
|
await WebBrowser.CoreWebView2.Profile.ClearBrowsingDataAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +0,0 @@
|
|||||||
<UserControl x:Class="YoutubeDownloader.Views.Dialogs.BrowserView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:dialogs="clr-namespace:YoutubeDownloader.ViewModels.Dialogs"
|
|
||||||
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
|
||||||
d:DataContext="{d:DesignInstance Type=dialogs:BrowserViewModel}"
|
|
||||||
Style="{DynamicResource MaterialDesignRoot}"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Width="380"
|
|
||||||
Height="450">
|
|
||||||
<Grid>
|
|
||||||
<wpf:WebView2
|
|
||||||
x:Name="WebBrowser"
|
|
||||||
Source="https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com"
|
|
||||||
NavigationCompleted="WebBrowser_OnNavigationCompleted"/>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@ -1,42 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using JsonExtensions;
|
|
||||||
using Microsoft.Web.WebView2.Core;
|
|
||||||
using YoutubeDownloader.Core.Utils;
|
|
||||||
using YoutubeDownloader.ViewModels.Dialogs;
|
|
||||||
|
|
||||||
namespace YoutubeDownloader.Views.Dialogs;
|
|
||||||
|
|
||||||
public partial class BrowserView
|
|
||||||
{
|
|
||||||
public BrowserView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private BrowserViewModel ViewModel => (BrowserViewModel) DataContext;
|
|
||||||
|
|
||||||
private async void WebBrowser_OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
|
|
||||||
{
|
|
||||||
var cookies = await WebBrowser.CoreWebView2.CookieManager.GetCookiesAsync("https://www.youtube.com");
|
|
||||||
var isOnYoutube = WebBrowser.Source.AbsoluteUri == "https://www.youtube.com/";
|
|
||||||
|
|
||||||
if (cookies.Any() && isOnYoutube)
|
|
||||||
{
|
|
||||||
var cookiesDic = cookies!.ToDictionary(i => i.Name, i => i.Value);
|
|
||||||
ViewModel.Cookies = cookiesDic;
|
|
||||||
|
|
||||||
var result = await Http.Client.GetStringAsync("https://www.youtube.com/getDatasyncIdsEndpoint");
|
|
||||||
var datasyncIds = string.Join("\n",result.Split("\n").Skip(1));
|
|
||||||
var dataSyncIdJson = Json.TryParse(datasyncIds);
|
|
||||||
var dataSyncId = dataSyncIdJson?.GetProperty("responseContext").GetProperty("mainAppWebResponseContext").GetProperty("datasyncId").GetString()?.Split("||");
|
|
||||||
var isRequired = dataSyncId?.Length >= 2 && !string.IsNullOrWhiteSpace(dataSyncId?[0]) && !string.IsNullOrWhiteSpace(dataSyncId[1]);
|
|
||||||
var id = dataSyncId?[0];
|
|
||||||
if (isRequired && id is not null) ViewModel.PageId = id;
|
|
||||||
|
|
||||||
|
|
||||||
await WebBrowser.CoreWebView2.Profile.ClearBrowsingDataAsync();
|
|
||||||
WebBrowser.Dispose();
|
|
||||||
ViewModel.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,9 @@
|
|||||||
x:Class="YoutubeDownloader.Views.Dialogs.SettingsView"
|
x:Class="YoutubeDownloader.Views.Dialogs.SettingsView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:YoutubeDownloader.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:dialogs="clr-namespace:YoutubeDownloader.ViewModels.Dialogs"
|
xmlns:dialogs="clr-namespace:YoutubeDownloader.ViewModels.Dialogs"
|
||||||
xmlns:converters="clr-namespace:YoutubeDownloader.Converters"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:s="https://github.com/canton7/Stylet"
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
Width="380"
|
Width="380"
|
||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Margin="16,16,16,8"
|
Margin="16"
|
||||||
FontSize="19"
|
FontSize="19"
|
||||||
FontWeight="Light"
|
FontWeight="Light"
|
||||||
Text="Settings" />
|
Text="Settings" />
|
||||||
@ -68,10 +68,27 @@
|
|||||||
x:Name="DarkModeToggleButton"
|
x:Name="DarkModeToggleButton"
|
||||||
Margin="16,8"
|
Margin="16,8"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Checked="DarkModeToggleButton_Checked"
|
Checked="DarkModeToggleButton_OnChecked"
|
||||||
DockPanel.Dock="Right"
|
DockPanel.Dock="Right"
|
||||||
IsChecked="{Binding IsDarkModeEnabled}"
|
IsChecked="{Binding IsDarkModeEnabled}"
|
||||||
Unchecked="DarkModeToggleButton_Unchecked" />
|
Unchecked="DarkModeToggleButton_OnUnchecked" />
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Persist authentication -->
|
||||||
|
<DockPanel
|
||||||
|
Background="Transparent"
|
||||||
|
LastChildFill="False"
|
||||||
|
ToolTip="Save authentication cookies to a file so that they can be persisted between sessions">
|
||||||
|
<TextBlock
|
||||||
|
Margin="16,8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
DockPanel.Dock="Left"
|
||||||
|
Text="Persist authentication" />
|
||||||
|
<ToggleButton
|
||||||
|
Margin="16,8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
DockPanel.Dock="Right"
|
||||||
|
IsChecked="{Binding IsAuthPersisted}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- Inject tags -->
|
<!-- Inject tags -->
|
||||||
@ -169,37 +186,17 @@
|
|||||||
Value="{Binding ParallelLimit}" />
|
Value="{Binding ParallelLimit}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<Button
|
|
||||||
Margin="16"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Command="{s:Action Logout}"
|
|
||||||
Content="Logout"
|
|
||||||
IsCancel="True"
|
|
||||||
IsDefault="True"
|
|
||||||
Visibility="{Binding IsLogged, Converter={x:Static converters:BoolToVisibilityConverter.VisibleOrCollapsed}}"
|
|
||||||
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
Margin="16"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Command="{s:Action Login}"
|
|
||||||
Content="Login"
|
|
||||||
IsCancel="True"
|
|
||||||
IsDefault="True"
|
|
||||||
Visibility="{Binding IsNotLogged, Converter={x:Static converters:BoolToVisibilityConverter.VisibleOrCollapsed}}"
|
|
||||||
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Save button -->
|
<!-- Close button -->
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Margin="16"
|
Margin="16"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Command="{s:Action Close}"
|
Command="{s:Action Close}"
|
||||||
Content="SAVE"
|
Content="CLOSE"
|
||||||
IsCancel="True"
|
IsCancel="True"
|
||||||
IsDefault="True"
|
IsDefault="True"
|
||||||
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
||||||
|
@ -9,9 +9,9 @@ public partial class SettingsView
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DarkModeToggleButton_Checked(object sender, RoutedEventArgs args) =>
|
private void DarkModeToggleButton_OnChecked(object sender, RoutedEventArgs args) =>
|
||||||
App.SetDarkTheme();
|
App.SetDarkTheme();
|
||||||
|
|
||||||
private void DarkModeToggleButton_Unchecked(object sender, RoutedEventArgs args) =>
|
private void DarkModeToggleButton_OnUnchecked(object sender, RoutedEventArgs args) =>
|
||||||
App.SetLightTheme();
|
App.SetLightTheme();
|
||||||
}
|
}
|
@ -2,12 +2,12 @@
|
|||||||
x:Class="YoutubeDownloader.Views.RootView"
|
x:Class="YoutubeDownloader.Views.RootView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:YoutubeDownloader.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:s="https://github.com/canton7/Stylet"
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
xmlns:viewModels="clr-namespace:YoutubeDownloader.ViewModels"
|
xmlns:viewModels="clr-namespace:YoutubeDownloader.ViewModels"
|
||||||
xmlns:converters="clr-namespace:YoutubeDownloader.Converters"
|
|
||||||
Width="600"
|
Width="600"
|
||||||
Height="580"
|
Height="580"
|
||||||
d:DataContext="{d:DesignInstance Type=viewModels:RootViewModel}"
|
d:DataContext="{d:DesignInstance Type=viewModels:RootViewModel}"
|
||||||
@ -21,7 +21,6 @@
|
|||||||
</Window.TaskbarItemInfo>
|
</Window.TaskbarItemInfo>
|
||||||
<materialDesign:DialogHost
|
<materialDesign:DialogHost
|
||||||
x:Name="DialogHost"
|
x:Name="DialogHost"
|
||||||
CloseOnClickAway="True"
|
|
||||||
Loaded="{s:Action OnViewFullyLoaded}"
|
Loaded="{s:Action OnViewFullyLoaded}"
|
||||||
SnackbarMessageQueue="{Binding Notifications}"
|
SnackbarMessageQueue="{Binding Notifications}"
|
||||||
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user