diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs
index e3c2cba..093d342 100644
--- a/CRD/Downloader/CalendarManager.cs
+++ b/CRD/Downloader/CalendarManager.cs
@@ -11,8 +11,10 @@ using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
+using CRD.Views;
using HtmlAgilityPack;
using Newtonsoft.Json;
+using ReactiveUI;
namespace CRD.Downloader;
@@ -75,6 +77,20 @@ public class CalendarManager{
var response = await HttpClientReq.Instance.SendHttpRequest(request);
+ if (!response.IsOk){
+ if (response.ResponseContent.Contains("
Just a moment...") ||
+ response.ResponseContent.Contains("Access denied") ||
+ response.ResponseContent.Contains("Attention Required! | Cloudflare") ||
+ response.ResponseContent.Trim().Equals("error code: 1020") ||
+ response.ResponseContent.IndexOf("DDOS-GUARD", StringComparison.OrdinalIgnoreCase) > -1){
+ MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5));
+ Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
+ } else{
+ Console.Error.WriteLine($"Calendar request failed");
+ }
+ return new CalendarWeek();
+ }
+
CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List();
@@ -314,8 +330,8 @@ public class CalendarManager{
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))){
+ foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date.Day == calendarEpisode.DateTime.Date.Day)
+ .Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID && ele.SeasonName != calendarEpisode.SeasonName))){
calendarDay.CalendarEpisodes.Add(calendarEpisode);
}
}
diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs
index f298019..3144176 100644
--- a/CRD/Downloader/Crunchyroll/CrEpisode.cs
+++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs
@@ -42,6 +42,24 @@ public class CrEpisode(){
return null;
}
+ if (epsidoe is{ Total: 1, Data: not null } &&
+ (epsidoe.Data.First().Versions ?? [])
+ .GroupBy(v => v.AudioLocale)
+ .Any(g => g.Count() > 1)){
+ Console.Error.WriteLine("Episode has Duplicate Audio Locales");
+ var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList();
+ //guid for episode id
+ foreach (var episodeVersionse in list){
+ foreach (var version in episodeVersionse){
+ var checkRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{version.Guid}", HttpMethod.Get, true, true, query);
+ var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest,true);
+ if (!checkResponse.IsOk){
+ epsidoe.Data.First().Versions?.Remove(version);
+ }
+ }
+ }
+ }
+
if (epsidoe.Total == 1 && epsidoe.Data != null){
return epsidoe.Data.First();
}
diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs
index 64a9ac0..ede28cf 100644
--- a/CRD/Downloader/Crunchyroll/CrMusic.cs
+++ b/CRD/Downloader/Crunchyroll/CrMusic.cs
@@ -17,7 +17,7 @@ public class CrMusic{
public async Task ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/featured/{seriesId}", crLocale, forcedLang);
- if (musicVideos.Data is{ Count: > 0 } && updateHistory){
+ if (musicVideos.Data is{ Count: > 0 } && updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
@@ -27,7 +27,7 @@ public class CrMusic{
public async Task ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideo = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos");
- if (musicVideo != null && updateHistory){
+ if (musicVideo != null && updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList([musicVideo]);
}
@@ -39,7 +39,7 @@ public class CrMusic{
if (concert != null){
concert.EpisodeType = EpisodeType.Concert;
- if (updateHistory){
+ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList([concert]);
}
}
@@ -50,7 +50,7 @@ public class CrMusic{
public async Task ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
- if (updateHistory){
+ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
@@ -66,7 +66,7 @@ public class CrMusic{
}
}
- if (updateHistory){
+ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(concerts.Data);
}
@@ -97,7 +97,7 @@ public class CrMusic{
musicVideos.Data.AddRange(concerts.Data);
}
- if (updateHistory){
+ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs
index 704ead7..7fb5767 100644
--- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs
+++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs
@@ -337,6 +337,7 @@ public class CrunchyrollManager{
SkipSubMux = options.SkipSubsMux,
Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}",
Mp4 = options.Mp4,
+ Mp3 = options.AudioOnlyToMp3,
MuxFonts = options.MuxFonts,
VideoTitle = res.VideoTitle,
Novids = options.Novids,
@@ -402,6 +403,7 @@ public class CrunchyrollManager{
SkipSubMux = options.SkipSubsMux,
Output = fileNameAndPath,
Mp4 = options.Mp4,
+ Mp3 = options.AudioOnlyToMp3,
MuxFonts = options.MuxFonts,
VideoTitle = res.VideoTitle,
Novids = options.Novids,
@@ -599,8 +601,10 @@ public class CrunchyrollManager{
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
if (data.FindAll(a => a.Type == DownloadMediaType.Audio).Count > 0){
- Console.WriteLine("Mux to MP3");
- muxToMp3 = true;
+ if (options.Mp3){
+ Console.WriteLine("Mux to MP3");
+ muxToMp3 = true;
+ }
} else{
Console.WriteLine("Skip muxing since no videos are downloaded");
return (null, false, false, "");
@@ -643,9 +647,9 @@ public class CrunchyrollManager{
var merger = new Merger(new MergerOptions{
DubLangList = options.DubLangList,
SubLangList = options.SubLangList,
- OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
+ OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty ,Bitrate = a.bitrate}).ToList(),
SkipSubMux = options.SkipSubMux,
- OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
+ OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty ,Bitrate = a.bitrate}).ToList(),
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
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(),
@@ -682,7 +686,7 @@ public class CrunchyrollManager{
List notSyncedDubs =[];
- if (options is{ SyncTiming: true, DlVideoOnce: true }){
+ if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){
crunchyEpMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
@@ -916,7 +920,7 @@ public class CrunchyrollManager{
if (mediaGuid.Contains(':')){
mediaGuid = mediaGuid.Split(':')[1];
}
-
+
Console.WriteLine("MediaGuid: " + mediaId);
#region Chapters
@@ -1154,7 +1158,7 @@ public class CrunchyrollManager{
Console.WriteLine("Downloading video...");
curStream = streams[options.Kstream - 1];
- Console.WriteLine($"Playlists URL: {curStream.Url} ({curStream.Type})");
+ Console.WriteLine($"Playlists URL: {string.Join(", ",curStream.Url)} ({curStream.Type})");
}
string tsFile = "";
@@ -1376,7 +1380,7 @@ public class CrunchyrollManager{
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
- fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
string onlyFileName = Path.GetFileName(fileName);
int maxLength = 220;
@@ -1391,7 +1395,7 @@ public class CrunchyrollManager{
if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){
titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength);
- fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
onlyFileName = Path.GetFileName(fileName);
if (onlyFileName.Length > maxLength){
@@ -1410,7 +1414,7 @@ public class CrunchyrollManager{
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale);
string tempFile = Path.Combine(FileNameManager
- .ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
+ .ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override)
.ToArray());
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile);
@@ -1608,7 +1612,8 @@ public class CrunchyrollManager{
Path = $"{tsFile}.video.m4s",
Lang = lang,
Language = lang,
- IsPrimary = isPrimary
+ IsPrimary = isPrimary,
+ bitrate = chosenVideoSegments.bandwidth / 1024
};
files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s");
@@ -1676,7 +1681,8 @@ public class CrunchyrollManager{
Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s",
Lang = lang,
- IsPrimary = isPrimary
+ IsPrimary = isPrimary,
+ bitrate = chosenAudioSegments.bandwidth / 1000
});
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
} else{
@@ -1693,7 +1699,8 @@ public class CrunchyrollManager{
Path = $"{tsFile}.video.m4s",
Lang = lang,
Language = lang,
- IsPrimary = isPrimary
+ IsPrimary = isPrimary,
+ bitrate = chosenVideoSegments.bandwidth / 1024
};
files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s");
@@ -1704,13 +1711,14 @@ public class CrunchyrollManager{
Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s",
Lang = lang,
- IsPrimary = isPrimary
+ IsPrimary = isPrimary,
+ bitrate = chosenAudioSegments.bandwidth / 1000
});
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
}
}
} else if (options.Novids){
- fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
Console.WriteLine("Downloading skipped!");
}
}
@@ -1718,14 +1726,14 @@ public class CrunchyrollManager{
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());
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
}
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());
- var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray());
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
+ var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
if (Path.IsPathRooted(outFile)){
tsFile = outFile;
} else{
@@ -1847,7 +1855,7 @@ public class CrunchyrollManager{
Error = dlFailed,
FileName = fileName.Length > 0 ? fileName : "unknown - " + Guid.NewGuid(),
ErrorText = "",
- VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers, options.Override).Last(),
+ VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).Last(),
FolderPath = fileDir,
TempFolderPath = tempFolderPath
};
diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs
index 0adcdfc..84242cd 100644
--- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs
+++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs
@@ -60,6 +60,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _muxToMp4;
+
+ [ObservableProperty]
+ private bool _muxToMp3;
[ObservableProperty]
private bool _muxFonts;
@@ -93,6 +96,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private string _fileName = "";
+
+ [ObservableProperty]
+ private string _fileNameWhitespaceSubstitute = "";
[ObservableProperty]
private string _fileTitle = "";
@@ -347,11 +353,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
KeepDubsSeparate = options.KeepDubsSeperate;
DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4;
+ MuxToMp3 = options.AudioOnlyToMp3;
MuxFonts = options.MuxFonts;
SyncTimings = options.SyncTiming;
SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers;
FileName = options.FileName;
+ FileNameWhitespaceSubstitute = options.FileNameWhitespaceSubstitute;
SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic;
ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null;
@@ -413,11 +421,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing;
CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4;
+ CrunchyrollManager.Instance.CrunOptions.AudioOnlyToMp3 = MuxToMp3;
CrunchyrollManager.Instance.CrunOptions.MuxFonts = MuxFonts;
CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings;
CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux;
CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10);
CrunchyrollManager.Instance.CrunOptions.FileName = FileName;
+ CrunchyrollManager.Instance.CrunOptions.FileNameWhitespaceSubstitute = FileNameWhitespaceSubstitute;
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs;
CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000);
diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml
index 3f5cce5..8b54821 100644
--- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml
+++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml
@@ -319,6 +319,14 @@
HorizontalAlignment="Stretch" />
+
+
+
+
+
+
@@ -345,11 +353,17 @@
-
+
+
+
+
+
+
+
diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs
index 042a5e1..f840b67 100644
--- a/CRD/Downloader/History.cs
+++ b/CRD/Downloader/History.cs
@@ -37,8 +37,12 @@ public class History{
}
}
} else{
- foreach (var historyEpisode in historySeries.Seasons.First(historySeason => historySeason.SeasonId == seasonId).EpisodesList){
- historyEpisode.IsEpisodeAvailableOnStreamingService = false;
+ var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId);
+
+ if (matchingSeason != null){
+ foreach (var historyEpisode in matchingSeason.EpisodesList){
+ historyEpisode.IsEpisodeAvailableOnStreamingService = false;
+ }
}
}
}
@@ -165,6 +169,7 @@ public class History{
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true,
+ ThumbnailImageUrl = historySource.GetImageUrl(),
};
historySeason.EpisodesList.Add(newHistoryEpisode);
@@ -179,6 +184,7 @@ public class History{
historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate();
historyEpisode.EpisodeType = historySource.GetEpisodeType();
historyEpisode.IsEpisodeAvailableOnStreamingService = true;
+ historyEpisode.ThumbnailImageUrl = historySource.GetImageUrl();
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs();
@@ -577,7 +583,8 @@ public class History{
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType(),
- IsEpisodeAvailableOnStreamingService = true
+ IsEpisodeAvailableOnStreamingService = true,
+ ThumbnailImageUrl = historySource.GetImageUrl(),
};
newSeason.EpisodesList.Add(newHistoryEpisode);
@@ -634,7 +641,7 @@ public class History{
var historyEpisodesWithSonarrIds = allHistoryEpisodes
.Where(e => !string.IsNullOrEmpty(e.SonarrEpisodeId))
.ToList();
-
+
Parallel.ForEach(historyEpisodesWithSonarrIds, historyEpisode => {
var sonarrEpisode = episodes.FirstOrDefault(e => e.Id.ToString().Equals(historyEpisode.SonarrEpisodeId));
@@ -644,9 +651,9 @@ public class History{
});
var historyEpisodeIds = new HashSet(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!));
-
+
episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString()));
-
+
allHistoryEpisodes = allHistoryEpisodes
.Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId))
.ToList();
diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs
index a95b8dd..3031dcc 100644
--- a/CRD/Downloader/QueueManager.cs
+++ b/CRD/Downloader/QueueManager.cs
@@ -95,7 +95,7 @@ public partial class QueueManager : ObservableObject{
}
- public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false, bool onlySubs = false){
+ public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false, EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){
if (string.IsNullOrEmpty(epId)){
return;
}
@@ -158,7 +158,7 @@ public partial class QueueManager : ObservableObject{
selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;
- selected.OnlySubs = onlySubs;
+ selected.OnlySubs = episodeDownloadMode == EpisodeDownloadMode.OnlySubs;
if (CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub && selected.Data.Count > 1){
var sortedMetaData = selected.Data
@@ -178,9 +178,24 @@ public partial class QueueManager : ObservableObject{
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
- if (selected.OnlySubs){
- newOptions.Novids = true;
- newOptions.Noaudio = true;
+ switch (episodeDownloadMode){
+ case EpisodeDownloadMode.OnlyVideo:
+ newOptions.Novids = false;
+ newOptions.Noaudio = true;
+ selected.DownloadSubs = ["none"];
+ break;
+ case EpisodeDownloadMode.OnlyAudio:
+ newOptions.Novids = true;
+ newOptions.Noaudio = false;
+ selected.DownloadSubs = ["none"];
+ break;
+ case EpisodeDownloadMode.OnlySubs:
+ newOptions.Novids = true;
+ newOptions.Noaudio = true;
+ break;
+ case EpisodeDownloadMode.Default:
+ default:
+ break;
}
newOptions.DubLang = dubLang;
@@ -227,13 +242,28 @@ public partial class QueueManager : ObservableObject{
if (movieMeta != null){
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
- movieMeta.OnlySubs = onlySubs;
+ movieMeta.OnlySubs = episodeDownloadMode == EpisodeDownloadMode.OnlySubs;
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
- if (movieMeta.OnlySubs){
- newOptions.Novids = true;
- newOptions.Noaudio = true;
+ switch (episodeDownloadMode){
+ case EpisodeDownloadMode.OnlyVideo:
+ newOptions.Novids = false;
+ newOptions.Noaudio = true;
+ movieMeta.DownloadSubs = ["none"];
+ break;
+ case EpisodeDownloadMode.OnlyAudio:
+ newOptions.Novids = true;
+ newOptions.Noaudio = false;
+ movieMeta.DownloadSubs = ["none"];
+ break;
+ case EpisodeDownloadMode.OnlySubs:
+ newOptions.Novids = true;
+ newOptions.Noaudio = true;
+ break;
+ case EpisodeDownloadMode.Default:
+ default:
+ break;
}
newOptions.DubLang = dubLang;
diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs
index 5d76d44..d036d1a 100644
--- a/CRD/Utils/Enums/EnumCollection.cs
+++ b/CRD/Utils/Enums/EnumCollection.cs
@@ -251,6 +251,13 @@ public enum CrunchyUrlType{
Unknown
}
+public enum EpisodeDownloadMode{
+ Default,
+ OnlyVideo,
+ OnlyAudio,
+ OnlySubs,
+}
+
public enum SonarrCoverType{
Banner,
FanArt,
diff --git a/CRD/Utils/Files/FileNameManager.cs b/CRD/Utils/Files/FileNameManager.cs
index b4f0eb7..f1b2f81 100644
--- a/CRD/Utils/Files/FileNameManager.cs
+++ b/CRD/Utils/Files/FileNameManager.cs
@@ -9,7 +9,7 @@ using CRD.Utils.Structs;
namespace CRD.Utils.Files;
public class FileNameManager{
- public static List ParseFileName(string input, List variables, int numbers, List @override){
+ public static List ParseFileName(string input, List variables, int numbers,string whiteSpaceReplace, List @override){
Regex varRegex = new Regex(@"\${[A-Za-z1-9]+}");
var matches = varRegex.Matches(input).Cast().Select(m => m.Value).ToList();
var overriddenVars = ParseOverride(variables, @override);
@@ -27,7 +27,7 @@ public class FileNameManager{
continue;
}
- string replacement = variable.ReplaceWith.ToString();
+ string replacement = variable.ReplaceWith.ToString() ?? string.Empty;
if (variable.Type == "int32"){
int len = replacement.Length;
replacement = len < numbers ? new string('0', numbers - len) + replacement : replacement;
@@ -38,6 +38,9 @@ public class FileNameManager{
replacement = replacement.Replace(",", ".");
} else if (variable.Sanitize){
replacement = CleanupFilename(replacement);
+ if (variable.Type == "string" && !string.IsNullOrEmpty(whiteSpaceReplace)){
+ replacement = replacement.Replace(" ",whiteSpaceReplace);
+ }
}
input = input.Replace(match, replacement);
diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs
index 49333e7..76d0354 100644
--- a/CRD/Utils/Muxing/Merger.cs
+++ b/CRD/Utils/Muxing/Merger.cs
@@ -131,15 +131,14 @@ public class Merger{
}
- foreach (var aud in options.OnlyAudio){
- args.Add($"-i \"{aud.Path}\"");
- metaData.Add($"-map {index}");
- metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}");
- index++;
- audioIndex++;
+ if (options.OnlyAudio.Count > 1){
+ Console.Error.WriteLine("Multiple audio files detected. Only one audio file can be converted to MP3 at a time.");
}
- args.Add("-c:a copy");
+ var audio = options.OnlyAudio.First();
+
+ args.Add($"-i \"{audio.Path}\"");
+ args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") );
args.Add($"\"{options.Output}\"");
return string.Join(" ", args);
}
@@ -443,6 +442,7 @@ public class MergerInput{
public int? Duration{ get; set; }
public int? Delay{ get; set; }
public bool? IsPrimary{ get; set; }
+ public int? Bitrate{ get; set; }
}
public class SubtitleInput{
@@ -469,6 +469,7 @@ public class CrunchyMuxOptions{
public bool? KeepAllVideos{ get; set; }
public bool? Novids{ get; set; }
public bool Mp4{ get; set; }
+ public bool Mp3{ get; set; }
public bool MuxFonts{ get; set; }
public bool MuxDescription{ get; set; }
public string ForceMuxer{ get; set; }
diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs
index e0d340d..93b134d 100644
--- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs
+++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs
@@ -155,6 +155,9 @@ public class CrDownloadOptions{
[JsonProperty("quality_audio")]
public string QualityAudio{ get; set; } = "";
+ [JsonProperty("file_name_whitespace_substitute")]
+ public string FileNameWhitespaceSubstitute{ get; set; } = "";
+
[JsonProperty("file_name")]
public string FileName{ get; set; } = "";
@@ -194,6 +197,9 @@ public class CrDownloadOptions{
[JsonProperty("mux_mp4")]
public bool Mp4{ get; set; }
+ [JsonProperty("mux_audio_only_to_mp3")]
+ public bool AudioOnlyToMp3 { get; set; }
+
[JsonProperty("mux_fonts")]
public bool MuxFonts{ get; set; }
diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs
index 5f0bff7..9127745 100644
--- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs
+++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs
@@ -289,6 +289,14 @@ public class CrunchyEpisode : IHistorySource{
public EpisodeType GetEpisodeType(){
return EpisodeType;
}
+
+ public string GetImageUrl(){
+ if (Images != null){
+ return Images.Thumbnail?.First().First().Source ?? string.Empty;
+ }
+
+ return string.Empty;
+ }
#endregion
}
diff --git a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs
index 312ab10..c0e440f 100644
--- a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs
+++ b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using CRD.Utils.Structs.History;
using Newtonsoft.Json;
@@ -170,6 +171,14 @@ public class CrunchyMusicVideo : IHistorySource{
public EpisodeType GetEpisodeType(){
return EpisodeType;
}
+
+ public string GetImageUrl(){
+ if (Images != null){
+ return Images.Thumbnail.First().Source ?? string.Empty;
+ }
+
+ return string.Empty;
+ }
#endregion
}
diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs
index b5114c1..b4f4c64 100644
--- a/CRD/Utils/Structs/HelperClasses.cs
+++ b/CRD/Utils/Structs/HelperClasses.cs
@@ -84,7 +84,8 @@ public class DownloadedMedia : SxItem{
public DownloadMediaType Type{ get; set; }
public required LanguageItem Lang{ get; set; }
public bool IsPrimary{ get; set; }
-
+
+ public int bitrate{ get; set; }
public bool? Cc{ get; set; }
public bool? Signs{ get; set; }
diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs
index c41e7ca..e955d0e 100644
--- a/CRD/Utils/Structs/History/HistoryEpisode.cs
+++ b/CRD/Utils/Structs/History/HistoryEpisode.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
+using Avalonia.Media.Imaging;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files;
@@ -34,19 +35,22 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; }
-
+
[JsonProperty("episode_available_on_streaming_service")]
public bool IsEpisodeAvailableOnStreamingService{ get; set; }
-
+
[JsonProperty("episode_type")]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
+ [JsonProperty("episode_thumbnail_url")]
+ public string? ThumbnailImageUrl{ get; set; }
+
[JsonProperty("sonarr_episode_id")]
public string? SonarrEpisodeId{ get; set; }
[JsonProperty("sonarr_has_file")]
public bool SonarrHasFile{ get; set; }
-
+
[JsonProperty("sonarr_is_monitored")]
public bool SonarrIsMonitored{ get; set; }
@@ -70,12 +74,32 @@ public class HistoryEpisode : INotifyPropertyChanged{
return $"S{SonarrSeasonNumber}E{SonarrEpisodeNumber}";
}
}
-
+
[JsonProperty("history_episode_available_soft_subs")]
public List HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_episode_available_dub_lang")]
public List HistoryEpisodeAvailableDubLang{ get; set; } =[];
+
+ [JsonIgnore]
+ public Bitmap? ThumbnailImage{ get; set; }
+
+ [JsonIgnore]
+ public bool IsImageLoaded{ get; private set; } = false;
+
+ public async Task LoadImage(){
+ if (IsImageLoaded || string.IsNullOrEmpty(ThumbnailImageUrl))
+ return;
+
+ try{
+ ThumbnailImage = await Helpers.LoadImage(ThumbnailImageUrl);
+ IsImageLoaded = true;
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
+ } catch (Exception ex){
+ Console.Error.WriteLine("Failed to load image: " + ex.Message);
+ }
+ }
[JsonIgnore]
public string ReleaseDateFormated{
@@ -92,7 +116,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
return string.Format("{0:00}.{1}.{2}", EpisodeCrPremiumAirDate.Value.Day, monthAbbreviation, EpisodeCrPremiumAirDate.Value.Year);
}
}
-
+
public event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){
@@ -115,7 +139,11 @@ public class HistoryEpisode : INotifyPropertyChanged{
CfgManager.UpdateHistoryFile();
}
- public async Task DownloadEpisode(bool onlySubs = false){
+ public async Task DownloadEpisodeDefault(){
+ await DownloadEpisode();
+ }
+
+ public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){
switch (EpisodeType){
case EpisodeType.MusicVideo:
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty);
@@ -128,19 +156,19 @@ public class HistoryEpisode : INotifyPropertyChanged{
default:
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId ?? string.Empty,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
- CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
+ CrunchyrollManager.Instance.CrunOptions.DubLang, false, episodeDownloadMode);
break;
}
}
-
- public void AssignSonarrEpisodeData(SonarrEpisode episode) {
+
+ public void AssignSonarrEpisodeData(SonarrEpisode episode){
SonarrEpisodeId = episode.Id.ToString();
SonarrEpisodeNumber = episode.EpisodeNumber.ToString();
SonarrHasFile = episode.HasFile;
SonarrIsMonitored = episode.Monitored;
SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber.ToString();
SonarrSeasonNumber = episode.SeasonNumber.ToString();
-
+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText)));
}
}
\ No newline at end of file
diff --git a/CRD/Utils/Structs/History/IHistorySource.cs b/CRD/Utils/Structs/History/IHistorySource.cs
index 7e9fc00..2eb61ea 100644
--- a/CRD/Utils/Structs/History/IHistorySource.cs
+++ b/CRD/Utils/Structs/History/IHistorySource.cs
@@ -10,6 +10,8 @@ public interface IHistorySource{
string GetSeasonNum();
string GetSeasonId();
+ string GetImageUrl();
+
string GetEpisodeId();
string GetEpisodeNumber();
string GetEpisodeTitle();
diff --git a/CRD/Utils/UI/UiEnumToBoolConverter.cs b/CRD/Utils/UI/UiEnumToBoolConverter.cs
new file mode 100644
index 0000000..993752d
--- /dev/null
+++ b/CRD/Utils/UI/UiEnumToBoolConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace CRD.Utils.UI;
+
+public class UiEnumToBoolConverter : IValueConverter{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture){
+ if (value == null || parameter == null)
+ return false;
+
+ string enumString = parameter.ToString();
+ if (enumString == null)
+ return false;
+
+ return value.ToString() == enumString;
+ }
+
+ public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture){
+ if ((bool)value && parameter != null){
+ return Enum.Parse(targetType, parameter.ToString() ?? string.Empty);
+ }
+
+ return Avalonia.Data.BindingOperations.DoNothing;
+ }
+}
\ No newline at end of file
diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs
index c00b527..b2557ce 100644
--- a/CRD/ViewModels/AddDownloadPageViewModel.cs
+++ b/CRD/ViewModels/AddDownloadPageViewModel.cs
@@ -208,6 +208,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
QueueManager.Instance.CrAddMusicMetaToQueue(meta);
}
}
+ } else if (AddAllEpisodes){
+ var musicClass = CrunchyrollManager.Instance.CrMusic;
+ foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){
+ QueueManager.Instance.CrAddMusicMetaToQueue(meta);
+ }
}
}
@@ -363,7 +368,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private void PopulateItemsFromMusicVideoList(){
- if (currentMusicVideoList?.Data != null){
+ if (currentMusicVideoList?.Data is{ Count: > 0 }){
foreach (var episode in currentMusicVideoList.Data){
string seasonKey;
switch (episode.EpisodeType){
@@ -394,7 +399,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
- CurrentSelectedSeason = SeasonList.First();
+ if (SeasonList.Count > 0){
+ CurrentSelectedSeason = SeasonList.First();
+ }
}
}
diff --git a/CRD/ViewModels/CalendarPageViewModel.cs b/CRD/ViewModels/CalendarPageViewModel.cs
index 67d8a8f..d491278 100644
--- a/CRD/ViewModels/CalendarPageViewModel.cs
+++ b/CRD/ViewModels/CalendarPageViewModel.cs
@@ -217,7 +217,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
}
var refreshDate = DateTime.Now;
- if (currentWeek?.FirstDayOfWeek != null){
+ if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
}
@@ -239,7 +239,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
}
var refreshDate = DateTime.Now;
- if (currentWeek?.FirstDayOfWeek != null){
+ if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
}
diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs
index 7e6cdc6..d9bef91 100644
--- a/CRD/ViewModels/HistoryPageViewModel.cs
+++ b/CRD/ViewModels/HistoryPageViewModel.cs
@@ -20,6 +20,7 @@ using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData;
+using FluentAvalonia.UI.Controls;
using ReactiveUI;
namespace CRD.ViewModels;
@@ -115,6 +116,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty]
private static string _progressText;
+ #region Table Mode
+
+ [ObservableProperty]
+ private static EpisodeDownloadMode _selectedDownloadMode = EpisodeDownloadMode.OnlySubs;
+
+ [ObservableProperty]
+ public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
+
+ #endregion
+
public Vector LastScrollOffset { get; set; } = Vector.Zero;
public HistoryPageViewModel(){
@@ -528,6 +539,26 @@ public partial class HistoryPageViewModel : ViewModelBase{
await episode.DownloadEpisode();
}
}
+
+ [RelayCommand]
+ public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
+ var downloadMode = SelectedDownloadMode;
+
+ if (downloadMode != EpisodeDownloadMode.Default){
+ await episode.DownloadEpisode(downloadMode);
+ }
+ }
+
+ [RelayCommand]
+ public async Task DownloadSeasonAllOnlyOptions(HistorySeason season){
+ var downloadMode = SelectedDownloadMode;
+
+ if (downloadMode != EpisodeDownloadMode.Default){
+ foreach (var episode in season.EpisodesList){
+ await episode.DownloadEpisode(downloadMode);
+ }
+ }
+ }
[RelayCommand]
public void ToggleDownloadedMark(SeasonDialogArgs seriesArgs){
@@ -575,6 +606,15 @@ public partial class HistoryPageViewModel : ViewModelBase{
public void ToggleInactive(){
CfgManager.UpdateHistoryFile();
}
+
+ partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
+ SelectedDownloadIcon = SelectedDownloadMode switch{
+ EpisodeDownloadMode.OnlyVideo => Symbol.Video,
+ EpisodeDownloadMode.OnlyAudio => Symbol.Audio,
+ EpisodeDownloadMode.OnlySubs => Symbol.ClosedCaption,
+ _ => Symbol.ClosedCaption
+ };
+ }
}
public class HistoryPageProperties{
diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs
index dd039bc..c2df0c3 100644
--- a/CRD/ViewModels/SeriesPageViewModel.cs
+++ b/CRD/ViewModels/SeriesPageViewModel.cs
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
+using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
@@ -32,9 +33,18 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _showMonitoredBookmark;
+ [ObservableProperty]
+ public static bool _showFeaturedMusicButton;
+
[ObservableProperty]
public static bool _sonarrConnected;
+ [ObservableProperty]
+ private static EpisodeDownloadMode _selectedDownloadMode = EpisodeDownloadMode.OnlySubs;
+
+ [ObservableProperty]
+ public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
+
private IStorageProvider? _storageProvider;
public SeriesPageViewModel(){
@@ -63,6 +73,10 @@ public partial class SeriesPageViewModel : ViewModelBase{
}
SelectedSeries.UpdateSeriesFolderPath();
+
+ if (SelectedSeries.SeriesStreamingService == StreamingService.Crunchyroll && SelectedSeries.SeriesType != SeriesType.Artist){
+ ShowFeaturedMusicButton = true;
+ }
}
@@ -95,6 +109,33 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.UpdateSeriesFolderPath();
}
+ [RelayCommand]
+ public async Task OpenFeaturedMusicDialog(){
+ if (SelectedSeries.SeriesStreamingService != StreamingService.Crunchyroll || SelectedSeries.SeriesType == SeriesType.Artist){
+ return;
+ }
+
+ var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(SelectedSeries.SeriesId ?? string.Empty,
+ CrunchyrollManager.Instance.CrunOptions.HistoryLang ?? CrunchyrollManager.Instance.DefaultLocale, true, true);
+
+ if (musicList is{ Data.Count: > 0 }){
+ var dialog = new CustomContentDialog(){
+ Title = "Featured Music",
+ CloseButtonText = "Close",
+ FullSizeDesired = true
+ };
+
+ var viewModel = new ContentDialogFeaturedMusicViewModel(dialog, musicList, CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists);
+ dialog.Content = new ContentDialogFeaturedMusicView(){
+ DataContext = viewModel
+ };
+
+ var dialogResult = await dialog.ShowAsync();
+ } else{
+ MessageBus.Current.SendMessage(new ToastMessage($"No featured music found", ToastType.Warning, 3));
+ }
+ }
+
[RelayCommand]
public async Task MatchSonarrSeries_Button(){
var dialog = new ContentDialog(){
@@ -151,7 +192,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (dialogResult == ContentDialogResult.Primary){
var sonarrEpisode = viewModel.CurrentSonarrEpisode;
-
+
foreach (var selectedSeriesSeason in SelectedSeries.Seasons){
foreach (var historyEpisode in selectedSeriesSeason.EpisodesList.Where(historyEpisode => historyEpisode.SonarrEpisodeId == sonarrEpisode.Id.ToString())){
historyEpisode.SonarrEpisodeId = string.Empty;
@@ -162,7 +203,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
historyEpisode.SonarrIsMonitored = false;
}
}
-
+
episode.AssignSonarrEpisodeData(sonarrEpisode);
CfgManager.UpdateHistoryFile();
}
@@ -190,6 +231,26 @@ public partial class SeriesPageViewModel : ViewModelBase{
}
}
+ [RelayCommand]
+ public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
+ var downloadMode = SelectedDownloadMode;
+
+ if (downloadMode != EpisodeDownloadMode.Default){
+ await episode.DownloadEpisode(downloadMode);
+ }
+ }
+
+ [RelayCommand]
+ public async Task DownloadSeasonAllOnlyOptions(HistorySeason season){
+ var downloadMode = SelectedDownloadMode;
+
+ if (downloadMode != EpisodeDownloadMode.Default){
+ foreach (var episode in season.EpisodesList){
+ await episode.DownloadEpisode(downloadMode);
+ }
+ }
+ }
+
[RelayCommand]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
@@ -263,4 +324,14 @@ public partial class SeriesPageViewModel : ViewModelBase{
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
}
}
+
+
+ partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
+ SelectedDownloadIcon = SelectedDownloadMode switch{
+ EpisodeDownloadMode.OnlyVideo => Symbol.Video,
+ EpisodeDownloadMode.OnlyAudio => Symbol.Audio,
+ EpisodeDownloadMode.OnlySubs => Symbol.ClosedCaption,
+ _ => Symbol.ClosedCaption
+ };
+ }
}
\ No newline at end of file
diff --git a/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs b/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs
new file mode 100644
index 0000000..d5c1221
--- /dev/null
+++ b/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CRD.Downloader;
+using CRD.Downloader.Crunchyroll;
+using CRD.Utils.Structs.Crunchyroll.Music;
+using CRD.Utils.Structs.History;
+using CRD.Utils.UI;
+using DynamicData;
+using FluentAvalonia.UI.Controls;
+
+namespace CRD.ViewModels.Utils;
+
+public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
+ private readonly CustomContentDialog dialog;
+
+ [ObservableProperty]
+ private ObservableCollection _featuredMusicList = new();
+
+ [ObservableProperty]
+ private bool _musicInHistory;
+
+ private CrunchyMusicVideoList featuredMusic;
+
+ public ContentDialogFeaturedMusicViewModel(CustomContentDialog contentDialog, CrunchyMusicVideoList featuredMusic, bool crunOptionsHistoryIncludeCrArtists){
+ ArgumentNullException.ThrowIfNull(contentDialog);
+
+ this.featuredMusic = featuredMusic;
+
+ dialog = contentDialog;
+ dialog.Closed += DialogOnClosed;
+ dialog.PrimaryButtonClick += SaveButton;
+
+ if (crunOptionsHistoryIncludeCrArtists){
+ var episodeList = featuredMusic.Data
+ .Select(video =>
+ CrunchyrollManager.Instance.HistoryList
+ .FirstOrDefault(h => h.SeriesId == video.GetSeriesId())?
+ .Seasons.FirstOrDefault(s => s.SeasonId == video.GetSeasonId())?
+ .EpisodesList.FirstOrDefault(e => e.EpisodeId == video.GetEpisodeId()))
+ .Where(episode => episode != null)
+ .ToList();
+
+ if (episodeList.Count > 0){
+ FeaturedMusicList.Clear();
+ FeaturedMusicList!.AddRange(episodeList);
+ }
+ } else{
+ List episodeList =[];
+
+ foreach (var crunchyMusicVideo in featuredMusic.Data){
+ var newHistoryEpisode = new HistoryEpisode{
+ EpisodeTitle = crunchyMusicVideo.GetEpisodeTitle(),
+ EpisodeDescription = crunchyMusicVideo.GetEpisodeDescription(),
+ EpisodeId = crunchyMusicVideo.GetEpisodeId(),
+ Episode = crunchyMusicVideo.GetEpisodeNumber(),
+ EpisodeSeasonNum = crunchyMusicVideo.GetSeasonNum(),
+ SpecialEpisode = crunchyMusicVideo.IsSpecialEpisode(),
+ HistoryEpisodeAvailableDubLang = crunchyMusicVideo.GetEpisodeAvailableDubLang(),
+ HistoryEpisodeAvailableSoftSubs = crunchyMusicVideo.GetEpisodeAvailableSoftSubs(),
+ EpisodeCrPremiumAirDate = crunchyMusicVideo.GetAvailableDate(),
+ EpisodeType = crunchyMusicVideo.GetEpisodeType(),
+ IsEpisodeAvailableOnStreamingService = true,
+ ThumbnailImageUrl = crunchyMusicVideo.GetImageUrl(),
+ };
+ episodeList.Add(newHistoryEpisode);
+ newHistoryEpisode.LoadImage();
+ }
+
+ if (episodeList.Count > 0){
+ FeaturedMusicList.Clear();
+ FeaturedMusicList.AddRange(episodeList);
+ }
+ }
+ }
+
+ [RelayCommand]
+ public void DownloadEpisode(HistoryEpisode episode){
+ episode.DownloadEpisode();
+ }
+
+ private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
+ dialog.PrimaryButtonClick -= SaveButton;
+ }
+
+ private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
+ dialog.Closed -= DialogOnClosed;
+ }
+}
\ No newline at end of file
diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml
index e083e80..3dbb7cc 100644
--- a/CRD/Views/HistoryPageView.axaml
+++ b/CRD/Views/HistoryPageView.axaml
@@ -13,12 +13,13 @@
Unloaded="OnUnloaded"
Loaded="Control_OnLoaded">
-
+
+
@@ -406,6 +407,7 @@
+
@@ -419,7 +421,8 @@
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
-
+
+
-
+
+
@@ -516,7 +520,7 @@
@@ -717,27 +721,41 @@
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
+ Command="{Binding DownloadEpisodeDefault}">
@@ -817,13 +835,14 @@
@@ -933,6 +952,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -137,6 +139,19 @@
+
+
+
Edit
-
+
-
-
-
-
-
-
+
+
-
+
@@ -448,13 +460,12 @@
-
-
+
-
+
-
+
+
-
+
@@ -520,8 +532,7 @@
+ Command="{Binding DownloadEpisodeDefault}">
@@ -533,13 +544,15 @@
+ Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).DownloadEpisodeOnlyOptions}"
+ CommandParameter="{Binding .}">
-
+
+
+
-
+
@@ -630,6 +643,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs b/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs
new file mode 100644
index 0000000..33af031
--- /dev/null
+++ b/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using CRD.Utils.UI;
+
+namespace CRD.Views.Utils;
+
+public partial class ContentDialogFeaturedMusicView : UserControl{
+ public ContentDialogFeaturedMusicView(){
+ InitializeComponent();
+ }
+}
\ No newline at end of file