Crunchy-Downloader/CRD/ViewModels/AddDownloadPageViewModel.cs

611 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
using CRD.Views;
using DynamicData;
using FluentAvalonia.Core;
using ReactiveUI;
namespace CRD.ViewModels;
public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty]
private string _urlInput = "";
[ObservableProperty]
private string _buttonText = "Enter Url";
[ObservableProperty]
private string _buttonTextSelectSeason = "Select Season";
[ObservableProperty]
private bool _addAllEpisodes = false;
[ObservableProperty]
private bool _buttonEnabled = false;
[ObservableProperty]
private bool _allButtonEnabled = false;
[ObservableProperty]
private bool _showLoading = false;
[ObservableProperty]
private bool _searchEnabled = false;
[ObservableProperty]
private bool _searchVisible = true;
[ObservableProperty]
private bool _slectSeasonVisible = false;
[ObservableProperty]
private bool _searchPopupVisible = false;
public ObservableCollection<ItemModel> Items{ get; } = new();
public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new();
public ObservableCollection<ItemModel> SelectedItems{ get; } = new();
[ObservableProperty]
public CrBrowseSeries _selectedSearchItem;
[ObservableProperty]
public ComboBoxItem _currentSelectedSeason;
public ObservableCollection<ComboBoxItem> SeasonList{ get; } = new();
private Dictionary<string, List<ItemModel>> episodesBySeason = new();
private List<string> selectedEpisodes = new();
private CrunchySeriesList? currentSeriesList;
private CrunchyMusicVideoList? currentMusicVideoList;
private bool CurrentSeasonFullySelected = false;
private readonly SemaphoreSlim _updateSearchSemaphore = new SemaphoreSlim(1, 1);
public AddDownloadPageViewModel(){
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
}
private async Task UpdateSearch(string value){
if (string.IsNullOrEmpty(value)){
SearchPopupVisible = false;
RaisePropertyChanged(nameof(SearchVisible));
SearchItems.Clear();
return;
}
var searchResults = await CrunchyrollManager.Instance.CrSeries.Search(value, CrunchyrollManager.Instance.CrunOptions.HistoryLang);
var searchItems = searchResults?.Data?.First().Items;
SearchItems.Clear();
if (searchItems is{ Count: > 0 }){
foreach (var episode in searchItems){
if (episode.ImageBitmap == null){
if (episode.Images.PosterTall != null){
var posterTall = episode.Images.PosterTall.First();
var imageUrl = posterTall.Find(ele => ele.Height == 180).Source
?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault().Source);
episode.LoadImage(imageUrl);
}
}
SearchItems.Add(episode);
}
SearchPopupVisible = true;
RaisePropertyChanged(nameof(SearchItems));
RaisePropertyChanged(nameof(SearchVisible));
return;
}
SearchPopupVisible = false;
RaisePropertyChanged(nameof(SearchVisible));
SearchItems.Clear();
}
#region UrlInput
partial void OnUrlInputChanged(string value){
if (SearchEnabled){
_ = UpdateSearch(value);
SetButtonProperties("Select Searched Series", false);
} else if (UrlInput.Length > 9){
EvaluateUrlInput();
} else{
SetButtonProperties("Enter Url", false);
SetVisibility(true, false);
}
}
private void EvaluateUrlInput(){
var (buttonText, isButtonEnabled) = DetermineButtonTextAndState();
SetButtonProperties(buttonText, isButtonEnabled);
SetVisibility(false, false);
}
private (string, bool) DetermineButtonTextAndState(){
return UrlInput switch{
_ when UrlInput.Contains("/artist/") => ("List Episodes", true),
_ when UrlInput.Contains("/watch/musicvideo/") => ("Add Music Video to Queue", true),
_ when UrlInput.Contains("/watch/concert/") => ("Add Concert to Queue", true),
_ when UrlInput.Contains("/watch/") => ("Add Episode to Queue", true),
_ when UrlInput.Contains("/series/") => ("List Episodes", true),
_ => ("Unknown", false),
};
}
private void SetButtonProperties(string text, bool isEnabled){
ButtonText = text;
ButtonEnabled = isEnabled;
}
private void SetVisibility(bool isSearchVisible, bool isSelectSeasonVisible){
SearchVisible = isSearchVisible;
SlectSeasonVisible = isSelectSeasonVisible;
}
#endregion
partial void OnSearchEnabledChanged(bool value){
ButtonText = SearchEnabled ? "Select Searched Series" : "Enter Url";
ButtonEnabled = false;
}
#region OnButtonPress
[RelayCommand]
public async void OnButtonPress(){
if (HasSelectedItemsOrEpisodes()){
Console.WriteLine("Added to Queue");
if (currentMusicVideoList != null){
AddSelectedMusicVideosToQueue();
} else{
AddSelectedEpisodesToQueue();
}
ResetState();
} else if (UrlInput.Length > 9){
await HandleUrlInputAsync();
} else{
Console.Error.WriteLine("Unknown input");
}
}
private bool HasSelectedItemsOrEpisodes(){
return selectedEpisodes.Count > 0 || SelectedItems.Count > 0 || AddAllEpisodes;
}
private void AddSelectedMusicVideosToQueue(){
if (SelectedItems.Count > 0){
var musicClass = CrunchyrollManager.Instance.CrMusic;
foreach (var selectedItem in SelectedItems){
var music = currentMusicVideoList.Value.Data?.FirstOrDefault(ele => ele.Id == selectedItem.Id);
if (music != null){
var meta = musicClass.EpisodeMeta(music);
QueueManager.Instance.CrAddEpMetaToQueue(meta);
}
}
}
}
private async void AddSelectedEpisodesToQueue(){
AddItemsToSelectedEpisodes();
if (currentSeriesList != null){
await QueueManager.Instance.CrAddSeriesToQueue(
currentSeriesList.Value,
new CrunchyMultiDownload(
CrunchyrollManager.Instance.CrunOptions.DubLang,
AddAllEpisodes,
false,
selectedEpisodes));
}
}
private void AddItemsToSelectedEpisodes(){
foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum);
}
}
}
private void ResetState(){
currentMusicVideoList = null;
UrlInput = "";
selectedEpisodes.Clear();
SelectedItems.Clear();
Items.Clear();
currentSeriesList = null;
SeasonList.Clear();
episodesBySeason.Clear();
AllButtonEnabled = false;
AddAllEpisodes = false;
ButtonText = "Enter Url";
ButtonEnabled = false;
SearchVisible = true;
SlectSeasonVisible = false;
}
private async Task HandleUrlInputAsync(){
episodesBySeason.Clear();
SeasonList.Clear();
var matchResult = ExtractLocaleAndIdFromUrl();
if (matchResult is (string locale, string id)){
switch (GetUrlType()){
case CrunchyUrlType.Artist:
await HandleArtistUrlAsync(locale, id);
break;
case CrunchyUrlType.MusicVideo:
HandleMusicVideoUrl(id);
break;
case CrunchyUrlType.Concert:
HandleConcertUrl(id);
break;
case CrunchyUrlType.Episode:
HandleEpisodeUrl(locale, id);
break;
case CrunchyUrlType.Series:
await HandleSeriesUrlAsync(locale, id);
break;
default:
Console.Error.WriteLine("Unknown input");
break;
}
}
}
private (string locale, string id)? ExtractLocaleAndIdFromUrl(){
var match = Regex.Match(UrlInput, "/([^/]+)/(?:artist|watch|series)(?:/(?:musicvideo|concert))?/([^/]+)/?");
return match.Success ? (match.Groups[1].Value, match.Groups[2].Value) : null;
}
private CrunchyUrlType GetUrlType(){
return UrlInput switch{
_ when UrlInput.Contains("/artist/") => CrunchyUrlType.Artist,
_ when UrlInput.Contains("/watch/musicvideo/") => CrunchyUrlType.MusicVideo,
_ when UrlInput.Contains("/watch/concert/") => CrunchyUrlType.Concert,
_ when UrlInput.Contains("/watch/") => CrunchyUrlType.Episode,
_ when UrlInput.Contains("/series/") => CrunchyUrlType.Series,
_ => CrunchyUrlType.Unknown,
};
}
private async Task HandleArtistUrlAsync(string locale, string id){
SetLoadingState(true);
var list = await CrunchyrollManager.Instance.CrMusic.ParseArtistMusicVideosByIdAsync(
id, DetermineLocale(locale), true);
SetLoadingState(false);
if (list != null){
currentMusicVideoList = list;
PopulateItemsFromMusicVideoList();
UpdateUiForSelection();
}
}
private void HandleMusicVideoUrl(string id){
_ = QueueManager.Instance.CrAddMusicVideoToQueue(id);
ResetState();
}
private void HandleConcertUrl(string id){
_ = QueueManager.Instance.CrAddConcertToQueue(id);
ResetState();
}
private void HandleEpisodeUrl(string locale, string id){
_ = QueueManager.Instance.CrAddEpisodeToQueue(
id, DetermineLocale(locale),
CrunchyrollManager.Instance.CrunOptions.DubLang, true);
ResetState();
}
private async Task HandleSeriesUrlAsync(string locale, string id){
SetLoadingState(true);
var list = await CrunchyrollManager.Instance.CrSeries.ListSeriesId(
id, DetermineLocale(locale),
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true));
SetLoadingState(false);
if (list != null){
currentSeriesList = list;
PopulateEpisodesBySeason();
UpdateUiForSelection();
} else{
ButtonEnabled = true;
}
}
private void PopulateItemsFromMusicVideoList(){
if (currentMusicVideoList?.Data != null){
foreach (var episode in currentMusicVideoList.Value.Data){
var imageUrl = episode.Images?.Thumbnail?.FirstOrDefault().Source ?? "";
var time = $"{(episode.DurationMs / 1000) / 60}:{(episode.DurationMs / 1000) % 60:D2}";
var newItem = new ItemModel(episode.Id ?? "", imageUrl, episode.Description ?? "", time, episode.Title ?? "", "",
episode.SequenceNumber.ToString(), episode.SequenceNumber.ToString(), new List<string>());
newItem.LoadImage(imageUrl);
Items.Add(newItem);
}
}
}
private void PopulateEpisodesBySeason(){
foreach (var episode in currentSeriesList?.List ?? Enumerable.Empty<Episode>()){
var seasonKey = "S" + episode.Season;
var itemModel = new ItemModel(
episode.Id, episode.Img, episode.Description, episode.Time, episode.Name, seasonKey,
episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum,
episode.E, episode.Lang);
if (!episodesBySeason.ContainsKey(seasonKey)){
episodesBySeason[seasonKey] = new List<ItemModel>{ itemModel };
SeasonList.Add(new ComboBoxItem{ Content = seasonKey });
} else{
episodesBySeason[seasonKey].Add(itemModel);
}
}
CurrentSelectedSeason = SeasonList.First();
}
private string DetermineLocale(string locale){
return string.IsNullOrEmpty(locale)
? (string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
? CrunchyrollManager.Instance.DefaultLocale
: CrunchyrollManager.Instance.CrunOptions.HistoryLang)
: Languages.Locale2language(locale).CrLocale;
}
private void SetLoadingState(bool isLoading){
ButtonEnabled = !isLoading;
ShowLoading = isLoading;
}
private void UpdateUiForSelection(){
ButtonEnabled = false;
AllButtonEnabled = true;
SlectSeasonVisible = false;
ButtonText = "Select Episodes";
}
#endregion
[RelayCommand]
public void OnSelectSeasonPressed(){
if (CurrentSeasonFullySelected){
foreach (var item in Items){
selectedEpisodes.Remove(item.AbsolutNum);
SelectedItems.Remove(item);
}
ButtonTextSelectSeason = "Select Season";
} else{
var selectedItemsSet = new HashSet<ItemModel>(SelectedItems);
foreach (var item in Items){
if (selectedItemsSet.Add(item)){
SelectedItems.Add(item);
}
}
ButtonTextSelectSeason = "Deselect Season";
}
}
partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){
foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum);
}
}
if (selectedEpisodes.Count > 0 || SelectedItems.Count > 0 || AddAllEpisodes){
ButtonText = "Add Episodes to Queue";
ButtonEnabled = true;
} else{
ButtonEnabled = false;
ButtonText = "Select Episodes";
}
}
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
if (CurrentSeasonFullySelected){
ButtonTextSelectSeason = "Deselect Season";
} else{
ButtonTextSelectSeason = "Select Season";
}
if (selectedEpisodes.Count > 0 || SelectedItems.Count > 0 || AddAllEpisodes){
ButtonText = "Add Episodes to Queue";
ButtonEnabled = true;
} else{
ButtonEnabled = false;
ButtonText = "Select Episodes";
}
}
partial void OnAddAllEpisodesChanged(bool value){
if ((selectedEpisodes.Count > 0 || SelectedItems.Count > 0 || AddAllEpisodes)){
ButtonText = "Add Episodes to Queue";
ButtonEnabled = true;
} else{
ButtonEnabled = false;
ButtonText = "Select Episodes";
}
}
#region SearchItemSelection
async partial void OnSelectedSearchItemChanged(CrBrowseSeries? value){
if (value is null || string.IsNullOrEmpty(value.Id)){
return;
}
UpdateUiForSearchSelection();
var list = await FetchSeriesListAsync(value.Id);
if (list != null){
currentSeriesList = list;
SearchPopulateEpisodesBySeason();
UpdateUiForEpisodeSelection();
} else{
ButtonEnabled = true;
}
}
private void UpdateUiForSearchSelection(){
SearchPopupVisible = false;
RaisePropertyChanged(nameof(SearchVisible));
SearchItems.Clear();
SearchVisible = false;
SlectSeasonVisible = true;
ButtonEnabled = false;
ShowLoading = true;
}
private async Task<CrunchySeriesList?> FetchSeriesListAsync(string seriesId){
var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
? CrunchyrollManager.Instance.DefaultLocale
: CrunchyrollManager.Instance.CrunOptions.HistoryLang;
return await CrunchyrollManager.Instance.CrSeries.ListSeriesId(
seriesId,
locale,
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true));
}
private void SearchPopulateEpisodesBySeason(){
if (currentSeriesList?.List == null){
return;
}
foreach (var episode in currentSeriesList.Value.List){
var seasonKey = "S" + episode.Season;
var episodeModel = new ItemModel(
episode.Id,
episode.Img,
episode.Description,
episode.Time,
episode.Name,
seasonKey,
episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum,
episode.E,
episode.Lang);
if (!episodesBySeason.ContainsKey(seasonKey)){
episodesBySeason[seasonKey] = new List<ItemModel>{ episodeModel };
SeasonList.Add(new ComboBoxItem{ Content = seasonKey });
} else{
episodesBySeason[seasonKey].Add(episodeModel);
}
}
CurrentSelectedSeason = SeasonList.First();
}
private void UpdateUiForEpisodeSelection(){
ShowLoading = false;
ButtonEnabled = false;
AllButtonEnabled = true;
ButtonText = "Select Episodes";
}
#endregion
partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){
if (value == null){
return;
}
string key = value.Content + "";
Items.Clear();
if (episodesBySeason.TryGetValue(key, out var season)){
foreach (var episode in season){
if (episode.ImageBitmap == null){
episode.LoadImage(episode.ImageUrl);
Items.Add(episode);
if (selectedEpisodes.Contains(episode.AbsolutNum)){
SelectedItems.Add(episode);
}
} else{
Items.Add(episode);
if (selectedEpisodes.Contains(episode.AbsolutNum)){
SelectedItems.Add(episode);
}
}
}
}
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
if (CurrentSeasonFullySelected){
ButtonTextSelectSeason = "Deselect Season";
} else{
ButtonTextSelectSeason = "Select Season";
}
}
}
public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
public string Id{ get; set; } = id;
public string ImageUrl{ get; set; } = imageUrl;
public Bitmap? ImageBitmap{ get; set; }
public string Title{ get; set; } = title;
public string Description{ get; set; } = description;
public string Time{ get; set; } = time;
public string Season{ get; set; } = season;
public string Episode{ get; set; } = episode;
public string AbsolutNum{ get; set; } = absolutNum;
public string TitleFull{ get; set; } = season + episode + " - " + title;
public List<string> AvailableAudios{ get; set; } = availableAudios;
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}