Add - Add search functionality to the add tab
Add - Add custom calendar option to display newly released episodes for the last 6 days and the current day Fix - Fix decryption issue with file paths containing special characters
This commit is contained in:
parent
4e98b64527
commit
acdbc7467b
|
@ -138,7 +138,7 @@ public class CrAuth{
|
|||
}
|
||||
}
|
||||
|
||||
public async void LoginWithToken(){
|
||||
public async Task LoginWithToken(){
|
||||
if (crunInstance.Token?.refresh_token == null){
|
||||
Console.Error.WriteLine("Missing Refresh Token");
|
||||
return;
|
||||
|
|
|
@ -160,7 +160,6 @@ public class CrEpisode(){
|
|||
var retMeta = new CrunchyEpMeta();
|
||||
|
||||
|
||||
|
||||
for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){
|
||||
var item = episodeP.EpisodeAndLanguages.Items[index];
|
||||
|
||||
|
@ -186,8 +185,10 @@ public class CrEpisode(){
|
|||
|
||||
var epMeta = new CrunchyEpMeta();
|
||||
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
|
||||
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
||||
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
||||
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ??
|
||||
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
||||
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ??
|
||||
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
||||
epMeta.EpisodeNumber = item.Episode;
|
||||
epMeta.EpisodeTitle = item.Title;
|
||||
epMeta.SeasonId = item.SeasonId;
|
||||
|
@ -223,7 +224,6 @@ public class CrEpisode(){
|
|||
if (retMeta.Data != null){
|
||||
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
|
||||
retMeta.Data.Add(epMetaData);
|
||||
|
||||
} else{
|
||||
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
|
||||
epMeta.Data[0] = epMetaData;
|
||||
|
@ -238,4 +238,31 @@ public class CrEpisode(){
|
|||
|
||||
return retMeta;
|
||||
}
|
||||
|
||||
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale){
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
}
|
||||
|
||||
query["n"] = "200";
|
||||
query["sort_by"] = "newly_added";
|
||||
query["type"] = "episode";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
|
@ -11,13 +11,13 @@ using System.Web;
|
|||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Views;
|
||||
using DynamicData;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace CRD.Downloader;
|
||||
|
||||
public class CrSeries(){
|
||||
|
||||
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
|
||||
|
||||
public async Task<List<CrunchyEpMeta>> DownloadFromSeriesId(string id, CrunchyMultiDownload data){
|
||||
|
@ -156,7 +156,6 @@ public class CrSeries(){
|
|||
int fallbackIndex = 0;
|
||||
var seasonData = await GetSeasonDataById(s.Id, "");
|
||||
if (seasonData.Data != null){
|
||||
|
||||
if (crunInstance.CrunOptions.History){
|
||||
crunInstance.CrHistory.UpdateWithSeasonData(seasonData);
|
||||
}
|
||||
|
@ -288,14 +287,8 @@ public class CrSeries(){
|
|||
public async Task<CrunchyEpisodeList> GetSeasonDataById(string seasonID, string? crLocale, bool forcedLang = false, bool log = false){
|
||||
CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List<CrunchyEpisode>(), Total = 0, Meta = new Meta() };
|
||||
|
||||
if (crunInstance.CmsToken?.Cms == null){
|
||||
Console.Error.WriteLine("Missing CMS Token");
|
||||
return episodeList;
|
||||
}
|
||||
|
||||
NameValueCollection query;
|
||||
if (log){
|
||||
|
||||
query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
query["preferred_audio_language"] = "ja-JP";
|
||||
|
@ -377,11 +370,6 @@ public class CrSeries(){
|
|||
}
|
||||
|
||||
public async Task<CrSeriesSearch?> ParseSeriesById(string id, string? crLocale, bool forced = false){
|
||||
if (crunInstance.CmsToken?.Cms == null){
|
||||
Console.Error.WriteLine("Missing CMS Access Token");
|
||||
return null;
|
||||
}
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
query["preferred_audio_language"] = "ja-JP";
|
||||
|
@ -390,7 +378,6 @@ public class CrSeries(){
|
|||
if (forced){
|
||||
query["force_locale"] = crLocale;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -414,11 +401,6 @@ public class CrSeries(){
|
|||
}
|
||||
|
||||
public async Task<CrSeriesBase?> SeriesById(string id, string? crLocale, bool forced = false){
|
||||
if (crunInstance.CmsToken?.Cms == null){
|
||||
Console.Error.WriteLine("Missing CMS Access Token");
|
||||
return null;
|
||||
}
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
query["preferred_audio_language"] = "ja-JP";
|
||||
|
@ -427,7 +409,6 @@ public class CrSeries(){
|
|||
if (forced){
|
||||
query["force_locale"] = crLocale;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}", HttpMethod.Get, true, true, query);
|
||||
|
@ -449,4 +430,71 @@ public class CrSeries(){
|
|||
return series;
|
||||
}
|
||||
|
||||
|
||||
public async Task<CrSearchSeriesBase?> Search(string searchString,string? crLocale){
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
}
|
||||
query["q"] = searchString;
|
||||
query["n"] = "6";
|
||||
query["type"] = "top_results";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{Api.Search}", HttpMethod.Get, true, false, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
CrSearchSeriesBase? series = Helpers.Deserialize<CrSearchSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){
|
||||
CrBrowseSeriesBase? complete = new CrBrowseSeriesBase();
|
||||
complete.Data =[];
|
||||
|
||||
var i = 0;
|
||||
|
||||
do{
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
}
|
||||
|
||||
query["start"] = i + "";
|
||||
query["n"] = "50";
|
||||
query["sort_by"] = "alphabetical";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
CrBrowseSeriesBase? series = Helpers.Deserialize<CrBrowseSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
if (series != null){
|
||||
complete.Total = series.Total;
|
||||
if (series.Data != null) complete.Data.AddRange(series.Data);
|
||||
} else{
|
||||
break;
|
||||
}
|
||||
|
||||
i += 50;
|
||||
} while (i < complete.Total);
|
||||
|
||||
|
||||
return complete;
|
||||
}
|
||||
}
|
|
@ -51,7 +51,19 @@ public class Crunchyroll{
|
|||
#region Calendar Variables
|
||||
|
||||
private Dictionary<string, CalendarWeek> calendar = new();
|
||||
private Dictionary<string, string> calendarLanguage = new();
|
||||
private Dictionary<string, string> calendarLanguage = new(){
|
||||
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
|
||||
{ "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
|
||||
{ "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" },
|
||||
{ "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" },
|
||||
{ "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" },
|
||||
{ "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" },
|
||||
{ "de", "https://www.crunchyroll.com/de/simulcastcalendar" },
|
||||
{ "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" },
|
||||
{ "it", "https://www.crunchyroll.com/it/simulcastcalendar" },
|
||||
{ "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" },
|
||||
{ "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" },
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -123,14 +135,6 @@ public class Crunchyroll{
|
|||
HasPremium = false,
|
||||
};
|
||||
|
||||
|
||||
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
|
||||
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken);
|
||||
CrAuth.LoginWithToken();
|
||||
} else{
|
||||
await CrAuth.AuthAnonymous();
|
||||
}
|
||||
|
||||
Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}");
|
||||
|
||||
CrunOptions.AutoDownload = false;
|
||||
|
@ -164,6 +168,19 @@ public class Crunchyroll{
|
|||
|
||||
CfgManager.UpdateSettingsFromFile();
|
||||
|
||||
if (CrunOptions.LogMode){
|
||||
CfgManager.EnableLogMode();
|
||||
} else{
|
||||
CfgManager.DisableLogMode();
|
||||
}
|
||||
|
||||
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
|
||||
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken);
|
||||
CrAuth.LoginWithToken();
|
||||
} else{
|
||||
await CrAuth.AuthAnonymous();
|
||||
}
|
||||
|
||||
if (CrunOptions.History){
|
||||
if (File.Exists(CfgManager.PathCrHistory)){
|
||||
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
|
||||
|
@ -172,25 +189,8 @@ public class Crunchyroll{
|
|||
RefreshSonarr();
|
||||
}
|
||||
|
||||
if (CrunOptions.LogMode){
|
||||
CfgManager.EnableLogMode();
|
||||
} else{
|
||||
CfgManager.DisableLogMode();
|
||||
}
|
||||
|
||||
calendarLanguage = new(){
|
||||
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
|
||||
{ "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
|
||||
{ "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" },
|
||||
{ "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" },
|
||||
{ "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" },
|
||||
{ "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" },
|
||||
{ "de", "https://www.crunchyroll.com/de/simulcastcalendar" },
|
||||
{ "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" },
|
||||
{ "it", "https://www.crunchyroll.com/it/simulcastcalendar" },
|
||||
{ "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" },
|
||||
{ "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" },
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public async void RefreshSonarr(){
|
||||
|
@ -300,7 +300,7 @@ public class Crunchyroll{
|
|||
calEpisode.DateTime = episodeTime;
|
||||
calEpisode.HasPassed = hasPassed;
|
||||
calEpisode.EpisodeName = episodeName;
|
||||
calEpisode.SeasonUrl = seasonLink;
|
||||
calEpisode.SeriesUrl = seasonLink;
|
||||
calEpisode.EpisodeUrl = episodeLink;
|
||||
calEpisode.ThumbnailUrl = thumbnailUrl;
|
||||
calEpisode.IsPremiumOnly = isPremiumOnly;
|
||||
|
@ -1194,8 +1194,10 @@ public class Crunchyroll{
|
|||
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
|
||||
var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
|
||||
var commandBase = $"--show-progress --key {keyId}:{key}";
|
||||
var commandVideo = commandBase + $" \"{tempTsFile}.video.enc.m4s\" \"{tempTsFile}.video.m4s\"";
|
||||
var commandAudio = commandBase + $" \"{tempTsFile}.audio.enc.m4s\" \"{tempTsFile}.audio.m4s\"";
|
||||
var tempTsFileName = Path.GetFileName(tempTsFile);
|
||||
var tempTsFileWorkDir = Path.GetDirectoryName(tempTsFile) ?? CfgManager.PathVIDEOS_DIR;
|
||||
var commandVideo = commandBase + $" \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\"";
|
||||
var commandAudio = commandBase + $" \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\"";
|
||||
if (videoDownloaded){
|
||||
Console.WriteLine("Started decrypting video");
|
||||
data.DownloadProgress = new DownloadProgress(){
|
||||
|
@ -1206,7 +1208,7 @@ public class Crunchyroll{
|
|||
Doing = "Decrypting video"
|
||||
};
|
||||
Queue.Refresh();
|
||||
var decryptVideo = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo);
|
||||
var decryptVideo = await Helpers.ExecuteCommandAsyncWorkDir("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo,tempTsFileWorkDir);
|
||||
|
||||
if (!decryptVideo.IsOk){
|
||||
Console.Error.WriteLine($"Decryption failed with exit code {decryptVideo.ErrorCode}");
|
||||
|
@ -1216,7 +1218,14 @@ public class Crunchyroll{
|
|||
} catch (IOException ex){
|
||||
Console.WriteLine($"An error occurred: {ex.Message}");
|
||||
}
|
||||
} else{
|
||||
return new DownloadResponse{
|
||||
Data = files,
|
||||
Error = dlFailed,
|
||||
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
||||
ErrorText = "Decryption failed"
|
||||
};
|
||||
}
|
||||
|
||||
Console.WriteLine("Decryption done for video");
|
||||
if (!options.Nocleanup){
|
||||
try{
|
||||
|
@ -1249,7 +1258,6 @@ public class Crunchyroll{
|
|||
Lang = lang.Value,
|
||||
IsPrimary = isPrimary
|
||||
});
|
||||
}
|
||||
} else{
|
||||
Console.WriteLine("No Video downloaded");
|
||||
}
|
||||
|
@ -1264,7 +1272,7 @@ public class Crunchyroll{
|
|||
Doing = "Decrypting audio"
|
||||
};
|
||||
Queue.Refresh();
|
||||
var decryptAudio = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio);
|
||||
var decryptAudio = await Helpers.ExecuteCommandAsyncWorkDir("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio,tempTsFileWorkDir);
|
||||
|
||||
if (!decryptAudio.IsOk){
|
||||
Console.Error.WriteLine($"Decryption failed with exit code {decryptAudio.ErrorCode}");
|
||||
|
@ -1273,7 +1281,14 @@ public class Crunchyroll{
|
|||
} catch (IOException ex){
|
||||
Console.WriteLine($"An error occurred: {ex.Message}");
|
||||
}
|
||||
} else{
|
||||
return new DownloadResponse{
|
||||
Data = files,
|
||||
Error = dlFailed,
|
||||
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
||||
ErrorText = "Decryption failed"
|
||||
};
|
||||
}
|
||||
|
||||
Console.WriteLine("Decryption done for audio");
|
||||
if (!options.Nocleanup){
|
||||
try{
|
||||
|
@ -1306,7 +1321,6 @@ public class Crunchyroll{
|
|||
Lang = lang.Value,
|
||||
IsPrimary = isPrimary
|
||||
});
|
||||
}
|
||||
} else{
|
||||
Console.WriteLine("No Audio downloaded");
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CRD.Utils;
|
||||
|
@ -194,6 +196,47 @@ public class Helpers{
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){
|
||||
try{
|
||||
using (var process = new Process()){
|
||||
process.StartInfo.WorkingDirectory = workingDir;
|
||||
process.StartInfo.FileName = bin;
|
||||
process.StartInfo.Arguments = command;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
process.OutputDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
Console.WriteLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
Console.WriteLine($"{e.Data}");
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
// Define success condition more appropriately based on the application
|
||||
bool isSuccess = process.ExitCode == 0;
|
||||
|
||||
return (IsOk: isSuccess, ErrorCode: process.ExitCode);
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred: {ex.Message}");
|
||||
return (IsOk: false, ErrorCode: -1);
|
||||
}
|
||||
}
|
||||
|
||||
public static double CalculateCosineSimilarity(string text1, string text2){
|
||||
var vector1 = ComputeWordFrequency(text1);
|
||||
var vector2 = ComputeWordFrequency(text2);
|
||||
|
@ -272,4 +315,23 @@ public class Helpers{
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task<Bitmap?> LoadImage(string imageUrl){
|
||||
try{
|
||||
using (var client = new HttpClient()){
|
||||
var response = await client.GetAsync(imageUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using (var stream = await response.Content.ReadAsStreamAsync()){
|
||||
return new Bitmap(stream);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex){
|
||||
// Handle exceptions
|
||||
Console.Error.WriteLine("Failed to load image: " + ex.Message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -123,6 +123,7 @@ public static class Api{
|
|||
public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile";
|
||||
public static readonly string BetaCmsToken = ApiBeta + "/index/v2";
|
||||
public static readonly string Search = ApiBeta + "/content/v2/discover/search";
|
||||
public static readonly string Browse = ApiBeta + "/content/v2/discover/browse";
|
||||
public static readonly string Cms = ApiBeta + "/content/v2/cms";
|
||||
public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
|
||||
public static readonly string BetaCms = ApiBeta + "/cms/v2";
|
||||
|
|
|
@ -26,7 +26,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
|||
public DateTime? DateTime{ get; set; }
|
||||
public bool? HasPassed{ get; set; }
|
||||
public string? EpisodeName{ get; set; }
|
||||
public string? SeasonUrl{ get; set; }
|
||||
public string? SeriesUrl{ get; set; }
|
||||
public string? EpisodeUrl{ get; set; }
|
||||
public string? ThumbnailUrl{ get; set; }
|
||||
public Bitmap? ImageBitmap{ get; set; }
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CRD.Utils.Structs;
|
||||
|
||||
public class CrBrowseEpisodeBase{
|
||||
public int Total{ get; set; }
|
||||
public List<CrBrowseEpisode>? Data{ get; set; }
|
||||
public Meta Meta{ get; set; }
|
||||
}
|
||||
|
||||
public class CrBrowseEpisode : INotifyPropertyChanged{
|
||||
[JsonProperty("external_id")]
|
||||
public string? ExternalId{ get; set; }
|
||||
|
||||
[JsonProperty("last_public")]
|
||||
public DateTime LastPublic{ get; set; }
|
||||
|
||||
public string? Description{ get; set; }
|
||||
|
||||
public bool New{ get; set; }
|
||||
|
||||
[JsonProperty("linked_resource_key")]
|
||||
public string? LinkedResourceKey{ get; set; }
|
||||
|
||||
[JsonProperty("slug_title")]
|
||||
public string? SlugTitle{ get; set; }
|
||||
|
||||
public string? Title{ get; set; }
|
||||
|
||||
[JsonProperty("promo_title")]
|
||||
public string? PromoTitle{ get; set; }
|
||||
|
||||
[JsonProperty("episode_metadata")]
|
||||
public CrBrowseEpisodeMetaData EpisodeMetadata{ get; set; }
|
||||
|
||||
public string? Id{ get; set; }
|
||||
|
||||
public Images Images{ get; set; }
|
||||
|
||||
[JsonProperty("promo_description")]
|
||||
public string? PromoDescription{ get; set; }
|
||||
|
||||
public string? Slug{ get; set; }
|
||||
|
||||
public string? Type{ get; set; }
|
||||
|
||||
[JsonProperty("channel_id")]
|
||||
public string? ChannelId{ get; set; }
|
||||
|
||||
[JsonProperty("streams_link")]
|
||||
public string? StreamsLink{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Bitmap? ImageBitmap{ get; set; }
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public async void LoadImage(string url){
|
||||
ImageBitmap = await Helpers.LoadImage(url);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class CrBrowseEpisodeMetaData{
|
||||
[JsonProperty("audio_locale")]
|
||||
public Locale? AudioLocale{ get; set; }
|
||||
|
||||
[JsonProperty("content_descriptors")]
|
||||
public List<string>? ContentDescriptors{ get; set; }
|
||||
|
||||
[JsonProperty("availability_notes")]
|
||||
public string? AvailabilityNotes{ get; set; }
|
||||
|
||||
public string? Episode{ get; set; }
|
||||
|
||||
[JsonProperty("episode_air_date")]
|
||||
public DateTime EpisodeAirDate{ get; set; }
|
||||
|
||||
[JsonProperty("episode_number")]
|
||||
public int EpisodeCount{ get; set; }
|
||||
|
||||
[JsonProperty("duration_ms")]
|
||||
public int DurationMs{ get; set; }
|
||||
|
||||
[JsonProperty("extended_maturity_rating")]
|
||||
public Dictionary<object, object>?
|
||||
ExtendedMaturityRating{ get; set; }
|
||||
|
||||
[JsonProperty("is_dubbed")]
|
||||
public bool IsDubbed{ get; set; }
|
||||
|
||||
[JsonProperty("is_mature")]
|
||||
public bool IsMature{ get; set; }
|
||||
|
||||
[JsonProperty("is_subbed")]
|
||||
public bool IsSubbed{ get; set; }
|
||||
|
||||
[JsonProperty("mature_blocked")]
|
||||
public bool MatureBlocked{ get; set; }
|
||||
|
||||
[JsonProperty("is_premium_only")]
|
||||
public bool IsPremiumOnly{ get; set; }
|
||||
|
||||
[JsonProperty("is_clip")]
|
||||
public bool IsClip{ get; set; }
|
||||
|
||||
[JsonProperty("maturity_ratings")]
|
||||
public List<string>? MaturityRatings{ get; set; }
|
||||
|
||||
[JsonProperty("season_number")]
|
||||
public double SeasonNumber{ get; set; }
|
||||
|
||||
[JsonProperty("season_sequence_number")]
|
||||
public double SeasonSequenceNumber{ get; set; }
|
||||
|
||||
[JsonProperty("sequence_number")]
|
||||
public double SequenceNumber{ get; set; }
|
||||
|
||||
[JsonProperty("upload_date")]
|
||||
public DateTime? UploadDate{ get; set; }
|
||||
|
||||
[JsonProperty("subtitle_locales")]
|
||||
public List<Locale>? SubtitleLocales{ get; set; }
|
||||
|
||||
[JsonProperty("premium_available_date")]
|
||||
public DateTime? PremiumAvailableDate{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("availability_ends")]
|
||||
public DateTime? AvailabilityEnds{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("availability_starts")]
|
||||
public DateTime? AvailabilityStarts{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("free_available_date")]
|
||||
public DateTime? FreeAvailableDate{ get; set; }
|
||||
|
||||
[JsonProperty("identifier")]
|
||||
public string? Identifier{ get; set; }
|
||||
|
||||
[JsonProperty("season_id")]
|
||||
public string? SeasonId{ get; set; }
|
||||
|
||||
[JsonProperty("series_id")]
|
||||
public string? SeriesId{ get; set; }
|
||||
|
||||
[JsonProperty("season_display_number")]
|
||||
public string? SeasonDisplayNumber{ get; set; }
|
||||
|
||||
[JsonProperty("eligible_region")]
|
||||
public string? EligibleRegion{ get; set; }
|
||||
|
||||
[JsonProperty("available_date")]
|
||||
public DateTime? AvailableDate{ get; set; }
|
||||
|
||||
[JsonProperty("premium_date")]
|
||||
public DateTime? PremiumDate{ get; set; }
|
||||
|
||||
[JsonProperty("available_offline")]
|
||||
public bool AvailableOffline{ get; set; }
|
||||
|
||||
[JsonProperty("closed_captions_available")]
|
||||
public bool ClosedCaptionsAvailable{ get; set; }
|
||||
|
||||
[JsonProperty("season_slug_title")]
|
||||
public string? SeasonSlugTitle{ get; set; }
|
||||
|
||||
[JsonProperty("season_title")]
|
||||
public string? SeasonTitle{ get; set; }
|
||||
|
||||
[JsonProperty("series_slug_title")]
|
||||
public string? SeriesSlugTitle{ get; set; }
|
||||
|
||||
[JsonProperty("series_title")]
|
||||
public string? SeriesTitle{ get; set; }
|
||||
|
||||
[JsonProperty("versions")]
|
||||
public List<CrBrowseEpisodeVersion>? versions{ get; set; }
|
||||
|
||||
}
|
||||
|
||||
public struct CrBrowseEpisodeVersion{
|
||||
[JsonProperty("audio_locale")]
|
||||
public Locale? AudioLocale{ get; set; }
|
||||
|
||||
public string? Guid{ get; set; }
|
||||
public bool? Original{ get; set; }
|
||||
public string? Variant{ get; set; }
|
||||
|
||||
[JsonProperty("season_guid")]
|
||||
public string? SeasonGuid{ get; set; }
|
||||
|
||||
[JsonProperty("media_guid")]
|
||||
public string? MediaGuid{ get; set; }
|
||||
|
||||
[JsonProperty("is_premium_only")]
|
||||
public bool? IsPremiumOnly{ get; set; }
|
||||
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CRD.Utils.Structs;
|
||||
|
||||
public class CrBrowseSeriesBase{
|
||||
public int Total{ get; set; }
|
||||
public List<CrBrowseSeries>? Data{ get; set; }
|
||||
public Meta Meta{ get; set; }
|
||||
}
|
||||
|
||||
public class CrBrowseSeries : INotifyPropertyChanged{
|
||||
[JsonProperty("external_id")]
|
||||
public string? ExternalId{ get; set; }
|
||||
|
||||
[JsonProperty("last_public")]
|
||||
public DateTime LastPublic{ get; set; }
|
||||
|
||||
public string? Description{ get; set; }
|
||||
|
||||
public bool New{ get; set; }
|
||||
|
||||
[JsonProperty("linked_resource_key")]
|
||||
public string? LinkedResourceKey{ get; set; }
|
||||
|
||||
[JsonProperty("slug_title")]
|
||||
public string? SlugTitle{ get; set; }
|
||||
|
||||
public string? Title{ get; set; }
|
||||
|
||||
[JsonProperty("promo_title")]
|
||||
public string? PromoTitle{ get; set; }
|
||||
|
||||
[JsonProperty("series_metadata")]
|
||||
public CrBrowseSeriesMetaData SeriesMetadata{ get; set; }
|
||||
|
||||
public string? Id{ get; set; }
|
||||
|
||||
public Images Images{ get; set; }
|
||||
|
||||
[JsonProperty("promo_description")]
|
||||
public string? PromoDescription{ get; set; }
|
||||
|
||||
public string? Slug{ get; set; }
|
||||
|
||||
public string? Type{ get; set; }
|
||||
|
||||
[JsonProperty("channel_id")]
|
||||
public string? ChannelId{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Bitmap? ImageBitmap{ get; set; }
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public async void LoadImage(string url){
|
||||
ImageBitmap = await Helpers.LoadImage(url);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class CrBrowseSeriesMetaData{
|
||||
[JsonProperty("audio_locales")]
|
||||
public List<Locale>? AudioLocales{ get; set; }
|
||||
|
||||
[JsonProperty("awards")]
|
||||
public List<object> awards{ get; set; }
|
||||
|
||||
[JsonProperty("availability_notes")]
|
||||
public string? AvailabilityNotes{ get; set; }
|
||||
|
||||
[JsonProperty("content_descriptors")]
|
||||
public List<string>? ContentDescriptors{ get; set; }
|
||||
|
||||
[JsonProperty("episode_count")]
|
||||
public int EpisodeCount{ get; set; }
|
||||
|
||||
[JsonProperty("extended_description")]
|
||||
public string? ExtendedDescription{ get; set; }
|
||||
|
||||
[JsonProperty("extended_maturity_rating")]
|
||||
public Dictionary<object, object>?
|
||||
ExtendedMaturityRating{ get; set; }
|
||||
|
||||
[JsonProperty("is_dubbed")]
|
||||
public bool IsDubbed{ get; set; }
|
||||
|
||||
[JsonProperty("is_mature")]
|
||||
public bool IsMature{ get; set; }
|
||||
|
||||
[JsonProperty("is_simulcast")]
|
||||
public bool IsSimulcast{ get; set; }
|
||||
|
||||
[JsonProperty("is_subbed")]
|
||||
public bool IsSubbed{ get; set; }
|
||||
|
||||
[JsonProperty("mature_blocked")]
|
||||
public bool MatureBlocked{ get; set; }
|
||||
|
||||
[JsonProperty("maturity_ratings")]
|
||||
public List<string>? MaturityRatings{ get; set; }
|
||||
|
||||
[JsonProperty("season_count")]
|
||||
public int SeasonCount{ get; set; }
|
||||
|
||||
[JsonProperty("series_launch_year")]
|
||||
public int SeriesLaunchYear{ get; set; }
|
||||
|
||||
[JsonProperty("subtitle_locales")]
|
||||
public List<Locale>? SubtitleLocales{ get; set; }
|
||||
|
||||
[JsonProperty("tenant_categories")]
|
||||
public List<string>? TenantCategories{ get; set; }
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace CRD.Utils.Structs;
|
||||
|
||||
public class CrSearchSeries{
|
||||
public int count{ get; set; }
|
||||
public List<CrBrowseSeries>? Items{ get; set; }
|
||||
public string? type{ get; set; }
|
||||
}
|
||||
|
||||
public class CrSearchSeriesBase{
|
||||
public int Total{ get; set; }
|
||||
public List<CrSearchSeries>? Data{ get; set; }
|
||||
public Meta Meta{ get; set; }
|
||||
}
|
|
@ -11,48 +11,90 @@ public class CrSeriesSearch{
|
|||
|
||||
public struct SeriesSearchItem{
|
||||
public string Description{ get; set; }
|
||||
[JsonProperty("seo_description")] public string SeoDescription{ get; set; }
|
||||
[JsonProperty("number_of_episodes")] public int NumberOfEpisodes{ get; set; }
|
||||
[JsonProperty("is_dubbed")] public bool IsDubbed{ get; set; }
|
||||
|
||||
[JsonProperty("seo_description")]
|
||||
public string SeoDescription{ get; set; }
|
||||
|
||||
[JsonProperty("number_of_episodes")]
|
||||
public int NumberOfEpisodes{ get; set; }
|
||||
|
||||
[JsonProperty("is_dubbed")]
|
||||
public bool IsDubbed{ get; set; }
|
||||
|
||||
public string Identifier{ get; set; }
|
||||
[JsonProperty("channel_id")] public string ChannelId{ get; set; }
|
||||
[JsonProperty("slug_title")] public string SlugTitle{ get; set; }
|
||||
|
||||
[JsonProperty("channel_id")]
|
||||
public string ChannelId{ get; set; }
|
||||
|
||||
[JsonProperty("slug_title")]
|
||||
public string SlugTitle{ get; set; }
|
||||
|
||||
[JsonProperty("season_sequence_number")]
|
||||
public int SeasonSequenceNumber{ get; set; }
|
||||
|
||||
[JsonProperty("season_tags")] public List<string> SeasonTags{ get; set; }
|
||||
[JsonProperty("season_tags")]
|
||||
public List<string> SeasonTags{ get; set; }
|
||||
|
||||
[JsonProperty("extended_maturity_rating")]
|
||||
public Dictionary<object, object>
|
||||
ExtendedMaturityRating{ get; set; }
|
||||
|
||||
[JsonProperty("is_mature")] public bool IsMature{ get; set; }
|
||||
[JsonProperty("audio_locale")] public string AudioLocale{ get; set; }
|
||||
[JsonProperty("season_number")] public int SeasonNumber{ get; set; }
|
||||
[JsonProperty("is_mature")]
|
||||
public bool IsMature{ get; set; }
|
||||
|
||||
[JsonProperty("audio_locale")]
|
||||
public string AudioLocale{ get; set; }
|
||||
|
||||
[JsonProperty("season_number")]
|
||||
public int SeasonNumber{ get; set; }
|
||||
|
||||
public Dictionary<object, object> Images{ get; set; }
|
||||
[JsonProperty("mature_blocked")] public bool MatureBlocked{ get; set; }
|
||||
|
||||
[JsonProperty("mature_blocked")]
|
||||
public bool MatureBlocked{ get; set; }
|
||||
|
||||
public List<Version>? Versions{ get; set; }
|
||||
public string Title{ get; set; }
|
||||
[JsonProperty("is_subbed")] public bool IsSubbed{ get; set; }
|
||||
|
||||
[JsonProperty("is_subbed")]
|
||||
public bool IsSubbed{ get; set; }
|
||||
|
||||
public string Id{ get; set; }
|
||||
[JsonProperty("audio_locales")] public List<string> AudioLocales{ get; set; }
|
||||
[JsonProperty("subtitle_locales")] public List<string> SubtitleLocales{ get; set; }
|
||||
[JsonProperty("availability_notes")] public string AvailabilityNotes{ get; set; }
|
||||
[JsonProperty("series_id")] public string SeriesId{ get; set; }
|
||||
|
||||
[JsonProperty("audio_locales")]
|
||||
public List<string> AudioLocales{ get; set; }
|
||||
|
||||
[JsonProperty("subtitle_locales")]
|
||||
public List<string> SubtitleLocales{ get; set; }
|
||||
|
||||
[JsonProperty("availability_notes")]
|
||||
public string AvailabilityNotes{ get; set; }
|
||||
|
||||
[JsonProperty("series_id")]
|
||||
public string SeriesId{ get; set; }
|
||||
|
||||
[JsonProperty("season_display_number")]
|
||||
public string SeasonDisplayNumber{ get; set; }
|
||||
|
||||
[JsonProperty("is_complete")] public bool IsComplete{ get; set; }
|
||||
[JsonProperty("is_complete")]
|
||||
public bool IsComplete{ get; set; }
|
||||
|
||||
public List<string> Keywords{ get; set; }
|
||||
[JsonProperty("maturity_ratings")] public List<string> MaturityRatings{ get; set; }
|
||||
[JsonProperty("is_simulcast")] public bool IsSimulcast{ get; set; }
|
||||
[JsonProperty("seo_title")] public string SeoTitle{ get; set; }
|
||||
|
||||
[JsonProperty("maturity_ratings")]
|
||||
public List<string> MaturityRatings{ get; set; }
|
||||
|
||||
[JsonProperty("is_simulcast")]
|
||||
public bool IsSimulcast{ get; set; }
|
||||
|
||||
[JsonProperty("seo_title")]
|
||||
public string SeoTitle{ get; set; }
|
||||
}
|
||||
|
||||
public struct Version{
|
||||
[JsonProperty("audio_locale")] public string? AudioLocale{ get; set; }
|
||||
[JsonProperty("audio_locale")]
|
||||
public string? AudioLocale{ get; set; }
|
||||
|
||||
public string? Guid{ get; set; }
|
||||
public bool? Original{ get; set; }
|
||||
public string? Variant{ get; set; }
|
||||
|
|
|
@ -10,20 +10,34 @@ public class PlaybackData{
|
|||
}
|
||||
|
||||
public class StreamDetails{
|
||||
[JsonProperty("hardsub_locale")] public Locale? HardsubLocale{ get; set; }
|
||||
[JsonProperty("hardsub_locale")]
|
||||
public Locale? HardsubLocale{ get; set; }
|
||||
|
||||
public string? Url{ get; set; }
|
||||
[JsonProperty("hardsub_lang")] public string? HardsubLang{ get; set; }
|
||||
[JsonProperty("audio_lang")] public string? AudioLang{ get; set; }
|
||||
|
||||
[JsonProperty("hardsub_lang")]
|
||||
public string? HardsubLang{ get; set; }
|
||||
|
||||
[JsonProperty("audio_lang")]
|
||||
public string? AudioLang{ get; set; }
|
||||
|
||||
public string? Type{ get; set; }
|
||||
}
|
||||
|
||||
public class PlaybackMeta{
|
||||
[JsonProperty("media_id")] public string? MediaId{ get; set; }
|
||||
[JsonProperty("media_id")]
|
||||
public string? MediaId{ get; set; }
|
||||
|
||||
public Subtitles? Subtitles{ get; set; }
|
||||
public List<string>? Bifs{ get; set; }
|
||||
public List<PlaybackVersion>? Versions{ get; set; }
|
||||
[JsonProperty("audio_locale")] public Locale? AudioLocale{ get; set; }
|
||||
[JsonProperty("closed_captions")] public Subtitles? ClosedCaptions{ get; set; }
|
||||
|
||||
[JsonProperty("audio_locale")]
|
||||
public Locale? AudioLocale{ get; set; }
|
||||
|
||||
[JsonProperty("closed_captions")]
|
||||
public Subtitles? ClosedCaptions{ get; set; }
|
||||
|
||||
public Dictionary<string, Caption>? Captions{ get; set; }
|
||||
}
|
||||
|
||||
|
@ -38,12 +52,22 @@ public class CrunchyStreams : Dictionary<string, StreamDetails>;
|
|||
public class Subtitles : Dictionary<string, SubtitleInfo>;
|
||||
|
||||
public class PlaybackVersion{
|
||||
[JsonProperty("audio_locale")] public Locale AudioLocale{ get; set; } // Assuming Locale is defined elsewhere
|
||||
[JsonProperty("audio_locale")]
|
||||
public Locale AudioLocale{ get; set; } // Assuming Locale is defined elsewhere
|
||||
|
||||
public string? Guid{ get; set; }
|
||||
[JsonProperty("is_premium_only")] public bool IsPremiumOnly{ get; set; }
|
||||
[JsonProperty("media_guid")] public string? MediaGuid{ get; set; }
|
||||
|
||||
[JsonProperty("is_premium_only")]
|
||||
public bool IsPremiumOnly{ get; set; }
|
||||
|
||||
[JsonProperty("media_guid")]
|
||||
public string? MediaGuid{ get; set; }
|
||||
|
||||
public bool Original{ get; set; }
|
||||
[JsonProperty("season_guid")] public string? SeasonGuid{ get; set; }
|
||||
|
||||
[JsonProperty("season_guid")]
|
||||
public string? SeasonGuid{ get; set; }
|
||||
|
||||
public string? Variant{ get; set; }
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@ 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;
|
||||
|
@ -13,6 +16,7 @@ using CRD.Downloader;
|
|||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Views;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
|
||||
|
@ -37,9 +41,22 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
public bool _showLoading = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public bool _searchEnabled = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public bool _searchVisible = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public 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;
|
||||
|
||||
|
@ -51,26 +68,75 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
private CrunchySeriesList? currentSeriesList;
|
||||
|
||||
private readonly SemaphoreSlim _updateSearchSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
public AddDownloadPageViewModel(){
|
||||
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
|
||||
}
|
||||
|
||||
private async Task UpdateSearch(string value){
|
||||
var searchResults = await Crunchyroll.Instance.CrSeries.Search(value, "");
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
partial void OnUrlInputChanged(string value){
|
||||
if (UrlInput.Length > 9){
|
||||
if (SearchEnabled){
|
||||
UpdateSearch(value);
|
||||
ButtonText = "Select Searched Series";
|
||||
ButtonEnabled = false;
|
||||
} else if (UrlInput.Length > 9){
|
||||
if (UrlInput.Contains("/watch/concert/") || UrlInput.Contains("/artist/")){
|
||||
MessageBus.Current.SendMessage(new ToastMessage("Concerts / Artists not implemented yet", ToastType.Error, 1));
|
||||
} else if (UrlInput.Contains("/watch/")){
|
||||
//Episode
|
||||
ButtonText = "Add Episode to Queue";
|
||||
ButtonEnabled = true;
|
||||
SearchVisible = false;
|
||||
} else if (UrlInput.Contains("/series/")){
|
||||
//Series
|
||||
ButtonText = "List Episodes";
|
||||
ButtonEnabled = true;
|
||||
SearchVisible = false;
|
||||
} else{
|
||||
ButtonEnabled = false;
|
||||
SearchVisible = true;
|
||||
}
|
||||
} else{
|
||||
ButtonText = "Enter Url";
|
||||
ButtonEnabled = false;
|
||||
SearchVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSearchEnabledChanged(bool value){
|
||||
if (SearchEnabled){
|
||||
ButtonText = "Select Searched Series";
|
||||
ButtonEnabled = false;
|
||||
} else{
|
||||
ButtonText = "Enter Url";
|
||||
ButtonEnabled = false;
|
||||
|
@ -106,6 +172,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
AddAllEpisodes = false;
|
||||
ButtonText = "Enter Url";
|
||||
ButtonEnabled = false;
|
||||
SearchVisible = true;
|
||||
} else if (UrlInput.Length > 9){
|
||||
episodesBySeason.Clear();
|
||||
SeasonList.Clear();
|
||||
|
@ -208,7 +275,43 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
async partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){
|
||||
async partial void OnSelectedSearchItemChanged(CrBrowseSeries value){
|
||||
if (value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
SearchPopupVisible = false;
|
||||
RaisePropertyChanged(nameof(SearchVisible));
|
||||
SearchItems.Clear();
|
||||
SearchVisible = false;
|
||||
ButtonEnabled = false;
|
||||
ShowLoading = true;
|
||||
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
|
||||
ShowLoading = false;
|
||||
if (list != null){
|
||||
currentSeriesList = list;
|
||||
foreach (var episode in currentSeriesList.Value.List){
|
||||
if (episodesBySeason.ContainsKey("S" + episode.Season)){
|
||||
episodesBySeason["S" + episode.Season].Add(new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E,
|
||||
episode.Lang));
|
||||
} else{
|
||||
episodesBySeason.Add("S" + episode.Season, new List<ItemModel>{
|
||||
new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E, episode.Lang)
|
||||
});
|
||||
SeasonList.Add(new ComboBoxItem{ Content = "S" + episode.Season });
|
||||
}
|
||||
}
|
||||
|
||||
CurrentSelectedSeason = SeasonList[0];
|
||||
ButtonEnabled = false;
|
||||
AllButtonEnabled = true;
|
||||
ButtonText = "Select Episodes";
|
||||
} else{
|
||||
ButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){
|
||||
if (value == null){
|
||||
return;
|
||||
}
|
||||
|
@ -218,7 +321,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
if (episodesBySeason.TryGetValue(key, out var season)){
|
||||
foreach (var episode in season){
|
||||
if (episode.ImageBitmap == null){
|
||||
await episode.LoadImage();
|
||||
episode.LoadImage(episode.ImageUrl);
|
||||
Items.Add(episode);
|
||||
if (selectedEpisodes.Contains(episode.AbsolutNum)){
|
||||
SelectedItems.Add(episode);
|
||||
|
@ -234,7 +337,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios){
|
||||
public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
|
||||
public string ImageUrl{ get; set; } = imageUrl;
|
||||
public Bitmap? ImageBitmap{ get; set; }
|
||||
public string Title{ get; set; } = title;
|
||||
|
@ -249,18 +352,10 @@ public class ItemModel(string imageUrl, string description, string time, string
|
|||
|
||||
public List<string> AvailableAudios{ get; set; } = availableAudios;
|
||||
|
||||
public async Task LoadImage(){
|
||||
try{
|
||||
using (var client = new HttpClient()){
|
||||
var response = await client.GetAsync(ImageUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using (var stream = await response.Content.ReadAsStreamAsync()){
|
||||
ImageBitmap = new Bitmap(stream);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex){
|
||||
// Handle exceptions
|
||||
Console.Error.WriteLine("Failed to load image: " + ex.Message);
|
||||
}
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public async void LoadImage(string url){
|
||||
ImageBitmap = await Helpers.LoadImage(url);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
|
@ -14,8 +15,14 @@ namespace CRD.ViewModels;
|
|||
public partial class CalendarPageViewModel : ViewModelBase{
|
||||
public ObservableCollection<CalendarDay> CalendarDays{ get; set; }
|
||||
|
||||
[ObservableProperty] private ComboBoxItem? _currentCalendarLanguage;
|
||||
[ObservableProperty] private bool? _showLoading = false;
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem? _currentCalendarLanguage;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showLoading = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _customCalendar = false;
|
||||
|
||||
public ObservableCollection<ComboBoxItem> CalendarLanguage{ get; } = new(){
|
||||
new ComboBoxItem(){ Content = "en-us" },
|
||||
|
@ -68,6 +75,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
ShowLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
currentWeek = week;
|
||||
CalendarDays.Clear();
|
||||
CalendarDays.AddRange(week.CalendarDays);
|
||||
|
@ -95,6 +103,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public void Refresh(){
|
||||
|
||||
if (CustomCalendar){
|
||||
BuildCustomCalendar();
|
||||
return;
|
||||
}
|
||||
|
||||
string mondayDate;
|
||||
|
||||
if (currentWeek is{ FirstDayOfWeekString: not null }){
|
||||
|
@ -140,4 +154,82 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
CfgManager.WriteSettingsToFile();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnCustomCalendarChanged(bool value){
|
||||
if (CustomCalendar){
|
||||
BuildCustomCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
private async void BuildCustomCalendar(){
|
||||
ShowLoading = true;
|
||||
|
||||
var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang);
|
||||
|
||||
CalendarWeek week = new CalendarWeek();
|
||||
week.CalendarDays = new List<CalendarDay>();
|
||||
|
||||
DateTime today = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < 7; i++){
|
||||
CalendarDay calDay = new CalendarDay();
|
||||
|
||||
calDay.CalendarEpisodes = new List<CalendarEpisode>();
|
||||
calDay.DateTime = today.AddDays(-i);
|
||||
calDay.DayName = calDay.DateTime.Value.DayOfWeek.ToString();
|
||||
|
||||
week.CalendarDays.Add(calDay);
|
||||
}
|
||||
|
||||
week.CalendarDays.Reverse();
|
||||
|
||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||
var newEpisodes = newEpisodesBase.Data;
|
||||
|
||||
foreach (var crBrowseEpisode in newEpisodes){
|
||||
var episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate;
|
||||
|
||||
if (crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)")){
|
||||
continue;
|
||||
}
|
||||
|
||||
var calendarDay = week.CalendarDays.FirstOrDefault(day => day.DateTime != null && day.DateTime.Value.Date == episodeAirDate.Date);
|
||||
|
||||
if (calendarDay != null){
|
||||
CalendarEpisode calEpisode = new CalendarEpisode();
|
||||
|
||||
calEpisode.DateTime = episodeAirDate;
|
||||
calEpisode.HasPassed = DateTime.Now > episodeAirDate;
|
||||
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
||||
calEpisode.SeriesUrl = "https://www.crunchyroll.com/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
|
||||
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/de/watch/{crBrowseEpisode.Id}/";
|
||||
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail.First().First().Source;
|
||||
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
|
||||
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
|
||||
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
||||
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
|
||||
|
||||
calendarDay.CalendarEpisodes?.Add(calEpisode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (var day in week.CalendarDays){
|
||||
if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
|
||||
}
|
||||
|
||||
currentWeek = week;
|
||||
CalendarDays.Clear();
|
||||
CalendarDays.AddRange(week.CalendarDays);
|
||||
RaisePropertyChanged(nameof(CalendarDays));
|
||||
ShowLoading = false;
|
||||
foreach (var calendarDay in CalendarDays){
|
||||
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
|
||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
||||
calendarDayCalendarEpisode.LoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:vm="clr-namespace:CRD.ViewModels"
|
||||
xmlns:structs="clr-namespace:CRD.Utils.Structs"
|
||||
x:DataType="vm:AddDownloadPageViewModel"
|
||||
x:Class="CRD.Views.AddDownloadPageView">
|
||||
|
||||
|
@ -19,10 +20,83 @@
|
|||
</Grid.RowDefinitions>
|
||||
|
||||
|
||||
<!-- Text Input Field -->
|
||||
<TextBox Grid.Row="0" Watermark="Enter series or episode url" Text="{Binding UrlInput}" Margin="10"
|
||||
<StackPanel Grid.Row="0" Orientation="Vertical">
|
||||
|
||||
<TextBox x:Name="SearchBar" Watermark="Enter series or episode url" Text="{Binding UrlInput}" Margin="10"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<Popup IsLightDismissEnabled="False"
|
||||
MaxWidth="{Binding Bounds.Width, ElementName=SearchBar}"
|
||||
MaxHeight="{Binding Bounds.Height, ElementName=Grid}"
|
||||
IsOpen="{Binding SearchPopupVisible}"
|
||||
Placement="Bottom"
|
||||
PlacementTarget="{Binding ElementName=SearchBar}">
|
||||
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
|
||||
<ListBox x:Name="ListBoxDubsSelection"
|
||||
|
||||
ItemsSource="{Binding SearchItems}"
|
||||
SelectedItem="{Binding SelectedSearchItem}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:CrBrowseSeries}">
|
||||
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Row="1" Grid.Column="0">
|
||||
<!-- Define a row with auto height to match the image height -->
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- Define columns if needed -->
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image Margin="10"
|
||||
Source="{Binding ImageBitmap}"
|
||||
Width="120"
|
||||
Height="180" />
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
<Grid Grid.Row="1" Grid.Column="1">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding Title}"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap"
|
||||
Text="{Binding Description}">
|
||||
</TextBlock>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Grid Grid.Row="1" Margin="10 0 10 0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
|
@ -36,10 +110,17 @@
|
|||
Content="{Binding ButtonText}">
|
||||
</Button>
|
||||
|
||||
<CheckBox Grid.Column="1" IsEnabled="{Binding AllButtonEnabled}" IsChecked="{Binding AddAllEpisodes}"
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<CheckBox IsEnabled="{Binding AllButtonEnabled}" IsChecked="{Binding AddAllEpisodes}"
|
||||
Content="All" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsVisible="{Binding SearchVisible}" IsChecked="{Binding SearchEnabled}"
|
||||
Content="Search" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<!-- ComboBox -->
|
||||
<ComboBox Grid.Column="2" MinWidth="200" SelectedItem="{Binding CurrentSelectedSeason}"
|
||||
ItemsSource="{Binding SeasonList}">
|
||||
|
@ -54,14 +135,13 @@
|
|||
Value="50"
|
||||
Maximum="100"
|
||||
MaxWidth="100"
|
||||
IsVisible="{Binding ShowLoading}"
|
||||
>
|
||||
IsVisible="{Binding ShowLoading}">
|
||||
</ProgressBar>
|
||||
</Grid>
|
||||
|
||||
<!-- ListBox with Custom Elements -->
|
||||
<ListBox Grid.Row="2" Margin="10" SelectionMode="Multiple,Toggle" VerticalAlignment="Stretch"
|
||||
SelectedItems="{Binding SelectedItems}" ItemsSource="{Binding Items}">
|
||||
SelectedItems="{Binding SelectedItems}" ItemsSource="{Binding Items}" x:Name="Grid">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:ItemModel}">
|
||||
<StackPanel>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Row="0" Grid.Column="0" Margin="10 10 0 0" HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding !CustomCalendar}"
|
||||
Command="{Binding PrevWeek}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:SymbolIcon Symbol="ChevronLeft" FontSize="18" />
|
||||
|
@ -44,10 +45,14 @@
|
|||
SelectedItem="{Binding CurrentCalendarLanguage}"
|
||||
ItemsSource="{Binding CalendarLanguage}">
|
||||
</ComboBox>
|
||||
<CheckBox IsChecked="{Binding CustomCalendar}"
|
||||
Content="Custom Calendar" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Button Grid.Row="0" Grid.Column="2" Margin="0 0 10 0" HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding !CustomCalendar}"
|
||||
Command="{Binding NextWeek}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:SymbolIcon Symbol="ChevronRight" FontSize="18" />
|
||||
|
|
Loading…
Reference in New Issue