diff --git a/CRD/App.axaml b/CRD/App.axaml
index 6d777cb..9e9d594 100644
--- a/CRD/App.axaml
+++ b/CRD/App.axaml
@@ -20,6 +20,7 @@
+
\ No newline at end of file
diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs
index 37dfbf5..e3c2cba 100644
--- a/CRD/Downloader/CalendarManager.cs
+++ b/CRD/Downloader/CalendarManager.cs
@@ -65,8 +65,8 @@ public class CalendarManager{
return forDate;
}
- var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "de")
- ? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "de"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null)
+ var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us")
+ ? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null)
: HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null);
diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs
index 7cf5b7e..704ead7 100644
--- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs
+++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs
@@ -147,64 +147,7 @@ public class CrunchyrollManager{
options.History = true;
- if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
- var optionsYaml = new CrDownloadOptionsYaml();
-
- optionsYaml.UseCrBetaApi = true;
- optionsYaml.AutoDownload = false;
- optionsYaml.RemoveFinishedDownload = false;
- optionsYaml.Chapters = true;
- optionsYaml.Hslang = "none";
- optionsYaml.Force = "Y";
- optionsYaml.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]";
- optionsYaml.Partsize = 10;
- optionsYaml.DlSubs = new List{ "en-US" };
- optionsYaml.SkipMuxing = false;
- optionsYaml.MkvmergeOptions = new List{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" };
- optionsYaml.FfmpegOptions = new();
- optionsYaml.DefaultAudio = "ja-JP";
- optionsYaml.DefaultSub = "en-US";
- optionsYaml.QualityAudio = "best";
- optionsYaml.QualityVideo = "best";
- optionsYaml.CcTag = "CC";
- optionsYaml.CcSubsFont = "Trebuchet MS";
- optionsYaml.FsRetryTime = 5;
- optionsYaml.Numbers = 2;
- optionsYaml.Timeout = 15000;
- optionsYaml.DubLang = new List(){ "ja-JP" };
- optionsYaml.SimultaneousDownloads = 2;
- // options.AccentColor = Colors.SlateBlue.ToString();
- optionsYaml.Theme = "System";
- optionsYaml.SelectedCalendarLanguage = "en-us";
- optionsYaml.CalendarDubFilter = "none";
- optionsYaml.CustomCalendar = true;
- optionsYaml.DlVideoOnce = true;
- optionsYaml.StreamEndpoint = "web/firefox";
- optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
- optionsYaml.HistoryLang = DefaultLocale;
-
- optionsYaml.BackgroundImageOpacity = 0.5;
- optionsYaml.BackgroundImageBlurRadius = 10;
-
- optionsYaml.HistoryPageProperties = new HistoryPageProperties{
- SelectedView = HistoryViewType.Posters,
- SelectedSorting = SortingType.SeriesTitle,
- SelectedFilter = FilterType.All,
- ScaleValue = 0.73,
- Ascending = false,
- ShowSeries = true,
- ShowArtists = true
- };
-
- optionsYaml.History = true;
-
- CfgManager.UpdateSettingsFromFileYAML(optionsYaml);
-
- options = Helpers.MigrateSettings(optionsYaml);
- } else{
- CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
- }
-
+ CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
return options;
}
@@ -226,11 +169,6 @@ public class CrunchyrollManager{
PreferredContentSubtitleLanguage = DefaultLocale,
HasPremium = false,
};
-
- if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
- CfgManager.WriteCrSettings();
- Helpers.DeleteFile(CfgManager.PathCrDownloadOptionsOld);
- }
}
public static async Task GetBase64EncodedTokenAsync(){
@@ -290,9 +228,6 @@ public class CrunchyrollManager{
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
Token = CfgManager.ReadJsonFromFile(CfgManager.PathCrToken);
await CrAuth.LoginWithToken();
- if (Path.Exists(CfgManager.PathCrTokenOld)){
- Helpers.DeleteFile(CfgManager.PathCrTokenOld);
- }
} else{
await CrAuth.AuthAnonymous();
}
@@ -391,7 +326,6 @@ public class CrunchyrollManager{
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
-
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List();
foreach (var keyValue in groupByDub){
@@ -999,9 +933,12 @@ public class CrunchyrollManager{
}
#endregion
-
-
- var fetchPlaybackData = await FetchPlaybackData(options, mediaId, mediaGuid, data.Music);
+
+ var fetchPlaybackData = await FetchPlaybackData(options.StreamEndpoint ?? "web/firefox", mediaId, mediaGuid, data.Music);
+ (bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
+ if (!string.IsNullOrEmpty(options.StreamEndpointSecondary) && !(options.StreamEndpoint ?? "web/firefox").Equals(options.StreamEndpointSecondary)){
+ fetchPlaybackData2 = await FetchPlaybackData(options.StreamEndpointSecondary, mediaId, mediaGuid, data.Music);
+ }
if (!fetchPlaybackData.IsOk){
var errorJson = fetchPlaybackData.error;
@@ -1048,6 +985,21 @@ public class CrunchyrollManager{
ErrorText = "Playback data not found"
};
}
+
+ if (fetchPlaybackData2.IsOk){
+ if (fetchPlaybackData.pbData.Data != null)
+ foreach (var keyValuePair in fetchPlaybackData.pbData.Data){
+ var value = fetchPlaybackData2.pbData?.Data?[keyValuePair.Key];
+ var url = value?.Url.First() ?? "";
+
+ var match = Regex.Match(url, @"(.*\.urlset\/)");
+ var shortendUrl = match.Success ? match.Value : url;
+
+ if (!keyValuePair.Value.Url.Any(arrayUrl => arrayUrl != null && arrayUrl.Contains(shortendUrl))){
+ keyValuePair.Value.Url.Add(url);
+ }
+ }
+ }
var pbData = fetchPlaybackData.pbData;
@@ -1106,7 +1058,7 @@ public class CrunchyrollManager{
streams = streams.Select(s => {
s.AudioLang = audDub;
s.HardsubLang = s.HardsubLang;
- s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}";
+ s.Type = $"{s.Format}/{s.AudioLang.CrLocale}/{s.HardsubLang.CrLocale}";
return s;
}).ToList();
@@ -1209,35 +1161,76 @@ public class CrunchyrollManager{
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
- var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
+
+
+ Dictionary streamPlaylistsReqResponseList =[];
- var streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq);
+ foreach (var streamUrl in curStream.Url){
+ var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl ?? string.Empty, HttpMethod.Get, true, true, null);
+ var streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq);
+
+ if (!streamPlaylistsReqResponse.IsOk){
+ dlFailed = true;
+ return new DownloadResponse{
+ Data = new List(),
+ Error = dlFailed,
+ FileName = "./unknown",
+ ErrorText = "Playlist fetch problem"
+ };
+ }
- if (!streamPlaylistsReqResponse.IsOk){
- dlFailed = true;
- return new DownloadResponse{
- Data = new List(),
- Error = dlFailed,
- FileName = "./unknown",
- ErrorText = "Playlist fetch problem"
- };
+ if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
+ streamPlaylistsReqResponseList[streamUrl ?? ""] = streamPlaylistsReqResponse.ResponseContent;
+ }
}
+
+ //Use again when cr has all endpoints with new encoding
+ // var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
+ //
+ // var streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq);
+ //
+ // if (!streamPlaylistsReqResponse.IsOk){
+ // dlFailed = true;
+ // return new DownloadResponse{
+ // Data = new List(),
+ // Error = dlFailed,
+ // FileName = "./unknown",
+ // ErrorText = "Playlist fetch problem"
+ // };
+ // }
if (dlFailed){
Console.WriteLine($"CAN\'T FETCH VIDEO PLAYLISTS!");
} else{
- if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
- var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
- var matchedUrl = match.Success ? match.Value : null;
- //Parse MPD Playlists
- var crLocal = "";
- if (pbData.Meta != null){
- crLocal = pbData.Meta.AudioLocale.CrLocale;
+ // if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
+ // var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
+ // var matchedUrl = match.Success ? match.Value : null;
+ // //Parse MPD Playlists
+ // var crLocal = "";
+ // if (pbData.Meta != null){
+ // crLocal = pbData.Meta.AudioLocale.CrLocale;
+ // }
+ //
+ // MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl);
+ //
+ // List streamServers = new List(streamPlaylists.Data.Keys);
+ if (streamPlaylistsReqResponseList.Count > 0){
+ HashSet streamServers =[];
+ Dictionary playListData = new Dictionary();
+
+ foreach (var curStreams in streamPlaylistsReqResponseList){
+ var match = Regex.Match(curStreams.Key ?? string.Empty, @"(.*\.urlset\/)");
+ var matchedUrl = match.Success ? match.Value : null;
+ //Parse MPD Playlists
+ var crLocal = "";
+ if (pbData.Meta != null){
+ crLocal = pbData.Meta.AudioLocale.CrLocale;
+ }
+ MPDParsed streamPlaylists = MPDParser.Parse(curStreams.Value, Languages.FindLang(crLocal), matchedUrl);
+ streamServers.UnionWith(streamPlaylists.Data.Keys);
+ Helpers.MergePlaylistData(playListData, streamPlaylists.Data);
}
-
- MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl);
-
- List streamServers = new List(streamPlaylists.Data.Keys);
+
options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
if (streamServers.Count == 0){
@@ -1253,8 +1246,11 @@ public class CrunchyrollManager{
options.StreamServer = 1;
}
- string selectedServer = streamServers[options.StreamServer - 1];
- ServerData selectedList = streamPlaylists.Data[selectedServer];
+ // string selectedServer = streamServers[options.StreamServer - 1];
+ // ServerData selectedList = streamPlaylists.Data[selectedServer];
+
+ string selectedServer = streamServers.ToList()[options.StreamServer - 1];
+ ServerData selectedList = playListData[selectedServer];
var videos = selectedList.video.Select(item => new VideoItem{
segments = item.segments,
@@ -1273,8 +1269,20 @@ public class CrunchyrollManager{
resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s"
}).ToList();
- videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width));
- audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
+ // Video: Remove duplicates by resolution (width, height), keep highest bandwidth, then sort
+ videos = videos
+ .GroupBy(v => new{ v.quality.width, v.quality.height })
+ .Select(g => g.OrderByDescending(v => v.bandwidth).First())
+ .OrderBy(v => v.quality.width)
+ .ThenBy(v => v.bandwidth)
+ .ToList();
+
+ // Audio: Remove duplicates, then sort by bandwidth
+ audios = audios
+ .GroupBy(a => new{ a.bandwidth, a.language }) // Add more properties if needed
+ .Select(g => g.First())
+ .OrderBy(a => a.bandwidth)
+ .ToList();
if (string.IsNullOrEmpty(data.VideoQuality)){
Console.Error.WriteLine("Warning: VideoQuality is null or empty. Defaulting to 'best' quality.");
@@ -1478,9 +1486,11 @@ public class CrunchyrollManager{
};
}
+ await CrAuth.RefreshToken(true);
+
Dictionary authDataDict = new Dictionary
- { { "authorization", "Bearer " + Token?.access_token },{"x-cr-content-id", mediaGuid},{"x-cr-video-token", pbData.Meta?.Token ?? string.Empty} };
-
+ { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
+
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
if (encryptionKeys.Count == 0){
@@ -1494,25 +1504,46 @@ public class CrunchyrollManager{
};
}
- if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
- var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
- var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
+ List encryptionKeysAudio =[];
+ if (!string.IsNullOrEmpty(chosenVideoSegments.pssh) && !chosenVideoSegments.pssh.Equals(chosenAudioSegments.pssh)){
+ Console.WriteLine("Video and Audio PSSH different requesting Audio encryption keys");
+ encryptionKeysAudio = await _widevine.getKeys(chosenAudioSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
+ if (encryptionKeysAudio.Count == 0){
+ Console.Error.WriteLine("Failed to get audio encryption keys");
+ dlFailed = true;
+ return new DownloadResponse{
+ Data = files,
+ Error = dlFailed,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
+ ErrorText = "Couldn't get DRM audio encryption keys"
+ };
+ }
+ }
- //mp4decrypt
- var commandBase = $"--show-progress --key {keyId}:{key}";
+ if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
var tempTsFileName = Path.GetFileName(tempTsFile);
var tempTsFileWorkDir = Path.GetDirectoryName(tempTsFile) ?? CfgManager.PathVIDEOS_DIR;
- var commandVideo = commandBase + $" \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\"";
- var commandAudio = commandBase + $" \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\"";
+
+ // Use audio keys if available, otherwise fallback to video keys
+ var audioKeysToUse = encryptionKeysAudio.Count > 0 ? encryptionKeysAudio : encryptionKeys;
+
+ // === mp4decrypt command ===
+ var videoKey = encryptionKeys[0];
+ var videoKeyParam = BuildMp4DecryptKeyParam(videoKey.KeyID, videoKey.Bytes);
+ var commandVideo = $"--show-progress {videoKeyParam} \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\"";
+
+ var audioKey = audioKeysToUse[0];
+ var audioKeyParam = BuildMp4DecryptKeyParam(audioKey.KeyID, audioKey.Bytes);
+ var commandAudio = $"--show-progress {audioKeyParam} \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\"";
bool shaka = Path.Exists(CfgManager.PathShakaPackager);
if (shaka){
- commandBase = " --enable_raw_key_decryption " +
- string.Join(" ",
- encryptionKeys.Select(kb =>
- $"--keys key_id={BitConverter.ToString(kb.KeyID).Replace("-", "").ToLower()}:key={BitConverter.ToString(kb.Bytes).Replace("-", "").ToLower()}"));
- commandVideo = $"input=\"{tempTsFileName}.video.enc.m4s\",stream=video,output=\"{tempTsFileName}.video.m4s\"" + commandBase;
- commandAudio = $"input=\"{tempTsFileName}.audio.enc.m4s\",stream=audio,output=\"{tempTsFileName}.audio.m4s\"" + commandBase;
+ // === shaka-packager command ===
+ var shakaVideoKeys = BuildShakaKeysParam(encryptionKeys);
+ commandVideo = $"input=\"{tempTsFileName}.video.enc.m4s\",stream=video,output=\"{tempTsFileName}.video.m4s\" {shakaVideoKeys}";
+
+ var shakaAudioKeys = BuildShakaKeysParam(audioKeysToUse);
+ commandAudio = $"input=\"{tempTsFileName}.audio.enc.m4s\",stream=audio,output=\"{tempTsFileName}.audio.m4s\" {shakaAudioKeys}";
}
if (videoDownloaded){
@@ -2085,13 +2116,13 @@ public class CrunchyrollManager{
#region Fetch Playback Data
- private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrDownloadOptions options, string mediaId, string mediaGuidId, bool music){
+ private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string streamEndpoint, string mediaId, string mediaGuidId, bool music){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary()
};
- var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
+ var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{streamEndpoint}/play";
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){
@@ -2119,12 +2150,12 @@ public class CrunchyrollManager{
return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent);
}
- private async Task<(bool IsOk, string ResponseContent)> SendPlaybackRequestAsync(string endpoint){
+ private async Task<(bool IsOk, string ResponseContent, string error)> SendPlaybackRequestAsync(string endpoint){
var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, false, null);
return await HttpClientReq.Instance.SendHttpRequest(request);
}
- private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string endpoint){
+ private async Task<(bool IsOk, string ResponseContent, string error)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent, string error) response, string endpoint){
if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response;
var error = StreamError.FromJson(response.ResponseContent);
@@ -2158,7 +2189,7 @@ public class CrunchyrollManager{
foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
- Url = stream.Url,
+ Url = [stream.Url],
IsHardsubbed = true,
HardsubLocale = stream.Hlang,
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
@@ -2167,7 +2198,7 @@ public class CrunchyrollManager{
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
- Url = playStream.Url,
+ Url = [playStream.Url],
IsHardsubbed = false,
HardsubLocale = Locale.DefaulT,
HardsubLang = Languages.DEFAULT_lang
@@ -2322,4 +2353,14 @@ public class CrunchyrollManager{
Console.Error.WriteLine("Chapter request failed");
}
}
+
+ private static string FormatKey(byte[] keyBytes) =>
+ BitConverter.ToString(keyBytes).Replace("-", "").ToLower();
+
+ private static string BuildMp4DecryptKeyParam(byte[] keyId, byte[] key) =>
+ $"--key {FormatKey(keyId)}:{FormatKey(key)}";
+
+ private static string BuildShakaKeysParam(List keys) =>
+ "--enable_raw_key_decryption " + string.Join(" ",
+ keys.Select(k => $"--keys key_id={FormatKey(k.KeyID)}:key={FormatKey(k.Bytes)}"));
}
\ No newline at end of file
diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs
index 366f215..0adcdfc 100644
--- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs
+++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs
@@ -126,6 +126,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint;
+
+ [ObservableProperty]
+ private ComboBoxItem _selectedStreamEndpointSecondary;
[ObservableProperty]
private ComboBoxItem _selectedDefaultDubLang;
@@ -202,13 +205,28 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "console/ps5" },
new(){ Content = "console/xbox_one" },
new(){ Content = "web/edge" },
- // new (){ Content = "web/safari" },
new(){ Content = "web/chrome" },
new(){ Content = "web/fallback" },
- // new (){ Content = "ios/iphone" },
- // new (){ Content = "ios/ipad" },
new(){ Content = "android/phone" },
- new(){ Content = "tv/samsung" }
+ new(){ Content = "android/tablet" },
+ new(){ Content = "tv/samsung" },
+ new(){ Content = "tv/vidaa" }
+ ];
+
+ public ObservableCollection StreamEndpointsSecondary{ get; } =[
+ new(){ Content = "" },
+ new(){ Content = "web/firefox" },
+ new(){ Content = "console/switch" },
+ new(){ Content = "console/ps4" },
+ new(){ Content = "console/ps5" },
+ new(){ Content = "console/xbox_one" },
+ new(){ Content = "web/edge" },
+ new(){ Content = "web/chrome" },
+ new(){ Content = "web/fallback" },
+ new(){ Content = "android/phone" },
+ new(){ Content = "android/tablet" },
+ new(){ Content = "tv/samsung" },
+ new(){ Content = "tv/vidaa" }
];
public ObservableCollection FFmpegHWAccel{ get; } =[];
@@ -282,6 +300,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
+ ComboBoxItem? streamEndpointSecondary = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondary ?? "")) ?? null;
+ SelectedStreamEndpointSecondary = streamEndpointSecondary ?? StreamEndpointsSecondary[0];
+
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
@@ -424,6 +445,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
+ CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondary = SelectedStreamEndpointSecondary.Content + "";
List dubLangs = new List();
foreach (var listBoxItem in SelectedDubLang){
diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml
index 29c4d19..3f5cce5 100644
--- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml
+++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml
@@ -233,8 +233,16 @@
-
-
+
+
+
+
+
+
+
+
diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs
index 4e42c17..042a5e1 100644
--- a/CRD/Downloader/History.cs
+++ b/CRD/Downloader/History.cs
@@ -614,7 +614,7 @@ public class History{
private static readonly object _lock = new object();
- public async Task MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){
+ public async Task MatchHistoryEpisodesWithSonarr(bool rematchAll, HistorySeries historySeries){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
@@ -630,22 +630,38 @@ public class History{
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
}
+ if (!rematchAll){
+ 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));
+
+ if (sonarrEpisode != null){
+ historyEpisode.AssignSonarrEpisodeData(sonarrEpisode);
+ }
+ });
+
+ 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();
+ }
+
List failedEpisodes =[];
Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
- if (updateAll || string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
+ if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
// Create a copy of the episodes list for each thread
var episodesCopy = new List(episodes);
var episode = FindClosestMatchEpisodes(episodesCopy, historyEpisode.EpisodeTitle ?? string.Empty);
if (episode != null){
- historyEpisode.SonarrEpisodeId = episode.Id + "";
- historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
- historyEpisode.SonarrHasFile = episode.HasFile;
- historyEpisode.SonarrIsMonitored = episode.Monitored;
- historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
- historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
-
+ historyEpisode.AssignSonarrEpisodeData(episode);
lock (_lock){
episodes.Remove(episode);
}
@@ -669,12 +685,8 @@ public class History{
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
});
if (episode != null){
- historyEpisode.SonarrEpisodeId = episode.Id + "";
- historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
- historyEpisode.SonarrHasFile = episode.HasFile;
- historyEpisode.SonarrIsMonitored = episode.Monitored;
- historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
- historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
+ historyEpisode.AssignSonarrEpisodeData(episode);
+
lock (_lock){
episodes.Remove(episode);
}
@@ -688,12 +700,8 @@ public class History{
});
if (episode1 != null){
- historyEpisode.SonarrEpisodeId = episode1.Id + "";
- historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + "";
- historyEpisode.SonarrHasFile = episode1.HasFile;
- historyEpisode.SonarrIsMonitored = episode1.Monitored;
- historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + "";
- historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + "";
+ historyEpisode.AssignSonarrEpisodeData(episode1);
+
lock (_lock){
episodes.Remove(episode1);
}
@@ -706,12 +714,8 @@ public class History{
return ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode;
});
if (episode2 != null){
- historyEpisode.SonarrEpisodeId = episode2.Id + "";
- historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + "";
- historyEpisode.SonarrHasFile = episode2.HasFile;
- historyEpisode.SonarrIsMonitored = episode2.Monitored;
- historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + "";
- historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
+ historyEpisode.AssignSonarrEpisodeData(episode2);
+
lock (_lock){
episodes.Remove(episode2);
}
diff --git a/CRD/Styling/ContentDialogCustomStyle.axaml b/CRD/Styling/ContentDialogCustomStyle.axaml
new file mode 100644
index 0000000..c4d9678
--- /dev/null
+++ b/CRD/Styling/ContentDialogCustomStyle.axaml
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRD/Utils/DRM/Session.cs b/CRD/Utils/DRM/Session.cs
index 39c635b..37ac645 100644
--- a/CRD/Utils/DRM/Session.cs
+++ b/CRD/Utils/DRM/Session.cs
@@ -80,12 +80,15 @@ public class Session{
public byte[] GetLicenseRequest(){
dynamic licenseRequest;
+ var random = new Random();
+ uint keyControlNonceId = (uint)(random.NextDouble() * Math.Pow(2, 31));
+
if (InitData is WidevineCencHeader){
licenseRequest = new SignedLicenseRequest{
Type = SignedLicenseRequest.MessageType.LicenseRequest,
Msg = new LicenseRequest{
Type = LicenseRequest.RequestType.New,
- KeyControlNonce = 1093602366,
+ KeyControlNonce = keyControlNonceId,
ProtocolVersion = ProtocolVersion.Current,
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
ContentId = new LicenseRequest.ContentIdentification{
@@ -102,7 +105,7 @@ public class Session{
Type = SignedLicenseRequestRaw.MessageType.LicenseRequest,
Msg = new LicenseRequestRaw{
Type = LicenseRequestRaw.RequestType.New,
- KeyControlNonce = 1093602366,
+ KeyControlNonce = keyControlNonceId,
ProtocolVersion = ProtocolVersion.Current,
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
ContentId = new LicenseRequestRaw.ContentIdentification{
diff --git a/CRD/Utils/DRM/Widevine.cs b/CRD/Utils/DRM/Widevine.cs
index f5f1651..f732aa8 100644
--- a/CRD/Utils/DRM/Widevine.cs
+++ b/CRD/Utils/DRM/Widevine.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.IsolatedStorage;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@@ -111,7 +112,28 @@ public class Widevine{
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
playbackRequest2.Content = content;
- var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);
+ // var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);
+
+ var response = (IsOk: false, ResponseContent: "", error: "");
+ for (var attempt = 0; attempt < 3 + 1; attempt++){
+ using (var request = Helpers.CloneHttpRequestMessage(playbackRequest2)){
+ response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ break;
+ }
+
+ if (response.error.Contains("System.Net.Sockets.SocketException (10054)")){
+ Console.Error.WriteLine($"Key Request Attempt {attempt + 1} failed.");
+ if (attempt == 3)
+ break;
+
+ await Task.Delay(1000);
+ } else{
+ break;
+ }
+ }
+ }
if (!response.IsOk){
Console.Error.WriteLine("Failed to get Keys!");
diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs
index 79450d5..9fcfa23 100644
--- a/CRD/Utils/Files/CfgManager.cs
+++ b/CRD/Utils/Files/CfgManager.cs
@@ -16,10 +16,7 @@ namespace CRD.Utils.Files;
public class CfgManager{
private static string workingDirectory = AppContext.BaseDirectory;
-
- public static readonly string PathCrTokenOld = Path.Combine(workingDirectory, "config", "cr_token.yml");
- public static readonly string PathCrDownloadOptionsOld = Path.Combine(workingDirectory, "config", "settings.yml");
-
+
public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
@@ -182,73 +179,6 @@ public class CfgManager{
return properties;
}
-
- #region YAML OLD
-
- public static void UpdateSettingsFromFileYAML(CrDownloadOptionsYaml options){
- string dirPath = Path.GetDirectoryName(PathCrDownloadOptionsOld) ?? string.Empty;
-
- if (!Directory.Exists(dirPath)){
- Directory.CreateDirectory(dirPath);
- }
-
- if (!File.Exists(PathCrDownloadOptionsOld)){
- using (var fileStream = File.Create(PathCrDownloadOptionsOld)){
- }
-
- return;
- }
-
- var input = File.ReadAllText(PathCrDownloadOptionsOld);
-
- if (input.Length <= 0){
- return;
- }
-
- var deserializer = new DeserializerBuilder()
- .WithNamingConvention(UnderscoredNamingConvention.Instance)
- .IgnoreUnmatchedProperties()
- .Build();
-
- var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input);
- var loadedOptions = deserializer.Deserialize(new StringReader(input));
- var instanceOptions = options;
-
- foreach (PropertyInfo property in typeof(CrDownloadOptionsYaml).GetProperties()){
- var yamlMemberAttribute = property.GetCustomAttribute();
- // var jsonMemberAttribute = property.GetCustomAttribute();
- string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name;
-
- if (propertiesPresentInYaml.Contains(yamlPropertyName)){
- PropertyInfo? instanceProperty = instanceOptions.GetType().GetProperty(property.Name);
- if (instanceProperty != null && instanceProperty.CanWrite){
- instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions));
- }
- }
- }
- }
-
- private static HashSet GetTopLevelPropertiesInYaml(string yamlContent){
- var reader = new StringReader(yamlContent);
- var yamlStream = new YamlStream();
- yamlStream.Load(reader);
-
- var properties = new HashSet();
-
- if (yamlStream.Documents.Count > 0 && yamlStream.Documents[0].RootNode is YamlMappingNode rootNode){
- foreach (var entry in rootNode.Children){
- if (entry.Key is YamlScalarNode scalarKey){
- properties.Add(scalarKey.Value);
- }
- }
- }
-
- return properties;
- }
-
- #endregion
-
-
public static void UpdateHistoryFile(){
if (!CrunchyrollManager.Instance.CrunOptions.History){
return;
diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs
index bfba120..1d94792 100644
--- a/CRD/Utils/Helpers.cs
+++ b/CRD/Utils/Helpers.cs
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
@@ -16,6 +17,7 @@ using Avalonia.Media.Imaging;
using CRD.Downloader;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
+using CRD.Utils.HLS;
using CRD.Utils.JsonConv;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
@@ -39,6 +41,22 @@ public class Helpers{
return default;
}
+ public static HttpRequestMessage CloneHttpRequestMessage(HttpRequestMessage originalRequest){
+ var clone = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri){
+ Content = originalRequest.Content?.Clone(),
+ Version = originalRequest.Version
+ };
+ foreach (var header in originalRequest.Headers){
+ clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+
+ foreach (var property in originalRequest.Properties){
+ clone.Properties.Add(property);
+ }
+
+ return clone;
+ }
+
public static T DeepCopy(T obj){
var settings = new JsonSerializerSettings{
ContractResolver = new DefaultContractResolver{
@@ -626,7 +644,7 @@ public class Helpers{
group.Add(descriptionMedia[0]);
}
}
-
+
return languageGroups;
}
@@ -765,94 +783,27 @@ public class Helpers{
}
}
- public static CrDownloadOptions MigrateSettings(CrDownloadOptionsYaml yaml){
- if (yaml == null){
- throw new ArgumentNullException(nameof(yaml));
+ public static void MergePlaylistData(
+ Dictionary target,
+ Dictionary source){
+ foreach (var kvp in source){
+ if (target.TryGetValue(kvp.Key, out var existing)){
+ // Merge audio
+ existing.audio ??=[];
+ if (kvp.Value.audio != null)
+ existing.audio.AddRange(kvp.Value.audio);
+
+ // Merge video
+ existing.video ??=[];
+ if (kvp.Value.video != null)
+ existing.video.AddRange(kvp.Value.video);
+ } else{
+ // Add new entry (clone lists to avoid reference issues)
+ target[kvp.Key] = new ServerData{
+ audio = kvp.Value.audio != null ? new List(kvp.Value.audio) : new List(),
+ video = kvp.Value.video != null ? new List(kvp.Value.video) : new List()
+ };
+ }
}
-
- return new CrDownloadOptions{
- // General Settings
- AutoDownload = yaml.AutoDownload,
- RemoveFinishedDownload = yaml.RemoveFinishedDownload,
- Timeout = yaml.Timeout,
- RetryDelay = yaml.FsRetryTime,
- Force = yaml.Force,
- SimultaneousDownloads = yaml.SimultaneousDownloads,
- Theme = yaml.Theme,
- AccentColor = yaml.AccentColor,
- BackgroundImagePath = yaml.BackgroundImagePath,
- BackgroundImageOpacity = yaml.BackgroundImageOpacity,
- BackgroundImageBlurRadius = yaml.BackgroundImageBlurRadius,
- Override = yaml.Override,
- CcTag = yaml.CcTag,
- Nocleanup = yaml.Nocleanup,
- History = yaml.History,
- HistoryIncludeCrArtists = yaml.HistoryIncludeCrArtists,
- HistoryLang = yaml.HistoryLang,
- HistoryAddSpecials = yaml.HistoryAddSpecials,
- HistorySkipUnmonitored = yaml.HistorySkipUnmonitored,
- HistoryCountSonarr = yaml.HistoryCountSonarr,
- SonarrProperties = yaml.SonarrProperties,
- LogMode = yaml.LogMode,
- DownloadDirPath = yaml.DownloadDirPath,
- DownloadTempDirPath = yaml.DownloadTempDirPath,
- DownloadToTempFolder = yaml.DownloadToTempFolder,
- HistoryPageProperties = yaml.HistoryPageProperties,
- SeasonsPageProperties = yaml.SeasonsPageProperties,
- DownloadSpeedLimit = yaml.DownloadSpeedLimit,
- ProxyEnabled = yaml.ProxyEnabled,
- ProxySocks = yaml.ProxySocks,
- ProxyHost = yaml.ProxyHost,
- ProxyPort = yaml.ProxyPort,
- ProxyUsername = yaml.ProxyUsername,
- ProxyPassword = yaml.ProxyPassword,
-
- // Crunchyroll Settings
- Hslang = yaml.Hslang,
- Kstream = yaml.Kstream,
- Novids = yaml.Novids,
- Noaudio = yaml.Noaudio,
- StreamServer = yaml.StreamServer,
- QualityVideo = yaml.QualityVideo,
- QualityAudio = yaml.QualityAudio,
- FileName = yaml.FileName,
- Numbers = yaml.Numbers,
- Partsize = yaml.Partsize,
- DlSubs = yaml.DlSubs,
- SkipSubs = yaml.SkipSubs,
- SkipSubsMux = yaml.SkipSubsMux,
- SubsAddScaledBorder = yaml.SubsAddScaledBorder,
- IncludeSignsSubs = yaml.IncludeSignsSubs,
- SignsSubsAsForced = yaml.SignsSubsAsForced,
- IncludeCcSubs = yaml.IncludeCcSubs,
- CcSubsFont = yaml.CcSubsFont,
- CcSubsMuxingFlag = yaml.CcSubsMuxingFlag,
- Mp4 = yaml.Mp4,
- VideoTitle = yaml.VideoTitle,
- IncludeVideoDescription = yaml.IncludeVideoDescription,
- DescriptionLang = yaml.DescriptionLang,
- FfmpegOptions = yaml.FfmpegOptions,
- MkvmergeOptions = yaml.MkvmergeOptions,
- DefaultSub = yaml.DefaultSub,
- DefaultSubSigns = yaml.DefaultSubSigns,
- DefaultSubForcedDisplay = yaml.DefaultSubForcedDisplay,
- DefaultAudio = yaml.DefaultAudio,
- DlVideoOnce = yaml.DlVideoOnce,
- KeepDubsSeperate = yaml.KeepDubsSeperate,
- SkipMuxing = yaml.SkipMuxing,
- SyncTiming = yaml.SyncTiming,
- IsEncodeEnabled = yaml.IsEncodeEnabled,
- EncodingPresetName = yaml.EncodingPresetName,
- Chapters = yaml.Chapters,
- DubLang = yaml.DubLang,
- SelectedCalendarLanguage = yaml.SelectedCalendarLanguage,
- CalendarDubFilter = yaml.CalendarDubFilter,
- CustomCalendar = yaml.CustomCalendar,
- CalendarHideDubs = yaml.CalendarHideDubs,
- CalendarFilterByAirDate = yaml.CalendarFilterByAirDate,
- CalendarShowUpcomingEpisodes = yaml.CalendarShowUpcomingEpisodes,
- StreamEndpoint = yaml.StreamEndpoint,
- SearchFetchFeaturedMusic = yaml.SearchFetchFeaturedMusic
- };
}
}
\ No newline at end of file
diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs
index d610e12..9b7dc55 100644
--- a/CRD/Utils/Http/HttpClientReq.cs
+++ b/CRD/Utils/Http/HttpClientReq.cs
@@ -163,7 +163,7 @@ public class HttpClientReq{
cookieStore[domain].Add(cookie);
}
- public async Task<(bool IsOk, string ResponseContent)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false){
+ public async Task<(bool IsOk, string ResponseContent,string error)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false){
string content = string.Empty;
try{
AttachCookies(request);
@@ -178,14 +178,14 @@ public class HttpClientReq{
response.EnsureSuccessStatusCode();
- return (IsOk: true, ResponseContent: content);
+ return (IsOk: true, ResponseContent: content,error:"");
} catch (Exception e){
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
if (!suppressError){
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
}
- return (IsOk: false, ResponseContent: content);
+ return (IsOk: false, ResponseContent: content,error: e.Message);
}
}
diff --git a/CRD/Utils/Parser/MPDTransformer.cs b/CRD/Utils/Parser/MPDTransformer.cs
index 7583992..b695741 100644
--- a/CRD/Utils/Parser/MPDTransformer.cs
+++ b/CRD/Utils/Parser/MPDTransformer.cs
@@ -59,8 +59,8 @@ public class MPDParsed{
}
public class ServerData{
- public List audio{ get; set; }
- public List video{ get; set; }
+ public List audio{ get; set; } =[];
+ public List video{ get; set; } =[];
}
public static class MPDParser{
diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs
index f9e0633..e0d340d 100644
--- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs
+++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs
@@ -274,261 +274,12 @@ public class CrDownloadOptions{
[JsonProperty("stream_endpoint")]
public string? StreamEndpoint{ get; set; }
+
+ [JsonProperty("stream_endpoint_secondary")]
+ public string? StreamEndpointSecondary { get; set; }
[JsonProperty("search_fetch_featured_music")]
public bool SearchFetchFeaturedMusic{ get; set; }
- #endregion
-}
-
-public class CrDownloadOptionsYaml{
- #region General Settings
-
- [YamlMember(Alias = "auto_download", ApplyNamingConventions = false)]
- public bool AutoDownload{ get; set; }
-
- [YamlMember(Alias = "remove_finished_downloads", ApplyNamingConventions = false)]
- public bool RemoveFinishedDownload{ get; set; }
-
- [YamlIgnore]
- public int Timeout{ get; set; }
-
- [YamlIgnore]
- public int FsRetryTime{ get; set; }
-
- [YamlIgnore]
- public string Force{ get; set; } = "";
-
- [YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)]
- public int SimultaneousDownloads{ get; set; }
-
- [YamlMember(Alias = "theme", ApplyNamingConventions = false)]
- public string Theme{ get; set; } = "";
-
- [YamlMember(Alias = "accent_color", ApplyNamingConventions = false)]
- public string? AccentColor{ get; set; }
-
- [YamlMember(Alias = "background_image_path", ApplyNamingConventions = false)]
- public string? BackgroundImagePath{ get; set; }
-
- [YamlMember(Alias = "background_image_opacity", ApplyNamingConventions = false)]
- public double BackgroundImageOpacity{ get; set; }
-
- [YamlMember(Alias = "background_image_blur_radius", ApplyNamingConventions = false)]
- public double BackgroundImageBlurRadius{ get; set; }
-
-
- [YamlIgnore]
- public List Override{ get; } =[];
-
- [YamlIgnore]
- public string CcTag{ get; set; } = "";
-
- [YamlIgnore]
- public bool Nocleanup{ get; } = false;
-
- [YamlMember(Alias = "history", ApplyNamingConventions = false)]
- public bool History{ get; set; }
-
- [YamlMember(Alias = "history_include_cr_artists", ApplyNamingConventions = false)]
- public bool HistoryIncludeCrArtists{ get; set; }
-
- [YamlMember(Alias = "history_lang", ApplyNamingConventions = false)]
- public string? HistoryLang{ get; set; }
-
- [YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)]
- public bool HistoryAddSpecials{ get; set; }
-
- [YamlMember(Alias = "history_skip_unmonitored", ApplyNamingConventions = false)]
- public bool HistorySkipUnmonitored{ get; set; }
-
- [YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)]
- public bool HistoryCountSonarr{ get; set; }
-
- [YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)]
- public SonarrProperties? SonarrProperties{ get; set; }
-
- [YamlMember(Alias = "log_mode", ApplyNamingConventions = false)]
- public bool LogMode{ get; set; }
-
- [YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)]
- public string? DownloadDirPath{ get; set; }
-
- [YamlMember(Alias = "download_temp_dir_path", ApplyNamingConventions = false)]
- public string? DownloadTempDirPath{ get; set; }
-
- [YamlMember(Alias = "download_to_temp_folder", ApplyNamingConventions = false)]
- public bool DownloadToTempFolder{ get; set; }
-
- [YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
- public HistoryPageProperties? HistoryPageProperties{ get; set; }
-
- [YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)]
- public SeasonsPageProperties? SeasonsPageProperties{ get; set; }
-
- [YamlMember(Alias = "download_speed_limit", ApplyNamingConventions = false)]
- public int DownloadSpeedLimit{ get; set; }
-
- [YamlMember(Alias = "proxy_enabled", ApplyNamingConventions = false)]
- public bool ProxyEnabled{ get; set; }
-
- [YamlMember(Alias = "proxy_socks", ApplyNamingConventions = false)]
- public bool ProxySocks{ get; set; }
-
- [YamlMember(Alias = "proxy_host", ApplyNamingConventions = false)]
- public string? ProxyHost{ get; set; }
-
- [YamlMember(Alias = "proxy_port", ApplyNamingConventions = false)]
- public int ProxyPort{ get; set; }
-
- [YamlMember(Alias = "proxy_username", ApplyNamingConventions = false)]
- public string? ProxyUsername{ get; set; }
-
- [YamlMember(Alias = "proxy_password", ApplyNamingConventions = false)]
- public string? ProxyPassword{ get; set; }
-
- #endregion
-
- #region Crunchyroll Settings
-
- [YamlIgnore]
- public bool UseCrBetaApi{ get; set; }
-
- [YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)]
- public string Hslang{ get; set; } = "";
-
- [YamlIgnore]
- public int Kstream{ get; set; }
-
- [YamlMember(Alias = "no_video", ApplyNamingConventions = false)]
- public bool Novids{ get; set; }
-
- [YamlMember(Alias = "no_audio", ApplyNamingConventions = false)]
- public bool Noaudio{ get; set; }
-
- [YamlIgnore]
- public int StreamServer{ get; set; }
-
- [YamlMember(Alias = "quality_video", ApplyNamingConventions = false)]
- public string QualityVideo{ get; set; } = "";
-
- [YamlMember(Alias = "quality_audio", ApplyNamingConventions = false)]
- public string QualityAudio{ get; set; } = "";
-
- [YamlMember(Alias = "file_name", ApplyNamingConventions = false)]
- public string FileName{ get; set; } = "";
-
- [YamlMember(Alias = "leading_numbers", ApplyNamingConventions = false)]
- public int Numbers{ get; set; }
-
- [YamlMember(Alias = "download_part_size", ApplyNamingConventions = false)]
- public int Partsize{ get; set; }
-
-
- [YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)]
- public List DlSubs{ get; set; } =[];
-
- [YamlIgnore]
- public bool SkipSubs{ get; set; }
-
- [YamlMember(Alias = "mux_skip_subs", ApplyNamingConventions = false)]
- public bool SkipSubsMux{ get; set; }
-
- [YamlMember(Alias = "subs_add_scaled_border", ApplyNamingConventions = false)]
- public ScaledBorderAndShadowSelection SubsAddScaledBorder{ get; set; }
-
- [YamlMember(Alias = "include_signs_subs", ApplyNamingConventions = false)]
- public bool IncludeSignsSubs{ get; set; }
-
- [YamlMember(Alias = "mux_signs_subs_flag", ApplyNamingConventions = false)]
- public bool SignsSubsAsForced{ get; set; }
-
- [YamlMember(Alias = "include_cc_subs", ApplyNamingConventions = false)]
- public bool IncludeCcSubs{ get; set; }
-
- [YamlMember(Alias = "cc_subs_font", ApplyNamingConventions = false)]
- public string? CcSubsFont{ get; set; }
-
- [YamlMember(Alias = "mux_cc_subs_flag", ApplyNamingConventions = false)]
- public bool CcSubsMuxingFlag{ get; set; }
-
- [YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)]
- public bool Mp4{ get; set; }
-
- [YamlMember(Alias = "mux_video_title", ApplyNamingConventions = false)]
- public string? VideoTitle{ get; set; }
-
- [YamlMember(Alias = "mux_video_description", ApplyNamingConventions = false)]
- public bool IncludeVideoDescription{ get; set; }
-
- [YamlMember(Alias = "mux_description_lang", ApplyNamingConventions = false)]
- public string? DescriptionLang{ get; set; }
-
- [YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)]
- public List FfmpegOptions{ get; set; } =[];
-
- [YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)]
- public List MkvmergeOptions{ get; set; } =[];
-
- [YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)]
- public string DefaultSub{ get; set; } = "";
-
- [YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)]
- public bool DefaultSubSigns{ get; set; }
-
- [YamlMember(Alias = "mux_default_sub_forced_display", ApplyNamingConventions = false)]
- public bool DefaultSubForcedDisplay{ get; set; }
-
- [YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)]
- public string DefaultAudio{ get; set; } = "";
-
- [YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
- public bool DlVideoOnce{ get; set; }
-
- [YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)]
- public bool KeepDubsSeperate{ get; set; }
-
- [YamlMember(Alias = "mux_skip_muxing", ApplyNamingConventions = false)]
- public bool SkipMuxing{ get; set; }
-
- [YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)]
- public bool SyncTiming{ get; set; }
-
- [YamlMember(Alias = "encode_enabled", ApplyNamingConventions = false)]
- public bool IsEncodeEnabled{ get; set; }
-
- [YamlMember(Alias = "encode_preset", ApplyNamingConventions = false)]
- public string? EncodingPresetName{ get; set; }
-
- [YamlMember(Alias = "chapters", ApplyNamingConventions = false)]
- public bool Chapters{ get; set; }
-
- [YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)]
- public List DubLang{ get; set; } =[];
-
- [YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)]
- public string? SelectedCalendarLanguage{ get; set; }
-
- [YamlMember(Alias = "calendar_dub_filter", ApplyNamingConventions = false)]
- public string? CalendarDubFilter{ get; set; }
-
- [YamlMember(Alias = "calendar_custom", ApplyNamingConventions = false)]
- public bool CustomCalendar{ get; set; }
-
- [YamlMember(Alias = "calendar_hide_dubs", ApplyNamingConventions = false)]
- public bool CalendarHideDubs{ get; set; }
-
- [YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
- public bool CalendarFilterByAirDate{ get; set; }
-
- [YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)]
- public bool CalendarShowUpcomingEpisodes{ get; set; }
-
- [YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
- public string? StreamEndpoint{ get; set; }
-
- [YamlMember(Alias = "search_fetch_featured_music", ApplyNamingConventions = false)]
- public bool SearchFetchFeaturedMusic{ get; set; }
-
#endregion
}
\ No newline at end of file
diff --git a/CRD/Utils/Structs/Crunchyroll/Playback.cs b/CRD/Utils/Structs/Crunchyroll/Playback.cs
index 9130c05..39a8812 100644
--- a/CRD/Utils/Structs/Crunchyroll/Playback.cs
+++ b/CRD/Utils/Structs/Crunchyroll/Playback.cs
@@ -13,7 +13,7 @@ public class StreamDetails{
[JsonProperty("hardsub_locale")]
public Locale? HardsubLocale{ get; set; }
- public string? Url{ get; set; }
+ public List Url{ get; set; }
[JsonProperty("hardsub_lang")]
public required LanguageItem HardsubLang{ get; set; }
@@ -28,7 +28,7 @@ public class StreamDetails{
public class StreamDetailsPop{
public Locale? HardsubLocale{ get; set; }
- public string? Url{ get; set; }
+ public List Url{ get; set; }
public required LanguageItem HardsubLang{ get; set; }
public bool IsHardsubbed{ get; set; }
diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs
index a0cd39f..c41e7ca 100644
--- a/CRD/Utils/Structs/History/HistoryEpisode.cs
+++ b/CRD/Utils/Structs/History/HistoryEpisode.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files;
+using CRD.Utils.Sonarr.Models;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
@@ -58,6 +59,18 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("sonarr_absolut_number")]
public string? SonarrAbsolutNumber{ get; set; }
+ [JsonIgnore]
+ public string SonarrSeasonEpisodeText{
+ get{
+ if (int.TryParse(SonarrSeasonNumber, out int season) &&
+ int.TryParse(SonarrEpisodeNumber, out int episode)){
+ return $"S{season:D2}E{episode:D2}";
+ }
+
+ return $"S{SonarrSeasonNumber}E{SonarrEpisodeNumber}";
+ }
+ }
+
[JsonProperty("history_episode_available_soft_subs")]
public List HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
@@ -118,7 +131,16 @@ public class HistoryEpisode : INotifyPropertyChanged{
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
break;
}
+ }
+
+ 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/UI/CustomContentDialog.cs b/CRD/Utils/UI/CustomContentDialog.cs
new file mode 100644
index 0000000..dd930e4
--- /dev/null
+++ b/CRD/Utils/UI/CustomContentDialog.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Media;
+using FluentAvalonia.UI.Controls;
+
+namespace CRD.Utils.UI;
+
+public class CustomContentDialog : ContentDialog{
+ public static readonly StyledProperty BackgroundImageProperty =
+ AvaloniaProperty.Register(nameof(BackgroundImage));
+
+ public static readonly StyledProperty BackgroundImageOpacityProperty =
+ AvaloniaProperty.Register(nameof(BackgroundImageOpacity), 0.5);
+
+ public static readonly StyledProperty BackgroundImageBlurRadiusProperty =
+ AvaloniaProperty.Register(nameof(BackgroundImageBlurRadius), 10);
+
+ public IImage BackgroundImage{
+ get => GetValue(BackgroundImageProperty);
+ set => SetValue(BackgroundImageProperty, value);
+ }
+
+ public double BackgroundImageOpacity{
+ get => GetValue(BackgroundImageOpacityProperty);
+ set => SetValue(BackgroundImageOpacityProperty, value);
+ }
+
+ public double BackgroundImageBlurRadius{
+ get => GetValue(BackgroundImageBlurRadiusProperty);
+ set => SetValue(BackgroundImageBlurRadiusProperty, value);
+ }
+
+ private Image? _backgroundImage;
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e){
+ base.OnApplyTemplate(e);
+
+ _backgroundImage = e.NameScope.Find("BackgroundImageElement");
+
+ if (_backgroundImage is not null){
+ _backgroundImage.Effect = new BlurEffect{
+ Radius = BackgroundImageBlurRadius
+ };
+ }
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change){
+ base.OnPropertyChanged(change);
+
+ if (change.Property == BackgroundImageBlurRadiusProperty && _backgroundImage?.Effect is BlurEffect blur){
+ blur.Radius = BackgroundImageBlurRadius;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs
index ff67028..c00b527 100644
--- a/CRD/ViewModels/AddDownloadPageViewModel.cs
+++ b/CRD/ViewModels/AddDownloadPageViewModel.cs
@@ -285,8 +285,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private (string locale, string id)? ExtractLocaleAndIdFromUrl(){
- var match = Regex.Match(UrlInput, "/([^/]+)/(?:artist|watch|series)(?:/(?:musicvideo|concert))?/([^/]+)/?");
- return match.Success ? (match.Groups[1].Value, match.Groups[2].Value) : null;
+ var match = Regex.Match(UrlInput, @"^(?:https?:\/\/[^/]+)?(?:\/([a-z]{2}))?\/(?:[^/]+\/)?(artist|watch|series)(?:\/(musicvideo|concert))?\/([^/]+)(?:\/[^/]*)?$");
+
+ return match.Success
+ ? (match.Groups[1].Value ?? "", match.Groups[4].Value)
+ : null;
}
private CrunchyUrlType GetUrlType(){
@@ -341,12 +344,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true);
-
+
if (musicList != null){
currentMusicVideoList = musicList;
PopulateItemsFromMusicVideoList();
}
-
}
SetLoadingState(false);
@@ -477,7 +479,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
-
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
if (CurrentSeasonFullySelected){
@@ -581,18 +582,17 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
episodesBySeason[seasonKey].Add(episodeModel);
}
}
-
+
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
? CrunchyrollManager.Instance.DefaultLocale
: CrunchyrollManager.Instance.CrunOptions.HistoryLang;
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(seriesId, DetermineLocale(locale), true);
-
+
if (musicList != null){
currentMusicVideoList = musicList;
PopulateItemsFromMusicVideoList();
}
-
}
CurrentSelectedSeason = SeasonList.First();
diff --git a/CRD/ViewModels/CalendarPageViewModel.cs b/CRD/ViewModels/CalendarPageViewModel.cs
index bc151c0..67d8a8f 100644
--- a/CRD/ViewModels/CalendarPageViewModel.cs
+++ b/CRD/ViewModels/CalendarPageViewModel.cs
@@ -7,7 +7,6 @@ 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 DynamicData;
diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs
index eda179d..bb5d20f 100644
--- a/CRD/ViewModels/DownloadsPageViewModel.cs
+++ b/CRD/ViewModels/DownloadsPageViewModel.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.ObjectModel;
-using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
@@ -11,7 +10,6 @@ using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
-using CRD.Utils.CustomList;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs
index 8386aa0..7e6cdc6 100644
--- a/CRD/ViewModels/HistoryPageViewModel.cs
+++ b/CRD/ViewModels/HistoryPageViewModel.cs
@@ -114,6 +114,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty]
private static string _progressText;
+
+ public Vector LastScrollOffset { get; set; } = Vector.Zero;
public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance;
@@ -343,7 +345,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
NavToSeries();
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
- if (SelectedSeries != null) _ = CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
+ if (SelectedSeries != null) _ = CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(false, SelectedSeries);
CfgManager.UpdateHistoryFile();
}
diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs
index d44d3b0..dd039bc 100644
--- a/CRD/ViewModels/SeriesPageViewModel.cs
+++ b/CRD/ViewModels/SeriesPageViewModel.cs
@@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
-using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
@@ -8,10 +7,10 @@ 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;
+using CRD.Utils.UI;
using CRD.ViewModels.Utils;
using CRD.Views;
using CRD.Views.Utils;
@@ -133,6 +132,42 @@ public partial class SeriesPageViewModel : ViewModelBase{
}
}
+ [RelayCommand]
+ public async Task MatchSonarrEpisode_Button(HistoryEpisode episode){
+ var dialog = new CustomContentDialog(){
+ Name = "CustomDialog",
+ Title = "Sonarr Episode Matching",
+ PrimaryButtonText = "Save",
+ CloseButtonText = "Close",
+ FullSizeDesired = true,
+ };
+
+ var viewModel = new ContentDialogSonarrMatchEpisodeViewModel(dialog, SelectedSeries, episode);
+ dialog.Content = new ContentDialogSonarrMatchEpisodeView(){
+ DataContext = viewModel
+ };
+
+ var dialogResult = await dialog.ShowAsync();
+
+ 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;
+ historyEpisode.SonarrAbsolutNumber = string.Empty;
+ historyEpisode.SonarrSeasonNumber = string.Empty;
+ historyEpisode.SonarrEpisodeNumber = string.Empty;
+ historyEpisode.SonarrHasFile = false;
+ historyEpisode.SonarrIsMonitored = false;
+ }
+ }
+
+ episode.AssignSonarrEpisodeData(sonarrEpisode);
+ CfgManager.UpdateHistoryFile();
+ }
+ }
+
[RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){
@@ -173,6 +208,12 @@ public partial class SeriesPageViewModel : ViewModelBase{
}
}
+ [RelayCommand]
+ public async Task RefreshSonarrEpisodeMatch(){
+ await CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
+ CfgManager.UpdateHistoryFile();
+ }
+
[RelayCommand]
public async Task UpdateData(string? season){
var result = await SelectedSeries.FetchData(season);
diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs
index 8106ac3..61a0493 100644
--- a/CRD/ViewModels/SettingsPageViewModel.cs
+++ b/CRD/ViewModels/SettingsPageViewModel.cs
@@ -10,8 +10,6 @@ using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using Image = Avalonia.Controls.Image;
-// ReSharper disable InconsistentNaming
-
namespace CRD.ViewModels;
public class SettingsPageViewModel : ViewModelBase{
diff --git a/CRD/ViewModels/UpdateViewModel.cs b/CRD/ViewModels/UpdateViewModel.cs
index d2cfc25..cfb5d27 100644
--- a/CRD/ViewModels/UpdateViewModel.cs
+++ b/CRD/ViewModels/UpdateViewModel.cs
@@ -1,21 +1,30 @@
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Reflection;
+using System.Text;
using System.Text.RegularExpressions;
using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Media;
-using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
+using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
using Markdig;
+using Markdig.Syntax;
+using Markdig.Syntax.Inlines;
+using Inline = Markdig.Syntax.Inlines.Inline;
namespace CRD.ViewModels;
public partial class UpdateViewModel : ViewModelBase{
-
[ObservableProperty]
private bool _updateAvailable;
@@ -27,24 +36,22 @@ public partial class UpdateViewModel : ViewModelBase{
[ObservableProperty]
private bool _failed;
-
- private AccountPageViewModel accountPageViewModel;
- [ObservableProperty]
- private string _changelogText = "No changelog found.
";
+ private AccountPageViewModel accountPageViewModel;
[ObservableProperty]
private string _currentVersion;
+ public ObservableCollection ChangelogBlocks{ get; } = new();
+
public UpdateViewModel(){
-
var version = Assembly.GetExecutingAssembly().GetName().Version;
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
-
+
LoadChangelog();
-
+
UpdateAvailable = ProgramManager.Instance.UpdateAvailable;
-
+
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
}
@@ -52,7 +59,6 @@ public partial class UpdateViewModel : ViewModelBase{
public void StartUpdate(){
Updating = true;
ProgramManager.Instance.NavigationLock = true;
- // Title = "Updating";
_ = Updater.Instance.DownloadAndUpdateAsync();
}
@@ -65,11 +71,20 @@ public partial class UpdateViewModel : ViewModelBase{
}
}
- private void LoadChangelog(){
+ #region Changelog Builder
+
+ private int textSize = 16;
+
+ public void LoadChangelog(){
string changelogPath = "CHANGELOG.md";
if (!File.Exists(changelogPath)){
- ChangelogText = "No changelog found.
";
+ ChangelogBlocks.Clear();
+ ChangelogBlocks.Add(new TextBlock{
+ Text = "No changelog found",
+ FontSize = 16,
+ TextWrapping = TextWrapping.Wrap
+ });
return;
}
@@ -77,104 +92,206 @@ public partial class UpdateViewModel : ViewModelBase{
markdownText = PreprocessMarkdown(markdownText);
- var pipeline = new MarkdownPipelineBuilder()
- .UseAdvancedExtensions()
- .UseSoftlineBreakAsHardlineBreak()
- .Build();
+ var pipeline = new MarkdownPipelineBuilder().Build();
+ var document = Markdown.Parse(markdownText, pipeline);
- string htmlContent = Markdown.ToHtml(markdownText, pipeline);
+ ChangelogBlocks.Clear();
- htmlContent = MakeIssueLinksClickable(htmlContent);
- htmlContent = ModifyImages(htmlContent);
-
- Color themeTextColor = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark ? Colors.White : Color.Parse("#E4000000");
- string cssColor = $"#{themeTextColor.R:X2}{themeTextColor.G:X2}{themeTextColor.B:X2}";
+ try{
+ foreach (var block in document){
+ switch (block){
+ case HeadingBlock heading:
+ string headingText = string.Concat(heading.Inline.Select(i => i.ToString()));
+ ChangelogBlocks.Add(new TextBlock{
+ Text = headingText,
+ FontSize = heading.Level switch{ 1 => textSize + 10, 2 => textSize + 6, _ => textSize + 4 },
+ FontWeight = FontWeight.Bold,
+ Margin = new Thickness(0, 20, 0, 5),
+ TextWrapping = TextWrapping.Wrap
+ });
+ break;
- string styledHtml = $@"
-
-
-
-
-
- {htmlContent}
-
- ";
+ case ParagraphBlock paragraph:
+ var inlineControls = BuildInlineControls(paragraph.Inline?.FirstChild);
+ var container = new WrapPanel{
+ Margin = new Thickness(0, 5, 0, 5)
+ };
+ foreach (var ctrl in inlineControls)
+ container.Children.Add(ctrl);
- ChangelogText = styledHtml;
+ ChangelogBlocks.Add(container);
+ break;
+
+ case ListBlock list:
+ foreach (ListItemBlock item in list){
+ foreach (var blocki in item){
+ if (blocki is ParagraphBlock para){
+ var container1 = new WrapPanel{ Margin = new Thickness(10, 2, 0, 2) };
+ container1.Children.Add(new TextBlock{ Text = "• ", FontWeight = FontWeight.Bold, FontSize = textSize});
+
+ foreach (var ctrl in BuildInlineControls(para.Inline?.FirstChild))
+ container1.Children.Add(ctrl);
+
+ ChangelogBlocks.Add(container1);
+ }
+ }
+ }
+
+ break;
+ }
+ }
+ } catch (Exception e){
+ Console.Error.WriteLine(e);
+ }
}
- private string MakeIssueLinksClickable(string htmlContent){
- // Match GitHub issue links
- string issuePattern = @"]*>[^<]+<\/a>";
+ IEnumerable BuildInlineControls(Inline? inline){
+ var controls = new List();
+ var urlRegex = new Regex(@"https?://[^\s]+", RegexOptions.Compiled);
+ var githubRefRegex = new Regex(
+ @"https://github\.com/[^/]+/[^/]+/(issues|discussions)/(\d+)",
+ RegexOptions.Compiled);
- // Match GitHub discussion links
- string discussionPattern = @"]*>[^<]+<\/a>";
+ while (inline != null){
+ switch (inline){
+ case LiteralInline lit:
+ var text = lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length);
- htmlContent = Regex.Replace(htmlContent, issuePattern, match => {
- string fullUrl = match.Groups[1].Value;
- string issueNumber = match.Groups[2].Value;
- return $"#{issueNumber}";
- });
+ var lastIndex = 0;
+ foreach (Match match in urlRegex.Matches(text)){
+ if (match.Index > lastIndex){
+ controls.Add(new TextBlock{
+ Text = text.Substring(lastIndex, match.Index - lastIndex),
+ TextWrapping = TextWrapping.Wrap,
+ FontSize = textSize
+ });
+ }
- htmlContent = Regex.Replace(htmlContent, discussionPattern, match => {
- string fullUrl = match.Groups[1].Value;
- string discussionNumber = match.Groups[2].Value;
- return $"#{discussionNumber}";
- });
+ string url = match.Value;
+ string buttonText = url;
- return htmlContent;
+ var ghMatch = githubRefRegex.Match(url);
+ if (ghMatch.Success && ghMatch.Groups.Count > 2){
+ buttonText = $"#{ghMatch.Groups[2].Value}";
+ }
+
+ controls.Add(CreateLinkButton(buttonText, url));
+ lastIndex = match.Index + match.Length;
+ }
+
+ if (lastIndex < text.Length){
+ controls.Add(new TextBlock{
+ Text = text.Substring(lastIndex),
+ TextWrapping = TextWrapping.Wrap,
+ FontSize = textSize
+ });
+ }
+
+ break;
+
+ case EmphasisInline emph:
+ var emphControls = BuildInlineControls(emph.FirstChild);
+ foreach (var ec in emphControls){
+ if (ec is TextBlock tb){
+ tb.FontWeight = emph.DelimiterChar == '*' ? FontWeight.Bold : FontWeight.Normal;
+ tb.FontStyle = emph.DelimiterChar == '_' ? FontStyle.Italic : FontStyle.Normal;
+ }
+
+ controls.Add(ec);
+ }
+
+ break;
+
+ case LinkInline link:
+ var linkText = ConvertInlinesToText(link.FirstChild);
+ controls.Add(CreateLinkButton(linkText, link.Url));
+ break;
+ }
+
+ inline = inline.NextSibling;
+ }
+
+ return controls;
}
- private string ModifyImages(string htmlContent){
- // Regex to match
tags
- string imgPattern = @"
";
+ string ConvertInlinesToText(Inline? inline){
+ var result = new StringBuilder();
- return Regex.Replace(htmlContent, imgPattern, match => {
- string imgUrl = match.Groups[1].Value;
- string altText = "View Image"; // match.Groups[3].Success ? match.Groups[3].Value : "View Image";
+ while (inline != null){
+ switch (inline){
+ case LiteralInline lit:
+ result.Append(lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length));
+ break;
- return $"{altText}";
- });
+ case EmphasisInline emph:
+ result.Append(ConvertInlinesToText(emph.FirstChild));
+ break;
+
+ case LinkInline link:
+ var linkText = ConvertInlinesToText(link.FirstChild);
+ result.Append($"{linkText} ({link.Url})");
+ break;
+
+ case LineBreakInline:
+ result.Append('\n');
+ break;
+
+ default:
+ if (inline is ContainerInline{ FirstChild: not null } container){
+ result.Append(ConvertInlinesToText(container.FirstChild));
+ }
+
+ break;
+ }
+
+ inline = inline.NextSibling;
+ }
+
+ return result.ToString();
}
+ Brush GetLinkBrush(){
+ try{
+ var color = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor ?? Brushes.LightBlue.Color.ToString());
+ return new SolidColorBrush(color);
+ } catch{
+ return new SolidColorBrush(Brushes.LightBlue.Color);
+ }
+ }
+
+ Button CreateLinkButton(string text, string url){
+ var button = new Button{
+ Content = new TextBlock{
+ Text = text,
+ FontSize = textSize,
+ Foreground = GetLinkBrush(),
+ TextDecorations = TextDecorations.Underline
+ },
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
+ Padding = new Thickness(0),
+ Cursor = new Cursor(StandardCursorType.Hand)
+ };
+
+ button.Click += (_, __) => {
+ try{
+ using var p = new Process();
+ p.StartInfo = new ProcessStartInfo{
+ FileName = url,
+ UseShellExecute = true
+ };
+ p.Start();
+ } catch{
+ Console.Error.WriteLine($"Failed to open link: {url}");
+ }
+ };
+
+ return button;
+ }
+
private string PreprocessMarkdown(string markdownText){
- // Regex to match blocks containing an image
string detailsPattern = @"\s*.*?<\/summary>\s*
\s*<\/details>";
return Regex.Replace(markdownText, detailsPattern, match => {
@@ -184,5 +301,6 @@ public partial class UpdateViewModel : ViewModelBase{
return $"";
});
}
-
+
+ #endregion
}
\ No newline at end of file
diff --git a/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs b/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs
index 2d126b9..757bd2f 100644
--- a/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs
+++ b/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs
@@ -5,7 +5,6 @@ using System.Linq;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.Structs;
diff --git a/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs b/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs
new file mode 100644
index 0000000..c32833b
--- /dev/null
+++ b/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CRD.Downloader.Crunchyroll;
+using CRD.Utils;
+using CRD.Utils.Sonarr;
+using CRD.Utils.Sonarr.Models;
+using CRD.Utils.Structs.History;
+using CRD.Utils.UI;
+using DynamicData;
+using FluentAvalonia.UI.Controls;
+
+namespace CRD.ViewModels.Utils;
+
+public partial class ContentDialogSonarrMatchEpisodeViewModel : ViewModelBase{
+ private readonly CustomContentDialog dialog;
+
+ [ObservableProperty]
+ private SonarrEpisode _currentSonarrEpisode;
+
+ [ObservableProperty]
+ private HistoryEpisode _currentHistoryEpisode;
+
+ [ObservableProperty]
+ private SonarrEpisode _selectedItem;
+
+ [ObservableProperty]
+ private ObservableCollection _sonarrSeasonList = new();
+
+ [ObservableProperty]
+ private SonarrSeries _sonarrSeries;
+
+ public ContentDialogSonarrMatchEpisodeViewModel(CustomContentDialog contentDialog, HistorySeries selectedSeries, HistoryEpisode episode){
+ ArgumentNullException.ThrowIfNull(contentDialog);
+
+ dialog = contentDialog;
+ dialog.Closed += DialogOnClosed;
+ dialog.PrimaryButtonClick += SaveButton;
+
+ CurrentHistoryEpisode = episode;
+
+ SonarrSeries = SonarrClient.Instance.SonarrSeries.Find(e => e.Id.ToString() == selectedSeries.SonarrSeriesId) ?? new SonarrSeries(){ Title = "No series matched" };
+ SetImageUrl(SonarrSeries);
+
+ _ = LoadList(selectedSeries.SonarrSeriesId);
+ }
+
+ private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
+ dialog.PrimaryButtonClick -= SaveButton;
+ }
+
+ private async Task LoadList(string? sonarrSeriesId){
+ if (string.IsNullOrEmpty(sonarrSeriesId)){
+ return;
+ }
+
+ var list = await PopulateSeriesList(sonarrSeriesId);
+ SonarrSeasonList.AddRange(list);
+ }
+
+ private async Task> PopulateSeriesList(string sonarrSeriesId){
+ List episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(sonarrSeriesId));
+
+ var seasonsDict = new Dictionary();
+
+ foreach (var episode in episodes){
+ if (CurrentHistoryEpisode.SonarrEpisodeId == episode.Id.ToString()){
+ CurrentSonarrEpisode = episode;
+ }
+
+ int seasonNumber = episode.SeasonNumber;
+ if (!seasonsDict.TryGetValue(seasonNumber, out var season)){
+ season = new SeasonsItem{
+ SeasonName = $"Season {seasonNumber}",
+ Episodes = new List()
+ };
+ seasonsDict[seasonNumber] = season;
+ }
+
+ season.Episodes.Add(episode);
+ }
+
+ var seasons = seasonsDict.Values
+ .OrderBy(s => int.TryParse(System.Text.RegularExpressions.Regex.Match(s.SeasonName, @"\d+").Value, out int n) ? n : int.MaxValue)
+ .ToList();
+
+ foreach (var season in seasons){
+ season.Episodes.Sort((a, b) => a.EpisodeNumber.CompareTo(b.EpisodeNumber));
+ }
+
+ return seasons;
+ }
+
+ private async void SetImageUrl(SonarrSeries sonarrSeries){
+ var properties = CrunchyrollManager.Instance.CrunOptions.SonarrProperties;
+ if (properties == null || sonarrSeries.Images == null){
+ return;
+ }
+
+ var baseUrl = "";
+ baseUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}{(properties.UrlBase ?? "")}";
+
+ sonarrSeries.ImageUrl = baseUrl + sonarrSeries.Images.Find(e => e.CoverType == SonarrCoverType.FanArt)?.Url;
+
+ var image = await Helpers.LoadImage(sonarrSeries.ImageUrl);
+
+ if (image == null) return;
+ dialog.BackgroundImage = image;
+ dialog.BackgroundImageOpacity = CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity;
+ dialog.BackgroundImageBlurRadius = CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius;
+ }
+
+ [RelayCommand]
+ public void SetSonarrEpisodeMatch(SonarrEpisode episode){
+ CurrentSonarrEpisode = episode;
+ }
+
+ private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
+ dialog.Closed -= DialogOnClosed;
+ }
+}
+
+public class SeasonsItem(){
+ public string SeasonName{ get; set; }
+
+ public string IsExpanded{ get; set; }
+ public List Episodes{ get; set; }
+}
\ No newline at end of file
diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml
index b003722..e083e80 100644
--- a/CRD/Views/HistoryPageView.axaml
+++ b/CRD/Views/HistoryPageView.axaml
@@ -9,7 +9,9 @@
xmlns:structs="clr-namespace:CRD.Utils.Structs"
x:DataType="vm:HistoryPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="CRD.Views.HistoryPageView">
+ x:Class="CRD.Views.HistoryPageView"
+ Unloaded="OnUnloaded"
+ Loaded="Control_OnLoaded">
@@ -235,7 +237,7 @@
-
diff --git a/CRD/Views/HistoryPageView.axaml.cs b/CRD/Views/HistoryPageView.axaml.cs
index 1859b0a..6cd7466 100644
--- a/CRD/Views/HistoryPageView.axaml.cs
+++ b/CRD/Views/HistoryPageView.axaml.cs
@@ -1,11 +1,26 @@
-using Avalonia.Controls;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using CRD.ViewModels;
namespace CRD.Views;
public partial class HistoryPageView : UserControl{
-
public HistoryPageView(){
InitializeComponent();
}
+
+ private void OnUnloaded(object? sender, RoutedEventArgs e){
+
+ if (DataContext is HistoryPageViewModel viewModel){
+ viewModel.LastScrollOffset = SeriesListBox.Scroll?.Offset ?? Vector.Zero;
+ }
+
+ }
+ private void Control_OnLoaded(object? sender, RoutedEventArgs e){
+ if (DataContext is HistoryPageViewModel viewModel){
+ if (SeriesListBox.Scroll != null) SeriesListBox.Scroll.Offset = viewModel.LastScrollOffset;
+ }
+ }
}
\ No newline at end of file
diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml
index bb1f712..444fdc3 100644
--- a/CRD/Views/SeriesPageView.axaml
+++ b/CRD/Views/SeriesPageView.axaml
@@ -152,7 +152,8 @@
+ IsChecked="{Binding SelectedSeries.IsInactive}"
+ Command="{Binding ToggleInactive}">
-
+
+
+
+
@@ -366,20 +388,21 @@
-
-
+
-
-
-
+
+
+
+
+
+
-
+
+
+
-
+
diff --git a/CRD/Views/SettingsPageView.axaml.cs b/CRD/Views/SettingsPageView.axaml.cs
index 86abc81..ee39889 100644
--- a/CRD/Views/SettingsPageView.axaml.cs
+++ b/CRD/Views/SettingsPageView.axaml.cs
@@ -17,19 +17,5 @@ public partial class SettingsPageView : UserControl{
SonarrClient.Instance.RefreshSonarr();
}
}
-
- private void ListBox_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){
- var listBox = sender as ListBox;
- var scrollViewer = listBox?.GetVisualDescendants().OfType().FirstOrDefault();
-
- if (scrollViewer != null){
- // Determine if the ListBox is at its bounds (top or bottom)
- bool atTop = scrollViewer.Offset.Y <= 0 && e.Delta.Y > 0;
- bool atBottom = scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height && e.Delta.Y < 0;
-
- if (atTop || atBottom){
- e.Handled = true; // Stop the event from propagating to the parent
- }
- }
- }
+
}
\ No newline at end of file
diff --git a/CRD/Views/UpdateView.axaml b/CRD/Views/UpdateView.axaml
index c546fc0..9c52a0d 100644
--- a/CRD/Views/UpdateView.axaml
+++ b/CRD/Views/UpdateView.axaml
@@ -3,12 +3,12 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels"
- xmlns:avalonia="clr-namespace:TheArtOfDev.HtmlRenderer.Avalonia;assembly=Avalonia.HtmlRenderer"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:DataType="vm:UpdateViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.UpdateView">
+
@@ -59,25 +59,37 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml b/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml
new file mode 100644
index 0000000..1e431db
--- /dev/null
+++ b/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs b/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs
new file mode 100644
index 0000000..3d14656
--- /dev/null
+++ b/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.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 ContentDialogSonarrMatchEpisodeView : UserControl{
+ public ContentDialogSonarrMatchEpisodeView(){
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml
index 629aabe..1befcd1 100644
--- a/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml
+++ b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml
@@ -6,8 +6,8 @@
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
x:DataType="vm:ContentDialogSonarrMatchViewModel"
x:Class="CRD.Views.Utils.ContentDialogSonarrMatchView">
-
-
+
+
@@ -18,7 +18,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -37,62 +37,64 @@
+
-
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+