Add - Added option to **download audio only as MP3**

Add - Added **File Name Whitespace Substitute** option in settings [#284](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/284)
Add - Added **download mode toggle (video/audio/subs)** for seasons with the ability to switch between options [#281](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/281)
Add - Added **download all** for only (video/audio/subs) for a season [#281](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/281)
Chg - Changed to **display a message** when the calendar fails to load due to Cloudflare issues [#283](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/283)
Chg - Adjusted **Calendar upcoming filter** for improved accuracy
Fix - Fixed **duplicate/wrong Crunchyroll versions** appearing in downloads [#285](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/285)
Fix - Fixed issue where **episodes with non-Japanese audio URLs** couldn't be added
Fix - Fixed **calendar crash** on Cloudflare failure [#283](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/283)
Fix - Fixed **audio-only downloads** [#279](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/279)
Fix - Fixed **crash when no featured music** is present
Fix - Fixed **"All" button not working** for music in the Add Downloads tab
Fix - Fixed that an **empty File Name Whitespace Substitute** removed all whitespaces
This commit is contained in:
Elwador 2025-06-28 09:13:28 +02:00
parent d80f9b56a0
commit 67f3d7a84a
27 changed files with 787 additions and 113 deletions

View file

@ -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("<title>Just a moment...</title>") ||
response.ResponseContent.Contains("<title>Access denied</title>") ||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
response.ResponseContent.Trim().Equals("error code: 1020") ||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", 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<CalendarDay>();
@ -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);
}
}

View file

@ -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();
}

View file

@ -17,7 +17,7 @@ public class CrMusic{
public async Task<CrunchyMusicVideoList?> 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<CrunchyMusicVideo?> 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<CrunchyMusicVideoList?> 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);
}

View file

@ -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<string> 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
};

View file

@ -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);

View file

@ -319,6 +319,14 @@
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="FileName Whitespace Substitute"
Description="Character used to replace whitespace in file name variables like ${seriesTitle}">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="50"
Text="{Binding FileNameWhitespaceSubstitute}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Filename"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs} - Folder with \\">
@ -345,11 +353,17 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP4" Description="Outputs a mp4 instead of a mkv - not recommended to use this option">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP4" Description="Outputs an MP4 instead of an MKV — not recommended to use this option">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP3" Description="Outputs an MP3 instead of an MKV/MP4 if only audio streams were downloaded">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxToMp3}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Keep Subtitles separate">
<controls:SettingsExpanderItem.Footer>

View file

@ -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<string>(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!));
episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString()));
allHistoryEpisodes = allHistoryEpisodes
.Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId))
.ToList();

View file

@ -95,7 +95,7 @@ public partial class QueueManager : ObservableObject{
}
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> 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;

View file

@ -251,6 +251,13 @@ public enum CrunchyUrlType{
Unknown
}
public enum EpisodeDownloadMode{
Default,
OnlyVideo,
OnlyAudio,
OnlySubs,
}
public enum SonarrCoverType{
Banner,
FanArt,

View file

@ -9,7 +9,7 @@ using CRD.Utils.Structs;
namespace CRD.Utils.Files;
public class FileNameManager{
public static List<string> ParseFileName(string input, List<Variable> variables, int numbers, List<string> @override){
public static List<string> ParseFileName(string input, List<Variable> variables, int numbers,string whiteSpaceReplace, List<string> @override){
Regex varRegex = new Regex(@"\${[A-Za-z1-9]+}");
var matches = varRegex.Matches(input).Cast<Match>().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);

View file

@ -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; }

View file

@ -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; }

View file

@ -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
}

View file

@ -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
}

View file

@ -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; }

View file

@ -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<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_episode_available_dub_lang")]
public List<string> 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)));
}
}

View file

@ -10,6 +10,8 @@ public interface IHistorySource{
string GetSeasonNum();
string GetSeasonId();
string GetImageUrl();
string GetEpisodeId();
string GetEpisodeNumber();
string GetEpisodeTitle();

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}

View file

@ -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{

View file

@ -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
};
}
}

View file

@ -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<HistoryEpisode> _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<HistoryEpisode> 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;
}
}

View file

@ -13,12 +13,13 @@
Unloaded="OnUnloaded"
Loaded="Control_OnLoaded">
<UserControl.Resources>
<UserControl.Resources>
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" />
<ui:UiEnumToBoolConverter x:Key="EnumToBoolConverter" />
</UserControl.Resources>
@ -406,6 +407,7 @@
<TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap"
Text="{Binding SeriesDescription}" MinWidth="200">
</TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock>
@ -419,7 +421,8 @@
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
@ -435,7 +438,8 @@
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
</StackPanel>
@ -516,7 +520,7 @@
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
IsChecked="{Binding IsInactive}"
IsChecked="{Binding IsInactive}"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleInactive}">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
@ -717,27 +721,41 @@
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<ui:EpisodeHighlightTextBlock
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
Episode="{Binding .}"
StreamingService="Crunchyroll"
MinWidth="50"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"/>
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
Margin="0 0 5 0 "
Symbol="AlertOn"
FontSize="18"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ToolTip.Tip>
<TextBlock Text="Episode unavailable — it might not be available on the streaming service or got moved to a different season" FontSize="15" />
</ToolTip.Tip>
</controls:SymbolIcon>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<ui:EpisodeHighlightTextBlock
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
Episode="{Binding .}"
StreamingService="Crunchyroll"
MinWidth="50"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="Dubs: ">
</TextBlock>
<ui:HighlightingTextBlock
Items="{Binding HistoryEpisodeAvailableDubLang}"
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
@ -747,19 +765,20 @@
FontSize="12"
Opacity="0.8"
FontStyle="Italic" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1"
<StackPanel Grid.Column="2"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center"
Margin="0 0 5 0"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}">
<controls:ImageIcon
IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png"
@ -806,8 +825,7 @@
<Button Margin="0 0 5 0" FontStyle="Italic"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DownloadEpisode}"
CommandParameter="false">
Command="{Binding DownloadEpisodeDefault}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download"
FontSize="18" />
@ -817,13 +835,14 @@
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
Command="{Binding DownloadEpisode}"
CommandParameter="true">
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).DownloadEpisodeOnlyOptions}"
CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" />
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Download Subs" FontSize="15" />
<TextBlock Text="Download Only" FontSize="15" />
</ToolTip.Tip>
</Button>
@ -933,6 +952,69 @@
</StackPanel>
</Button>
<StackPanel>
<ToggleButton x:Name="DropdownButtonOnlyDownload"
Margin="10 0 0 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonOnlyDownload, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonOnlyDownload}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel Orientation="Vertical">
<Button Margin="10 5"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Content="Download All"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).DownloadSeasonAllOnlyOptions}"
CommandParameter="{Binding }">
</Button>
<Rectangle Height="1" Fill="Gray" Margin="0,8,0,8" />
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Video" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyVideo}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Audio" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyAudio}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Sub" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlySubs}" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic"

View file

@ -14,8 +14,10 @@
<UserControl.Resources>
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
<ui:UiEnumToBoolConverter x:Key="EnumToBoolConverter" />
</UserControl.Resources>
<Grid>
<Grid Margin="10">
<Grid.RowDefinitions>
@ -137,6 +139,19 @@
<StackPanel Orientation="Horizontal">
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Refresh Series</Button>
<Button Margin="0 0 5 10" FontStyle="Italic"
IsVisible="{Binding ShowFeaturedMusicButton}"
VerticalAlignment="Center"
Command="{Binding OpenFeaturedMusicDialog}">
<ToolTip.Tip>
<TextBlock Text="Featured Music" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Audio" FontSize="18" />
</StackPanel>
</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic"
@ -324,7 +339,7 @@
</StackPanel>
<StackPanel IsVisible="{Binding EditMode}">
<Button Width="30" Height="30" Margin="0 0 5 0"
<Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0"
IsVisible="{Binding SonarrConnected}"
Command="{Binding MatchSonarrSeries_Button}">
@ -336,20 +351,17 @@
</Grid>
</Button>
</StackPanel>
<StackPanel IsVisible="{Binding EditMode}">
<Button Height="30" Margin="0 0 10 0"
<Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0"
IsVisible="{Binding SonarrConnected}"
Command="{Binding RefreshSonarrEpisodeMatch}">
<Grid>
<StackPanel Orientation="Horizontal">
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
<TextBlock Margin="5 0 0 0" Text="Rematch Episodes"></TextBlock>
</StackPanel>
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
<ToolTip.Tip>
<TextBlock Text="Rematch all Sonarr Episodes" FontSize="15" />
<TextBlock Text="Refresh all Sonarr Episode matches" FontSize="15" />
</ToolTip.Tip>
</Grid>
</Button>
@ -448,13 +460,12 @@
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
<TextBlock Text="{Binding SonarrSeasonEpisodeText}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ShowMonitoredBookmark}"
HorizontalAlignment="Center"
@ -476,6 +487,7 @@
</controls:SymbolIcon>
</StackPanel>
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).MatchSonarrEpisode_Button}"
@ -488,7 +500,7 @@
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
Source="../Assets/sonarr_inactive.png" Width="25"
Height="25" />
</StackPanel>
</Button>
@ -520,8 +532,7 @@
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DownloadEpisode}"
CommandParameter="false">
Command="{Binding DownloadEpisodeDefault}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" FontSize="18" />
</StackPanel>
@ -533,13 +544,15 @@
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
Command="{Binding DownloadEpisode}"
CommandParameter="true">
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).DownloadEpisodeOnlyOptions}"
CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" />
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Download Subs" FontSize="15" />
<TextBlock Text="Download Only" FontSize="15" />
</ToolTip.Tip>
</Button>
@ -630,6 +643,70 @@
</StackPanel>
</Button>
<StackPanel>
<ToggleButton x:Name="DropdownButtonOnlyDownload"
Margin="10 0 0 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonOnlyDownload, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonOnlyDownload}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel Orientation="Vertical">
<Button Margin="10 5"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Content="Download All"
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).DownloadSeasonAllOnlyOptions}"
CommandParameter="{Binding }">
</Button>
<Rectangle Height="1" Fill="Gray" Margin="0,8,0,8" />
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Video" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyVideo}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Audio" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyAudio}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Sub" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlySubs}" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"

View file

@ -0,0 +1,99 @@
<ui:CustomContentDialog xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CRD.ViewModels.Utils"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:CRD.Utils.UI"
x:DataType="vm:ContentDialogFeaturedMusicViewModel"
x:Class="CRD.Views.Utils.ContentDialogFeaturedMusicView">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding FeaturedMusicList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="RootGrid">
<Image Width="320" Height="180" Stretch="Fill" HorizontalAlignment="Center" Source="/Assets/coming_soon_ep.jpg" />
<Image Width="320" Height="180" Source="{Binding ThumbnailImage}" Stretch="UniformToFill" />
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" IsVisible="{Binding $parent[UserControl].((vm:ContentDialogFeaturedMusicViewModel)DataContext).MusicInHistory}">
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
Padding="5"
IsVisible="{Binding !WasDownloaded}">
<Grid>
<Ellipse Width="25" Height="25" Fill="Gray" />
<controls:SymbolIcon Symbol="Checkmark" FontSize="18" />
</Grid>
</Button>
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
Padding="5"
IsVisible="{Binding WasDownloaded}">
<Grid>
<Ellipse Width="25" Height="25" Fill="#21a556" />
<controls:SymbolIcon Symbol="Checkmark" FontSize="18" />
</Grid>
</Button>
</StackPanel>
<Border Grid.Row="0" Background="#80000000"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding #RootGrid.IsPointerOver}">
<Button FontStyle="Italic"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:ContentDialogFeaturedMusicViewModel)DataContext).DownloadEpisode}"
CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" FontSize="24" />
</StackPanel>
</Button>
</Border>
</Grid>
<!-- 320 180 -->
<TextBlock Grid.Row="1" HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding EpisodeTitle}"
TextWrapping="Wrap"
Width="310"
FontSize="14"
Height="35"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding EpisodeTitle}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</ui:CustomContentDialog>

View file

@ -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();
}
}