Add - Added sorting to seasons tab

Add -  Added a download subs button to the history
Chg - Changed CDM error message to include a GitHub wiki link
Chg - Changed the style of settings tabs
Chg - Changed the date format in the seasons tab
Fix - Fixed Crunchyroll seasons numbering issue (e.g., Blue Exorcist)
Fix - Fixed window slightly moving on startup
Fix - Fixed a bug in the upcoming episodes display for the next week
This commit is contained in:
Elwador 2025-01-05 02:10:52 +01:00
parent 5c200b3199
commit 51e3102503
25 changed files with 540 additions and 205 deletions

View file

@ -153,7 +153,9 @@ public class CalendarManager{
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
await LoadAnilistUpcoming();
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
await LoadAnilistUpcoming();
}
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
return forDate;
@ -303,15 +305,16 @@ public class CalendarManager{
}
}
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
foreach (var calendarDay in week.CalendarDays){
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
foreach (var calendarDay in week.CalendarDays){
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date)
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){
calendarDay.CalendarEpisodes.Add(calendarEpisode);
foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date)
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){
calendarDay.CalendarEpisodes.Add(calendarEpisode);
}
}
}
}
@ -320,7 +323,8 @@ public class CalendarManager{
foreach (var weekCalendarDay in week.CalendarDays){
if (weekCalendarDay.CalendarEpisodes.Count > 0)
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
.OrderBy(e => e.DateTime)
.OrderBy(e => e.AnilistEpisode) // False first, then true
.ThenBy(e => e.DateTime)
.ThenBy(e => e.SeasonName)
.ThenBy(e => {
double parsedNumber;
@ -331,9 +335,9 @@ public class CalendarManager{
}
foreach (var day in week.CalendarDays){
if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
}
// foreach (var day in week.CalendarDays){
// if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
// }
calendar["C" + calTargetDate.ToString("yyyy-MM-dd")] = week;
@ -437,38 +441,32 @@ public class CalendarManager{
if (match.Success){
crunchyrollID = match.Groups[1].Value;
calEp.CrSeriesID = crunchyrollID;
AdjustReleaseTimeToHistory(calEp, crunchyrollID);
} else{
Uri uri = new Uri(url);
if (CrunchyrollManager.Instance.CrunOptions.History){
var historySeries = CrunchyrollManager.Instance.HistoryList.FirstOrDefault(item => item.SeriesId == crunchyrollID);
if (uri.Host == "www.crunchyroll.com"
&& uri.AbsolutePath != "/"
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)){
HttpRequestMessage getUrlRequest = new HttpRequestMessage(HttpMethod.Head, url);
if (historySeries != null){
var oldestRelease = DateTime.MinValue;
foreach (var historySeriesSeason in historySeries.Seasons){
if (historySeriesSeason.EpisodesList.Any()){
var releaseDate = historySeriesSeason.EpisodesList.Last().EpisodeCrPremiumAirDate;
string? finalUrl = "";
if (releaseDate.HasValue && oldestRelease < releaseDate.Value){
oldestRelease = releaseDate.Value;
}
}
}
try{
HttpResponseMessage getUrlResponse = await HttpClientReq.Instance.GetHttpClient().SendAsync(getUrlRequest);
if (oldestRelease != DateTime.MinValue){
calEp.DateTime = new DateTime(
calEp.DateTime.Year,
calEp.DateTime.Month,
calEp.DateTime.Day,
oldestRelease.Hour,
oldestRelease.Minute,
oldestRelease.Second,
calEp.DateTime.Kind
);
}
finalUrl = getUrlResponse.RequestMessage?.RequestUri?.ToString();
} catch (Exception ex){
Console.WriteLine($"Error: {ex.Message}");
}
Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern);
if (match2.Success){
crunchyrollID = match2.Groups[1].Value;
AdjustReleaseTimeToHistory(calEp, crunchyrollID);
}
}
} else{
crunchyrollID = "";
}
}
@ -487,6 +485,45 @@ public class CalendarManager{
}
}
private static void AdjustReleaseTimeToHistory(CalendarEpisode calEp, string crunchyrollId){
calEp.CrSeriesID = crunchyrollId;
if (CrunchyrollManager.Instance.CrunOptions.History){
var historySeries = CrunchyrollManager.Instance.HistoryList.FirstOrDefault(item => item.SeriesId == crunchyrollId);
if (historySeries != null){
var oldestRelease = DateTime.MinValue;
foreach (var historySeriesSeason in historySeries.Seasons){
if (historySeriesSeason.EpisodesList.Any()){
var releaseDate = historySeriesSeason.EpisodesList.Last().EpisodeCrPremiumAirDate;
if (releaseDate.HasValue && oldestRelease < releaseDate.Value){
oldestRelease = releaseDate.Value;
}
}
}
if (oldestRelease != DateTime.MinValue){
var adjustedDate = new DateTime(
calEp.DateTime.Year,
calEp.DateTime.Month,
calEp.DateTime.Day,
oldestRelease.Hour,
oldestRelease.Minute,
oldestRelease.Second,
calEp.DateTime.Kind
);
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
adjustedDate = oldestRelease.AddDays(7);
}
calEp.DateTime = adjustedDate;
}
}
}
}
#region Query
private string query = @"query ($weekStart: Int, $weekEnd: Int, $page: Int) {

View file

@ -108,6 +108,8 @@ public class CrunchyrollManager{
options.FfmpegOptions = new();
options.DefaultAudio = "ja-JP";
options.DefaultSub = "en-US";
options.QualityAudio = "best";
options.QualityVideo = "best";
options.CcTag = "CC";
options.CcSubsFont = "Trebuchet MS";
options.FsRetryTime = 5;
@ -150,11 +152,9 @@ public class CrunchyrollManager{
Username = "???",
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE",
PreferredContentSubtitleLanguage = DefaultLocale,
HasPremium = false,
};
Console.WriteLine($"CDM available: {_widevine.canDecrypt}");
}
public async Task Init(){
@ -254,10 +254,10 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh();
var fileNameAndPath = CrunOptions.DownloadToTempFolder
var fileNameAndPath = options.DownloadToTempFolder
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (CrunOptions is{ DlVideoOnce: false, KeepDubsSeperate: true }){
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){
@ -277,7 +277,8 @@ public class CrunchyrollManager{
SyncTiming = options.SyncTiming,
CcTag = options.CcTag,
KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription
MuxDescription = options.IncludeVideoDescription,
DlVideoOnce = options.DlVideoOnce
},
fileNameAndPath);
@ -293,7 +294,7 @@ public class CrunchyrollManager{
foreach (var merger in mergers){
merger.CleanUp();
if (CrunOptions.IsEncodeEnabled){
if (options.IsEncodeEnabled){
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
@ -304,11 +305,11 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data);
await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data);
}
if (CrunOptions.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
if (options.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
}
}
} else{
@ -328,7 +329,8 @@ public class CrunchyrollManager{
SyncTiming = options.SyncTiming,
CcTag = options.CcTag,
KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription
MuxDescription = options.IncludeVideoDescription,
DlVideoOnce = options.DlVideoOnce
},
fileNameAndPath);
@ -338,7 +340,7 @@ public class CrunchyrollManager{
result.merger.CleanUp();
}
if (CrunOptions.IsEncodeEnabled){
if (options.IsEncodeEnabled){
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
@ -349,11 +351,11 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data);
await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data);
}
if (CrunOptions.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
if (options.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
}
}
@ -367,19 +369,19 @@ public class CrunchyrollManager{
Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "")
};
if (CrunOptions.RemoveFinishedDownload && !syncError){
if (options.RemoveFinishedDownload && !syncError){
QueueManager.Instance.Queue.Remove(data);
}
} else{
Console.WriteLine("Skipping mux");
res.Data.ForEach(file => Helpers.DeleteFile(file.Path + ".resume"));
if (CrunOptions.DownloadToTempFolder){
if (options.DownloadToTempFolder){
if (string.IsNullOrEmpty(res.TempFolderPath) || !Directory.Exists(res.TempFolderPath)){
Console.WriteLine("Invalid or non-existent temp folder path.");
} else{
// Move files
foreach (var downloadedMedia in res.Data){
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR);
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
}
}
}
@ -393,7 +395,7 @@ public class CrunchyrollManager{
Doing = "Done - Skipped muxing"
};
if (CrunOptions.RemoveFinishedDownload){
if (options.RemoveFinishedDownload){
QueueManager.Instance.Queue.Remove(data);
}
}
@ -402,7 +404,7 @@ public class CrunchyrollManager{
QueueManager.Instance.ActiveDownloads--;
QueueManager.Instance.Queue.Refresh();
if (CrunOptions.History && data.Data != null && data.Data.Count > 0){
if (options.History && data.Data != null && data.Data.Count > 0){
History.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId);
}
@ -412,8 +414,8 @@ public class CrunchyrollManager{
#region Temp Files Move
private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, string tempFolderPath, IEnumerable<DownloadedMedia> subtitles){
if (!CrunOptions.DownloadToTempFolder) return;
private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, CrDownloadOptions options, string tempFolderPath, IEnumerable<DownloadedMedia> subtitles){
if (!options.DownloadToTempFolder) return;
data.DownloadProgress = new DownloadProgress{
IsDownloading = true,
@ -431,15 +433,15 @@ public class CrunchyrollManager{
}
// Move the main output file
await MoveFile(merger?.options.Output ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR);
await MoveFile(merger?.options.Output ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
// Move the subtitle files
foreach (var downloadedMedia in subtitles){
await MoveFile(downloadedMedia.Path ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR);
await MoveFile(downloadedMedia.Path ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
}
}
private async Task MoveFile(string sourcePath, string tempFolderPath, string downloadPath){
private async Task MoveFile(string sourcePath, string tempFolderPath, string downloadPath, CrDownloadOptions options){
if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath)){
// Console.Error.WriteLine("Source file does not exist or path is invalid.");
return;
@ -454,8 +456,8 @@ public class CrunchyrollManager{
var fileName = sourcePath[tempFolderPath.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var destinationFolder = !string.IsNullOrEmpty(downloadPath)
? downloadPath
: !string.IsNullOrEmpty(CrunOptions.DownloadDirPath)
? CrunOptions.DownloadDirPath
: !string.IsNullOrEmpty(options.DownloadDirPath)
? options.DownloadDirPath
: CfgManager.PathVIDEOS_DIR;
var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName ?? string.Empty);
@ -539,7 +541,7 @@ public class CrunchyrollManager{
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos,
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList),
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList),
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle,
Options = new MuxOptions(){
@ -565,7 +567,7 @@ public class CrunchyrollManager{
bool isMuxed, syncError = false;
if (options.SyncTiming && CrunOptions.DlVideoOnce){
if (options is{ SyncTiming: true, DlVideoOnce: true }){
var basePath = merger.options.OnlyVid.First().Path;
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
@ -666,13 +668,13 @@ public class CrunchyrollManager{
}
if (!_widevine.canDecrypt){
Console.Error.WriteLine("L3 key files missing");
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.");
Console.Error.WriteLine("CDM files missing");
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", true);
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Missing L3 Key"
ErrorText = "Missing CDM files"
};
}
@ -715,8 +717,8 @@ public class CrunchyrollManager{
string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId);
fileDir = CrunOptions.DownloadToTempFolder ? !string.IsNullOrEmpty(CrunOptions.DownloadTempDirPath)
? Path.Combine(CrunOptions.DownloadTempDirPath, Helpers.GetValidFolderName(currentMediaId))
fileDir = options.DownloadToTempFolder ? !string.IsNullOrEmpty(options.DownloadTempDirPath)
? Path.Combine(options.DownloadTempDirPath, Helpers.GetValidFolderName(currentMediaId))
: Path.Combine(CfgManager.PathTEMP_DIR, Helpers.GetValidFolderName(currentMediaId)) :
!string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath :
!string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR;
@ -788,7 +790,7 @@ public class CrunchyrollManager{
#endregion
var fetchPlaybackData = await FetchPlaybackData(mediaId, mediaGuid, data.Music);
var fetchPlaybackData = await FetchPlaybackData(options, mediaId, mediaGuid, data.Music);
if (!fetchPlaybackData.IsOk){
if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){
@ -888,7 +890,7 @@ public class CrunchyrollManager{
Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
}
if (dlVideoOnce && CrunOptions.DlVideoOnce){
if (dlVideoOnce && options.DlVideoOnce){
streams = streams.Where((s) => {
if (s.HardsubLang != "-"){
return false;
@ -1054,6 +1056,11 @@ public class CrunchyrollManager{
videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width));
audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
if (string.IsNullOrEmpty(data.VideoQuality)){
Console.Error.WriteLine("Warning: VideoQuality is null or empty. Defaulting to 'best' quality.");
data.VideoQuality = "best";
}
int chosenVideoQuality;
if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){
chosenVideoQuality = 1;
@ -1062,7 +1069,7 @@ public class CrunchyrollManager{
} else if (data.VideoQuality == "worst"){
chosenVideoQuality = 1;
} else{
var tempIndex = videos.FindIndex(a => a.quality.height + "" == data.VideoQuality.Replace("p", ""));
var tempIndex = videos.FindIndex(a => a.quality.height + "" == data.VideoQuality?.Replace("p", ""));
if (tempIndex < 0){
chosenVideoQuality = videos.Count;
} else{
@ -1473,11 +1480,15 @@ public class CrunchyrollManager{
Console.WriteLine("Downloading skipped!");
}
}
} else if (options.Novids && options.Noaudio){
} else if (options is{ Novids: true, Noaudio: true }){
variables.Add(new Variable("height", 360, false));
variables.Add(new Variable("width", 640, false));
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
}
if (compiledChapters.Count > 0){
if (compiledChapters.Count > 0 && options is not{ Novids: true, Noaudio: true }){
try{
// Parsing and constructing the file names
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
@ -1536,16 +1547,12 @@ public class CrunchyrollManager{
} else{
Console.WriteLine("Subtitles downloading skipped!");
}
}
}
// await Task.Delay(options.Waittime);
}
}
// variables.Add(new Variable("height", quality == 0 ? plQuality.Last().RESOLUTION.Height : plQuality[quality - 1].RESOLUTION.Height, false));
// variables.Add(new Variable("width", quality == 0 ? plQuality.Last().RESOLUTION.Width : plQuality[quality - 1].RESOLUTION.Width, false));
if (options.IncludeVideoDescription){
string fullPath = (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) + ".xml";
@ -1590,7 +1597,7 @@ public class CrunchyrollManager{
}
var tempFolderPath = "";
if (CrunOptions.DownloadToTempFolder){
if (options.DownloadToTempFolder){
tempFolderPath = fileDir;
fileDir = !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath :
!string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR;
@ -1745,7 +1752,7 @@ public class CrunchyrollManager{
}
File.WriteAllText(sxData.Path, subsAssReqResponse.ResponseContent);
Console.WriteLine($"Subtitle downloaded: ${sxData.File}");
Console.WriteLine($"Subtitle downloaded: {sxData.File}");
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Subtitle,
Cc = isCc,
@ -1788,10 +1795,10 @@ public class CrunchyrollManager{
M3U8Json videoJson = new M3U8Json{
Segments = chosenVideoSegments.segments.Cast<dynamic>().ToList()
};
data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s");
data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s.resume" : $"{tsFile}.video.m4s.resume");
var videoDownloader = new HlsDownloader(new HlsOptions{
Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s",
Timeout = options.Timeout,
@ -1844,7 +1851,7 @@ public class CrunchyrollManager{
M3U8Json audioJson = new M3U8Json{
Segments = chosenAudioSegments.segments.Cast<dynamic>().ToList()
};
data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s");
data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s.resume" : $"{tsFile}.audio.m4s.resume");
@ -1866,13 +1873,13 @@ public class CrunchyrollManager{
#region Fetch Playback Data
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, bool music){
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrDownloadOptions options, string mediaId, string mediaGuidId, bool music){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary<string, StreamDetails>()
};
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{CrunOptions.StreamEndpoint}/play";
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){

View file

@ -302,6 +302,7 @@ public class History(){
historyEpisode.EpisodeId = crunchyEpisode.Id;
historyEpisode.Episode = crunchyEpisode.Episode;
historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "";
historyEpisode.EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate;
historyEpisode.HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList);
historyEpisode.HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales);
@ -333,8 +334,7 @@ public class History(){
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
await RefreshSeriesData(seriesId, historySeries);
historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes();
historySeries.Init();

View file

@ -86,7 +86,7 @@ public class QueueManager{
}
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false){
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
await CrunchyrollManager.Instance.CrAuth.RefreshToken(true);
var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale);
@ -100,7 +100,7 @@ public class QueueManager{
var sList = await CrunchyrollManager.Instance.CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory);
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath,string videoQuality) historyEpisode = (null, [], [], "","");
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
if (CrunchyrollManager.Instance.CrunOptions.History){
var episode = sList.EpisodeAndLanguages.Items.First();
@ -142,9 +142,11 @@ public class QueueManager{
}
selected.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;
selected.OnlySubs = onlySubs;
Queue.Add(selected);
if (selected.Data.Count < dubLang.Count){
@ -154,7 +156,8 @@ public class QueueManager{
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray();
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ?? [])}]");
Console.Error.WriteLine(
$"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue but couldn't find all selected dubs", ToastType.Warning, 2));
} else{
Console.WriteLine("Added Episode to Queue");
@ -167,7 +170,7 @@ public class QueueManager{
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray();
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ?? [])}]");
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
}
} else{
@ -180,6 +183,7 @@ public class QueueManager{
if (movieMeta != null){
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
movieMeta.OnlySubs = onlySubs;
Queue.Add(movieMeta);
Console.WriteLine("Added Movie to Queue");
@ -255,7 +259,7 @@ public class QueueManager{
}
var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId);
crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;

View file

@ -2,12 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives">
<!--
NavView style in MainView for main app navigation
While you are free to copy this into your own apps
if you want an MS store like NavView, this will NOT
be an officially supported thing in the main library
-->
<Style Selector="ui|NavigationView.SampleAppNav">
<Setter Property="IsPaneToggleButtonVisible" Value="False" />
<Setter Property="OpenPaneLength" Value="72" />

View file

@ -69,6 +69,8 @@ public class Widevine{
Console.Error.WriteLine("Widevine: " + e);
canDecrypt = false;
}
Console.WriteLine($"CDM available: {canDecrypt}");
}
public async Task<List<ContentKey>> getKeys(string? pssh, string licenseServer, Dictionary<string, string> authData){

View file

@ -36,6 +36,11 @@ public class Helpers{
return default;
}
public static T DeepCopy<T>(T obj){
var json = JsonConvert.SerializeObject(obj);
return JsonConvert.DeserializeObject<T>(json);
}
public static string ConvertTimeFormat(string time){
var timeParts = time.Split(':', '.');
int hours = int.Parse(timeParts[0]);
@ -71,7 +76,7 @@ public class Helpers{
}
public static void EnsureDirectoriesExist(string path){
Console.WriteLine($"Check if path exists: {path}");
// Console.WriteLine($"Check if path exists: {path}");
// Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(path);
@ -507,22 +512,30 @@ public class Helpers{
}
public static string? ExtractNumberAfterS(string input){
// Regular expression pattern to match |S followed by a number and optionally C followed by another number
string pattern = @"\|S(\d+)(?:C(\d+))?";
// Regular expression pattern to match |S followed by a number and optionally C or P followed by another number
string pattern = @"\|S(\d+)(?:C(\d+)|P(\d+))?";
Match match = Regex.Match(input, pattern);
if (match.Success){
string sNumber = match.Groups[1].Value;
string cNumber = match.Groups[2].Value;
string sNumber = match.Groups[1].Value; // Extract the S number
string cNumber = match.Groups[2].Value; // Extract the C number if present
string pNumber = match.Groups[3].Value; // Extract the P number if present
if (!string.IsNullOrEmpty(cNumber)){
// Case for C: Return S + . + C
return $"{sNumber}.{cNumber}";
} else if (!string.IsNullOrEmpty(pNumber)){
// Case for P: Increment S by P - 1
if (int.TryParse(sNumber, out int sNumeric) && int.TryParse(pNumber, out int pNumeric)){
return (sNumeric + (pNumeric - 1)).ToString();
}
} else{
// Return only S if no C or P is present
return sNumber;
}
} else{
return null;
}
return null;
}

View file

@ -459,6 +459,7 @@ public class CrunchyMuxOptions{
public LanguageItem DefaultAudio{ get; set; }
public string CcTag{ get; set; }
public bool SyncTiming{ get; set; }
public bool DlVideoOnce{ get; set; }
}
public class MergerOptions{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
@ -9,7 +10,7 @@ public partial class AnilistSeries : ObservableObject{
public int Id{ get; set; }
public int? IdMal{ get; set; }
public Title Title{ get; set; }
public Date StartDate{ get; set; }
public Date? StartDate{ get; set; }
public Date EndDate{ get; set; }
public string Status{ get; set; }
public string Season{ get; set; }
@ -37,13 +38,25 @@ public partial class AnilistSeries : ObservableObject{
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonIgnore]
public string StartDateForm => $"{StartDate.Day}.{StartDate.Month}.{StartDate.Year}";
public string StartDateForm{
get{
if (StartDate == null)
return string.Empty;
var cultureInfo = System.Globalization.CultureInfo.InvariantCulture;
string monthAbbreviation = cultureInfo.DateTimeFormat.GetAbbreviatedMonthName(StartDate.Month);
return string.Format("{0:00}.{1}.{2}", StartDate.Day, monthAbbreviation, StartDate.Year);
}
}
[JsonIgnore]
public string? CrunchyrollID;
[JsonIgnore]
[ObservableProperty]
public bool _hasCrID;
@ -51,7 +64,6 @@ public partial class AnilistSeries : ObservableObject{
[JsonIgnore]
[ObservableProperty]
public bool _isInHistory;
}
public class Title{
@ -61,9 +73,20 @@ public class Title{
}
public class Date{
public int? Year{ get; set; }
public int? Month{ get; set; }
public int? Day{ get; set; }
public int Year{ get; set; }
public int Month{ get; set; }
public int Day{ get; set; }
public DateTime? ToDateTime(){
if (Year == 0 || Month == 0 || Day == 0)
return DateTime.MinValue;
try{
return new DateTime(Year, Month, Day);
} catch{
return DateTime.MinValue;;
}
}
}
public class CoverImage{

View file

@ -80,6 +80,9 @@ public class CrDownloadOptions{
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; }
[YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)]
public SeasonsPageProperties? SeasonsPageProperties{ get; set; }
[YamlMember(Alias = "download_speed_limit", ApplyNamingConventions = false)]
public int DownloadSpeedLimit{ get; set; }
@ -225,7 +228,10 @@ public class CrDownloadOptions{
[YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
public bool CalendarFilterByAirDate{ get; set; }
[YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)]
public bool CalendarShowUpcomingEpisodes{ get; set; }
[YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
public string? StreamEndpoint{ get; set; }

View file

@ -259,6 +259,8 @@ public class CrunchyEpMeta{
public string Resolution{ get; set; }
public List<string> downloadedFiles{ get; set; } =[];
public bool OnlySubs{ get; set; }
}
public class DownloadProgress{

View file

@ -9,7 +9,6 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_title")]
public string? EpisodeTitle{ get; set; }
@ -24,7 +23,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_cr_season_number")]
public string? EpisodeSeasonNum{ get; set; }
[JsonProperty("episode_cr_premium_air_date")]
public DateTime? EpisodeCrPremiumAirDate{ get; set; }
@ -48,7 +47,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("sonarr_absolut_number")]
public string? SonarrAbsolutNumber{ get; set; }
[JsonProperty("history_episode_available_soft_subs")]
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
@ -77,8 +76,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
CfgManager.UpdateHistoryFile();
}
public async Task DownloadEpisode(){
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId, string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
CrunchyrollManager.Instance.CrunOptions.DubLang);
public async Task DownloadEpisode(bool onlySubs = false){
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
}
}

View file

@ -37,6 +37,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _filterByAirDate;
[ObservableProperty]
private bool _showUpcomingEpisodes;
[ObservableProperty]
private bool _hideDubs;
@ -79,6 +82,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate;
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0];
@ -292,6 +296,15 @@ public partial class CalendarPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
CfgManager.WriteSettingsToFile();
}
partial void OnShowUpcomingEpisodesChanged(bool value){
if (loading){
return;
}
CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes = value;
CfgManager.WriteSettingsToFile();
}
partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){
if (loading){

View file

@ -186,7 +186,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
epMeta.DownloadProgress.IsDownloading = true;
Paused = !epMeta.Paused && !isDownloading || epMeta.Paused;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
await CrunchyrollManager.Instance.DownloadEpisode(epMeta, CrunchyrollManager.Instance.CrunOptions);
CrDownloadOptions newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (epMeta.OnlySubs){
newOptions.Novids = true;
newOptions.Noaudio = true;
}
await CrunchyrollManager.Instance.DownloadEpisode(epMeta,newOptions );
}
}

View file

@ -108,7 +108,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance;
_storageProvider = ProgramManager.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.StorageProvider));
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
@ -177,8 +177,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ScaleValue = ScaleValue;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedView = currentViewType;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.Ascending = SortDir;
} else{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties(){ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType };
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties()
{ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType, Ascending = SortDir };
}
CfgManager.WriteSettingsToFile();
@ -503,6 +505,12 @@ public class HistoryPageProperties(){
public bool Ascending{ get; set; }
}
public class SeasonsPageProperties(){
public SortingType? SelectedSorting{ get; set; }
public bool Ascending{ get; set; }
}
public class SortingListElement(){
public SortingType SelectedSorting{ get; set; }
public string? SortingTitle{ get; set; }

View file

@ -221,7 +221,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
await Task.WhenAll(downloadTasks);
}
[RelayCommand]
public async Task UpdateData(string? season){
await SelectedSeries.FetchData(season);

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll.ViewModels;
using CRD.Downloader.Crunchyroll.Views;

View file

@ -13,6 +13,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
@ -154,6 +155,19 @@ public partial class UpcomingPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private SortingListElement? _selectedSorting;
[ObservableProperty]
private static bool _sortingSelectionOpen;
private SortingType currentSortingType;
[ObservableProperty]
private static bool _sortDir = false;
public ObservableCollection<SortingListElement> SortingList{ get; } =[];
public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[];
public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[];
@ -165,6 +179,23 @@ public partial class UpcomingPageViewModel : ViewModelBase{
}
private async void LoadSeasons(){
SeasonsPageProperties? properties = CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties;
currentSortingType = properties?.SelectedSorting ?? SortingType.SeriesTitle;
SortDir = properties?.Ascending ?? false;
foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){
if (sortingType == SortingType.HistorySeriesAddDate){
continue;
}
var combobox = new SortingListElement(){ SortingTitle = sortingType.GetEnumMemberValue(), SelectedSorting = sortingType };
SortingList.Add(combobox);
if (sortingType == currentSortingType){
SelectedSorting = combobox;
}
}
Seasons = GetTargetSeasonsAndYears();
currentSelection = Seasons.Last();
@ -175,6 +206,8 @@ public partial class UpcomingPageViewModel : ViewModelBase{
foreach (var anilistSeries in list){
SelectedSeason.Add(anilistSeries);
}
SortItems();
}
[RelayCommand]
@ -188,6 +221,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
foreach (var anilistSeries in list){
SelectedSeason.Add(anilistSeries);
}
SortItems();
}
[RelayCommand]
@ -304,7 +338,6 @@ public partial class UpcomingPageViewModel : ViewModelBase{
HttpResponseMessage getUrlResponse = await HttpClientReq.Instance.GetHttpClient().SendAsync(getUrlRequest);
finalUrl = getUrlResponse.RequestMessage?.RequestUri?.ToString();
} catch (Exception ex){
Console.WriteLine($"Error: {ex.Message}");
}
@ -391,4 +424,73 @@ public partial class UpcomingPageViewModel : ViewModelBase{
partial void OnSelectedSeriesChanged(AnilistSeries? value){
SelectionChangedOfSeries(value);
}
#region Sorting
private void UpdateSettings(){
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null){
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending = SortDir;
} else{
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties = new SeasonsPageProperties(){ SelectedSorting = currentSortingType, Ascending = SortDir };
}
CfgManager.WriteSettingsToFile();
}
partial void OnSelectedSortingChanged(SortingListElement? oldValue, SortingListElement? newValue){
if (newValue == null){
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null){
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending = !CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending;
SortDir = CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending;
}
Dispatcher.UIThread.InvokeAsync(() => {
SelectedSorting = oldValue ?? SortingList.First();
RaisePropertyChanged(nameof(SelectedSorting));
});
return;
}
if (newValue.SelectedSorting != null){
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null) CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType;
SortItems();
}
SortingSelectionOpen = false;
UpdateSettings();
}
private void SortItems(){
var sortingDir = CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null && CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending;
var sortedList = currentSortingType switch{
SortingType.SeriesTitle => sortingDir
? SelectedSeason
.OrderByDescending(item => item.Title.English)
.ToList()
: SelectedSeason
.OrderBy(item => item.Title.English)
.ToList(),
SortingType.NextAirDate => sortingDir
? SelectedSeason
.OrderByDescending(item => item.StartDate?.ToDateTime() ?? DateTime.MinValue)
.ThenByDescending(item => item.Title.English)
.ToList()
: SelectedSeason
.OrderBy(item => item.StartDate?.ToDateTime() ?? DateTime.MinValue)
.ThenBy(item => item.Title.English)
.ToList(),
_ => SelectedSeason.ToList()
};
SelectedSeason.Clear();
foreach (var item in sortedList){
SelectedSeason.Add(item);
}
}
#endregion
}

View file

@ -87,10 +87,11 @@
</controls:SettingsExpander>
<controls:SettingsExpander IsVisible="{Binding CustomCalendar}" Header="Custom Calendar Dub Filter">
<controls:SettingsExpander IsVisible="{Binding CustomCalendar}" Header="Custom Calendar">
<controls:SettingsExpander.Footer>
<StackPanel Orientation="Vertical">
<TextBlock Text="Dub Filter"></TextBlock>
<ComboBox HorizontalAlignment="Center" Margin="5 0 0 5" MinWidth="200"
SelectedItem="{Binding CurrentCalendarDubFilter}"
ItemsSource="{Binding CalendarDubFilter}">
@ -98,6 +99,9 @@
<CheckBox IsChecked="{Binding FilterByAirDate}"
Content="Filter by episode air date" Margin="5 5 0 0">
</CheckBox>
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
Content="Show Upcoming episodes" Margin="5 5 0 0">
</CheckBox>
</StackPanel>
</controls:SettingsExpander.Footer>

View file

@ -104,14 +104,22 @@ public partial class MainWindow : AppWindow{
.Subscribe(message => ShowToast(message.Message, message.Type, message.Seconds));
}
public async void ShowError(string message){
public async void ShowError(string message,bool githubWikiButton = false){
var dialog = new ContentDialog(){
Title = "Error",
Content = message,
CloseButtonText = "Close"
};
_ = await dialog.ShowAsync();
if (githubWikiButton){
dialog.PrimaryButtonText = "Github Wiki";
}
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary){
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
}
}
@ -191,7 +199,7 @@ public partial class MainWindow : AppWindow{
var screen = screens[settings.ScreenIndex];
// Restore the position first
Position = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment);
Position = new PixelPoint(settings.PosX, settings.PosY);
// Restore the size
Width = settings.Width;
@ -199,10 +207,10 @@ public partial class MainWindow : AppWindow{
// Set restore size and position for non-maximized state
_restoreSize = new Size(settings.Width, settings.Height);
_restorePosition = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment);
_restorePosition = new PixelPoint(settings.PosX, settings.PosY);
// Ensure the window is on the correct screen before maximizing
Position = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment);
Position = new PixelPoint(settings.PosX, settings.PosY );
}
if (settings.IsMaximized){

View file

@ -138,51 +138,51 @@
<controls:SettingsExpander.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<!-- <ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" -->
<!-- ItemsSource="{Binding CrunchyrollManager.VideoQualityList}" -->
<!-- SelectedItem="{Binding SelectedSeries.SelectedVideoQuality}"> -->
<!-- </ComboBox> -->
<StackPanel>
<ToggleButton x:Name="OverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem.stringValue, FallbackValue=''}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=OverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SelectedSeries.VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
<ToggleButton x:Name="OverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem.stringValue, FallbackValue=''}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=OverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SelectedSeries.VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
@ -342,6 +342,7 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
@ -385,7 +386,25 @@
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" FontSize="18" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Download Episode" FontSize="15" />
</ToolTip.Tip>
</Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DownloadEpisode}"
CommandParameter="true">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Download Subs" FontSize="15" />
</ToolTip.Tip>
</Button>
</StackPanel>
</Grid>
@ -488,10 +507,10 @@
<controls:SettingsExpander.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
@ -525,10 +544,10 @@
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>

View file

@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace CRD.Views;
@ -6,4 +7,6 @@ public partial class SeriesPageView : UserControl{
public SeriesPageView(){
InitializeComponent();
}
}

View file

@ -12,11 +12,26 @@
<Design.DataContext>
<vm:SettingsPageViewModel />
</Design.DataContext>
<controls:TabView TabItems="{Binding Tabs}"
AllowDropTabs="False" IsAddTabButtonVisible="False"
Background="Transparent" CanDragTabs="False" CanReorderTabs="False"
CanDragTabs="False" CanReorderTabs="False"
VerticalAlignment="Stretch">
<controls:TabView.Styles>
<Style Selector="controls|TabViewItem:selected /template/ Border#TabContainerBorder">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="controls|TabViewItem:selected /template/ Path#SelectedBackgroundPath">
<Setter Property="Fill" Value="#10FFFFFF" />
</Style>
<Style Selector="controls|TabView:not(:selected) /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
</controls:TabView.Styles>
</controls:TabView>
</UserControl>

View file

@ -16,8 +16,13 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="10">
<ItemsControl ItemsSource="{Binding Seasons}">
<Grid Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="10">
<!-- Define columns: one for centering and one for the right-aligned content -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- For centering the ItemsControl -->
<ColumnDefinition Width="Auto" /> <!-- For the settings StackPanel -->
</Grid.ColumnDefinitions>
<ItemsControl ItemsSource="{Binding Seasons}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
@ -45,7 +50,65 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<StackPanel Grid.Column="1" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Center">
<StackPanel>
<ToggleButton x:Name="DropdownButtonSorting" Width="50" Height="50"
BorderThickness="0" VerticalAlignment="Center"
IsEnabled="{Binding !IsLoading}"
IsChecked="{Binding SortingSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sort" FontSize="25" />
<TextBlock Text="Sort" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonSorting, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonSorting}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SortingList}" SelectedItem="{Binding SelectedSorting}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<controls:SymbolIcon IsVisible="{Binding !$parent[UserControl].((vm:UpcomingPageViewModel)DataContext).SortDir}" Symbol="ChevronUp" FontSize="12" Margin="0 0 10 0" />
<controls:SymbolIcon IsVisible="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).SortDir}" Symbol="ChevronDown" FontSize="12" Margin="0 0 10 0" />
<TextBlock Text="{Binding SortingTitle}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
<!-- <ToggleButton x:Name="CalendarSettings" HorizontalContentAlignment="Stretch"> -->
<!-- <StackPanel Orientation="Horizontal"> -->
<!-- <controls:SymbolIcon Symbol="Settings" FontSize="18" /> -->
<!-- </StackPanel> -->
<!-- </ToggleButton> -->
<!-- <Popup IsLightDismissEnabled="True" -->
<!-- MaxWidth="400" -->
<!-- MaxHeight="600" -->
<!-- IsOpen="{Binding IsChecked, ElementName=CalendarSettings, Mode=TwoWay}" -->
<!-- Placement="Bottom" -->
<!-- PlacementTarget="{Binding ElementName=CalendarSettings}"> -->
<!-- <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> -->
<!-- -->
<!-- <StackPanel> -->
<!-- -->
<!-- </StackPanel> -->
<!-- -->
<!-- </Border> -->
<!-- </Popup> -->
</StackPanel>
</Grid>
<ListBox Grid.Row="1" IsVisible="{Binding !IsLoading}" ItemsSource="{Binding SelectedSeason}"
@ -88,15 +151,15 @@
</Image>
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right"
IsVisible="{Binding IsInHistory}" Margin="0 5 5 5" >
IsVisible="{Binding IsInHistory}" Margin="0 5 5 5">
<Border Background="DarkGray" CornerRadius="50">
<controls:SymbolIcon Symbol="Library" Foreground="Black" FontSize="22" Margin="2" />
<controls:SymbolIcon Symbol="Library" Foreground="Black" FontSize="22" Margin="2" />
<ToolTip.Tip>
<TextBlock Text="Series is in History" FontSize="15" />
</ToolTip.Tip>
</Border>
</StackPanel>
</Grid>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
@ -125,14 +188,14 @@
</StackPanel>
<Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right" >
<Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right">
<Expander.Styles>
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="Expander:expanded /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="ToggleButton:pointerover /template/ Border#ExpandCollapseChevronBorder">
@ -146,12 +209,12 @@
</Style>
</Expander.Styles>
<Expander.Header>
<Border Width="117" Height="315" />
<Border Width="117" Height="315" />
</Expander.Header>
<Expander.Content>
<StackPanel>
<ScrollViewer MaxHeight="265" MinHeight="265" PointerWheelChanged="ScrollViewer_PointerWheelChanged" Margin="0 0 0 5">
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding Description}"
@ -162,22 +225,23 @@
</TextBlock>
</ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Trailer" Margin=" 0 0 5 0"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}"
CommandParameter="{Binding}"></Button>
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Trailer" Margin=" 0 0 5 0"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}"
CommandParameter="{Binding}">
</Button>
<StackPanel IsVisible="{Binding HasCrID}">
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom"
IsVisible="{Binding !IsInHistory}"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).AddToHistory}"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).AddToHistory}"
CommandParameter="{Binding}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Library" FontSize="20" />
<controls:SymbolIcon Symbol="Add" FontSize="20" />
<controls:SymbolIcon Symbol="Add" FontSize="20" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</StackPanel>
</Expander.Content>
@ -190,6 +254,6 @@
</ListBox>
<controls:ProgressRing Grid.Row="1" IsVisible="{Binding IsLoading}" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="100"></controls:ProgressRing>
</Grid>
</UserControl>

View file

@ -14,7 +14,7 @@
</Design.DataContext>
<ScrollViewer Padding="20 20 20 0">
<ScrollViewer Padding="20 20 20 0" >
<StackPanel Spacing="8">
<controls:SettingsExpander Header="History"