mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-03-11 17:45:39 +00:00
Add - Added **retry** for license key requests
Add - Added **Sonarr season/episode numbers** to the history view Add - Added option to **match episodes to Sonarr episodes** to correct mismatches Add - Added **button to rematch all Sonarr episodes** Add - Added an **optional secondary endpoint** Chg - Changed **changelog heading sizes** Chg - Changed Sonarr matching to only process **new or unmatched episodes** Chg - Changed logic to **request audio license keys only when needed** Fix - Fixed **Sonarr series manual matching dialog**
This commit is contained in:
parent
ae0f936ff5
commit
5b33d2336c
34 changed files with 1281 additions and 748 deletions
|
|
@ -20,6 +20,7 @@
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<sty:FluentAvaloniaTheme PreferSystemTheme="True" PreferUserAccentColor="True"/>
|
<sty:FluentAvaloniaTheme PreferSystemTheme="True" PreferUserAccentColor="True"/>
|
||||||
<StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" />
|
<StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" />
|
||||||
|
<StyleInclude Source="avares://CRD/Styling/ContentDialogCustomStyle.axaml" />
|
||||||
<StyleInclude Source="avares://CRD/Assets/Icons.axaml"></StyleInclude>
|
<StyleInclude Source="avares://CRD/Assets/Icons.axaml"></StyleInclude>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
@ -65,8 +65,8 @@ public class CalendarManager{
|
||||||
return forDate;
|
return forDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "de")
|
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us")
|
||||||
? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "de"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null)
|
? 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);
|
: HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,64 +147,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
options.History = true;
|
options.History = true;
|
||||||
|
|
||||||
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
|
CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
|
||||||
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<string>{ "en-US" };
|
|
||||||
optionsYaml.SkipMuxing = false;
|
|
||||||
optionsYaml.MkvmergeOptions = new List<string>{ "--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<string>(){ "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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
@ -226,11 +169,6 @@ public class CrunchyrollManager{
|
||||||
PreferredContentSubtitleLanguage = DefaultLocale,
|
PreferredContentSubtitleLanguage = DefaultLocale,
|
||||||
HasPremium = false,
|
HasPremium = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
|
|
||||||
CfgManager.WriteCrSettings();
|
|
||||||
Helpers.DeleteFile(CfgManager.PathCrDownloadOptionsOld);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string> GetBase64EncodedTokenAsync(){
|
public static async Task<string> GetBase64EncodedTokenAsync(){
|
||||||
|
|
@ -290,9 +228,6 @@ public class CrunchyrollManager{
|
||||||
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
|
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
|
||||||
Token = CfgManager.ReadJsonFromFile<CrToken>(CfgManager.PathCrToken);
|
Token = CfgManager.ReadJsonFromFile<CrToken>(CfgManager.PathCrToken);
|
||||||
await CrAuth.LoginWithToken();
|
await CrAuth.LoginWithToken();
|
||||||
if (Path.Exists(CfgManager.PathCrTokenOld)){
|
|
||||||
Helpers.DeleteFile(CfgManager.PathCrTokenOld);
|
|
||||||
}
|
|
||||||
} else{
|
} else{
|
||||||
await CrAuth.AuthAnonymous();
|
await CrAuth.AuthAnonymous();
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +326,6 @@ public class CrunchyrollManager{
|
||||||
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
||||||
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
||||||
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
|
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
|
||||||
|
|
||||||
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
||||||
var mergers = new List<Merger>();
|
var mergers = new List<Merger>();
|
||||||
foreach (var keyValue in groupByDub){
|
foreach (var keyValue in groupByDub){
|
||||||
|
|
@ -999,9 +933,12 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
var fetchPlaybackData = await FetchPlaybackData(options.StreamEndpoint ?? "web/firefox", mediaId, mediaGuid, data.Music);
|
||||||
var fetchPlaybackData = await FetchPlaybackData(options, 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){
|
if (!fetchPlaybackData.IsOk){
|
||||||
var errorJson = fetchPlaybackData.error;
|
var errorJson = fetchPlaybackData.error;
|
||||||
|
|
@ -1048,6 +985,21 @@ public class CrunchyrollManager{
|
||||||
ErrorText = "Playback data not found"
|
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;
|
var pbData = fetchPlaybackData.pbData;
|
||||||
|
|
@ -1106,7 +1058,7 @@ public class CrunchyrollManager{
|
||||||
streams = streams.Select(s => {
|
streams = streams.Select(s => {
|
||||||
s.AudioLang = audDub;
|
s.AudioLang = audDub;
|
||||||
s.HardsubLang = s.HardsubLang;
|
s.HardsubLang = s.HardsubLang;
|
||||||
s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}";
|
s.Type = $"{s.Format}/{s.AudioLang.CrLocale}/{s.HardsubLang.CrLocale}";
|
||||||
return s;
|
return s;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
|
@ -1209,35 +1161,76 @@ public class CrunchyrollManager{
|
||||||
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
|
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
|
||||||
|
|
||||||
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
|
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<string, string> 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<DownloadedMedia>(),
|
||||||
|
Error = dlFailed,
|
||||||
|
FileName = "./unknown",
|
||||||
|
ErrorText = "Playlist fetch problem"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!streamPlaylistsReqResponse.IsOk){
|
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
||||||
dlFailed = true;
|
streamPlaylistsReqResponseList[streamUrl ?? ""] = streamPlaylistsReqResponse.ResponseContent;
|
||||||
return new DownloadResponse{
|
}
|
||||||
Data = new List<DownloadedMedia>(),
|
|
||||||
Error = dlFailed,
|
|
||||||
FileName = "./unknown",
|
|
||||||
ErrorText = "Playlist fetch problem"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//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<DownloadedMedia>(),
|
||||||
|
// Error = dlFailed,
|
||||||
|
// FileName = "./unknown",
|
||||||
|
// ErrorText = "Playlist fetch problem"
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
if (dlFailed){
|
if (dlFailed){
|
||||||
Console.WriteLine($"CAN\'T FETCH VIDEO PLAYLISTS!");
|
Console.WriteLine($"CAN\'T FETCH VIDEO PLAYLISTS!");
|
||||||
} else{
|
} else{
|
||||||
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
// if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
||||||
var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
|
// var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
|
||||||
var matchedUrl = match.Success ? match.Value : null;
|
// var matchedUrl = match.Success ? match.Value : null;
|
||||||
//Parse MPD Playlists
|
// //Parse MPD Playlists
|
||||||
var crLocal = "";
|
// var crLocal = "";
|
||||||
if (pbData.Meta != null){
|
// if (pbData.Meta != null){
|
||||||
crLocal = pbData.Meta.AudioLocale.CrLocale;
|
// crLocal = pbData.Meta.AudioLocale.CrLocale;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl);
|
||||||
|
//
|
||||||
|
// List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
|
||||||
|
if (streamPlaylistsReqResponseList.Count > 0){
|
||||||
|
HashSet<string> streamServers =[];
|
||||||
|
Dictionary<string, ServerData> playListData = new Dictionary<string, ServerData>();
|
||||||
|
|
||||||
|
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<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
|
|
||||||
options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
|
options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
|
||||||
|
|
||||||
if (streamServers.Count == 0){
|
if (streamServers.Count == 0){
|
||||||
|
|
@ -1253,8 +1246,11 @@ public class CrunchyrollManager{
|
||||||
options.StreamServer = 1;
|
options.StreamServer = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
string selectedServer = streamServers[options.StreamServer - 1];
|
// string selectedServer = streamServers[options.StreamServer - 1];
|
||||||
ServerData selectedList = streamPlaylists.Data[selectedServer];
|
// ServerData selectedList = streamPlaylists.Data[selectedServer];
|
||||||
|
|
||||||
|
string selectedServer = streamServers.ToList()[options.StreamServer - 1];
|
||||||
|
ServerData selectedList = playListData[selectedServer];
|
||||||
|
|
||||||
var videos = selectedList.video.Select(item => new VideoItem{
|
var videos = selectedList.video.Select(item => new VideoItem{
|
||||||
segments = item.segments,
|
segments = item.segments,
|
||||||
|
|
@ -1273,8 +1269,20 @@ public class CrunchyrollManager{
|
||||||
resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s"
|
resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s"
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width));
|
// Video: Remove duplicates by resolution (width, height), keep highest bandwidth, then sort
|
||||||
audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
|
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)){
|
if (string.IsNullOrEmpty(data.VideoQuality)){
|
||||||
Console.Error.WriteLine("Warning: VideoQuality is null or empty. Defaulting to 'best' quality.");
|
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<string, string> authDataDict = new Dictionary<string, string>
|
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
||||||
{ { "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);
|
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
||||||
|
|
||||||
if (encryptionKeys.Count == 0){
|
if (encryptionKeys.Count == 0){
|
||||||
|
|
@ -1494,25 +1504,46 @@ public class CrunchyrollManager{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
|
List<ContentKey> encryptionKeysAudio =[];
|
||||||
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
|
if (!string.IsNullOrEmpty(chosenVideoSegments.pssh) && !chosenVideoSegments.pssh.Equals(chosenAudioSegments.pssh)){
|
||||||
var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
|
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
|
if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
|
||||||
var commandBase = $"--show-progress --key {keyId}:{key}";
|
|
||||||
var tempTsFileName = Path.GetFileName(tempTsFile);
|
var tempTsFileName = Path.GetFileName(tempTsFile);
|
||||||
var tempTsFileWorkDir = Path.GetDirectoryName(tempTsFile) ?? CfgManager.PathVIDEOS_DIR;
|
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);
|
bool shaka = Path.Exists(CfgManager.PathShakaPackager);
|
||||||
if (shaka){
|
if (shaka){
|
||||||
commandBase = " --enable_raw_key_decryption " +
|
// === shaka-packager command ===
|
||||||
string.Join(" ",
|
var shakaVideoKeys = BuildShakaKeysParam(encryptionKeys);
|
||||||
encryptionKeys.Select(kb =>
|
commandVideo = $"input=\"{tempTsFileName}.video.enc.m4s\",stream=video,output=\"{tempTsFileName}.video.m4s\" {shakaVideoKeys}";
|
||||||
$"--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;
|
var shakaAudioKeys = BuildShakaKeysParam(audioKeysToUse);
|
||||||
commandAudio = $"input=\"{tempTsFileName}.audio.enc.m4s\",stream=audio,output=\"{tempTsFileName}.audio.m4s\"" + commandBase;
|
commandAudio = $"input=\"{tempTsFileName}.audio.enc.m4s\",stream=audio,output=\"{tempTsFileName}.audio.m4s\" {shakaAudioKeys}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoDownloaded){
|
if (videoDownloaded){
|
||||||
|
|
@ -2085,13 +2116,13 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#region Fetch Playback Data
|
#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{
|
var temppbData = new PlaybackData{
|
||||||
Total = 0,
|
Total = 0,
|
||||||
Data = new Dictionary<string, StreamDetails>()
|
Data = new Dictionary<string, StreamDetails>()
|
||||||
};
|
};
|
||||||
|
|
||||||
var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
|
var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{streamEndpoint}/play";
|
||||||
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
|
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
|
||||||
|
|
||||||
if (!playbackRequestResponse.IsOk){
|
if (!playbackRequestResponse.IsOk){
|
||||||
|
|
@ -2119,12 +2150,12 @@ public class CrunchyrollManager{
|
||||||
return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent);
|
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);
|
var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, false, null);
|
||||||
return await HttpClientReq.Instance.SendHttpRequest(request);
|
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;
|
if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response;
|
||||||
|
|
||||||
var error = StreamError.FromJson(response.ResponseContent);
|
var error = StreamError.FromJson(response.ResponseContent);
|
||||||
|
|
@ -2158,7 +2189,7 @@ public class CrunchyrollManager{
|
||||||
foreach (var hardsub in playStream.HardSubs){
|
foreach (var hardsub in playStream.HardSubs){
|
||||||
var stream = hardsub.Value;
|
var stream = hardsub.Value;
|
||||||
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
||||||
Url = stream.Url,
|
Url = [stream.Url],
|
||||||
IsHardsubbed = true,
|
IsHardsubbed = true,
|
||||||
HardsubLocale = stream.Hlang,
|
HardsubLocale = stream.Hlang,
|
||||||
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
||||||
|
|
@ -2167,7 +2198,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
||||||
Url = playStream.Url,
|
Url = [playStream.Url],
|
||||||
IsHardsubbed = false,
|
IsHardsubbed = false,
|
||||||
HardsubLocale = Locale.DefaulT,
|
HardsubLocale = Locale.DefaulT,
|
||||||
HardsubLang = Languages.DEFAULT_lang
|
HardsubLang = Languages.DEFAULT_lang
|
||||||
|
|
@ -2322,4 +2353,14 @@ public class CrunchyrollManager{
|
||||||
Console.Error.WriteLine("Chapter request failed");
|
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<ContentKey> keys) =>
|
||||||
|
"--enable_raw_key_decryption " + string.Join(" ",
|
||||||
|
keys.Select(k => $"--keys key_id={FormatKey(k.KeyID)}:key={FormatKey(k.Bytes)}"));
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +126,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedStreamEndpoint;
|
private ComboBoxItem _selectedStreamEndpoint;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ComboBoxItem _selectedStreamEndpointSecondary;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedDefaultDubLang;
|
private ComboBoxItem _selectedDefaultDubLang;
|
||||||
|
|
@ -202,13 +205,28 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
new(){ Content = "console/ps5" },
|
new(){ Content = "console/ps5" },
|
||||||
new(){ Content = "console/xbox_one" },
|
new(){ Content = "console/xbox_one" },
|
||||||
new(){ Content = "web/edge" },
|
new(){ Content = "web/edge" },
|
||||||
// new (){ Content = "web/safari" },
|
|
||||||
new(){ Content = "web/chrome" },
|
new(){ Content = "web/chrome" },
|
||||||
new(){ Content = "web/fallback" },
|
new(){ Content = "web/fallback" },
|
||||||
// new (){ Content = "ios/iphone" },
|
|
||||||
// new (){ Content = "ios/ipad" },
|
|
||||||
new(){ Content = "android/phone" },
|
new(){ Content = "android/phone" },
|
||||||
new(){ Content = "tv/samsung" }
|
new(){ Content = "android/tablet" },
|
||||||
|
new(){ Content = "tv/samsung" },
|
||||||
|
new(){ Content = "tv/vidaa" }
|
||||||
|
];
|
||||||
|
|
||||||
|
public ObservableCollection<ComboBoxItem> 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<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
|
public ObservableCollection<StringItemWithDisplayName> 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;
|
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
|
||||||
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
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());
|
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||||
|
|
||||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
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.StreamEndpoint = SelectedStreamEndpoint.Content + "";
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondary = SelectedStreamEndpointSecondary.Content + "";
|
||||||
|
|
||||||
List<string> dubLangs = new List<string>();
|
List<string> dubLangs = new List<string>();
|
||||||
foreach (var listBoxItem in SelectedDubLang){
|
foreach (var listBoxItem in SelectedDubLang){
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,16 @@
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Stream Endpoint Secondary">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||||
|
ItemsSource="{Binding StreamEndpointsSecondary}"
|
||||||
|
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
|
||||||
|
</ComboBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="Video">
|
<controls:SettingsExpanderItem Content="Video">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
|
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
|
||||||
|
|
|
||||||
|
|
@ -614,7 +614,7 @@ public class History{
|
||||||
|
|
||||||
private static readonly object _lock = new object();
|
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 }){
|
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -630,22 +630,38 @@ public class History{
|
||||||
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
|
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<string>(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!));
|
||||||
|
|
||||||
|
episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString()));
|
||||||
|
|
||||||
|
allHistoryEpisodes = allHistoryEpisodes
|
||||||
|
.Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
List<HistoryEpisode> failedEpisodes =[];
|
List<HistoryEpisode> failedEpisodes =[];
|
||||||
|
|
||||||
Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
|
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
|
// Create a copy of the episodes list for each thread
|
||||||
var episodesCopy = new List<SonarrEpisode>(episodes);
|
var episodesCopy = new List<SonarrEpisode>(episodes);
|
||||||
|
|
||||||
var episode = FindClosestMatchEpisodes(episodesCopy, historyEpisode.EpisodeTitle ?? string.Empty);
|
var episode = FindClosestMatchEpisodes(episodesCopy, historyEpisode.EpisodeTitle ?? string.Empty);
|
||||||
if (episode != null){
|
if (episode != null){
|
||||||
historyEpisode.SonarrEpisodeId = episode.Id + "";
|
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||||
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrHasFile = episode.HasFile;
|
|
||||||
historyEpisode.SonarrIsMonitored = episode.Monitored;
|
|
||||||
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
|
|
||||||
|
|
||||||
lock (_lock){
|
lock (_lock){
|
||||||
episodes.Remove(episode);
|
episodes.Remove(episode);
|
||||||
}
|
}
|
||||||
|
|
@ -669,12 +685,8 @@ public class History{
|
||||||
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
|
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
|
||||||
});
|
});
|
||||||
if (episode != null){
|
if (episode != null){
|
||||||
historyEpisode.SonarrEpisodeId = episode.Id + "";
|
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||||
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrHasFile = episode.HasFile;
|
|
||||||
historyEpisode.SonarrIsMonitored = episode.Monitored;
|
|
||||||
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
|
|
||||||
lock (_lock){
|
lock (_lock){
|
||||||
episodes.Remove(episode);
|
episodes.Remove(episode);
|
||||||
}
|
}
|
||||||
|
|
@ -688,12 +700,8 @@ public class History{
|
||||||
});
|
});
|
||||||
|
|
||||||
if (episode1 != null){
|
if (episode1 != null){
|
||||||
historyEpisode.SonarrEpisodeId = episode1.Id + "";
|
historyEpisode.AssignSonarrEpisodeData(episode1);
|
||||||
historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrHasFile = episode1.HasFile;
|
|
||||||
historyEpisode.SonarrIsMonitored = episode1.Monitored;
|
|
||||||
historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + "";
|
|
||||||
lock (_lock){
|
lock (_lock){
|
||||||
episodes.Remove(episode1);
|
episodes.Remove(episode1);
|
||||||
}
|
}
|
||||||
|
|
@ -706,12 +714,8 @@ public class History{
|
||||||
return ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode;
|
return ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode;
|
||||||
});
|
});
|
||||||
if (episode2 != null){
|
if (episode2 != null){
|
||||||
historyEpisode.SonarrEpisodeId = episode2.Id + "";
|
historyEpisode.AssignSonarrEpisodeData(episode2);
|
||||||
historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrHasFile = episode2.HasFile;
|
|
||||||
historyEpisode.SonarrIsMonitored = episode2.Monitored;
|
|
||||||
historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + "";
|
|
||||||
historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
|
|
||||||
lock (_lock){
|
lock (_lock){
|
||||||
episodes.Remove(episode2);
|
episodes.Remove(episode2);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
CRD/Styling/ContentDialogCustomStyle.axaml
Normal file
202
CRD/Styling/ContentDialogCustomStyle.axaml
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:ui="clr-namespace:CRD.Utils.UI">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="20">
|
||||||
|
<!-- Add Controls for Previewer Here -->
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<Style Selector="ui|CustomContentDialog">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ContentDialogForeground}" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ContentDialogBorderBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="{DynamicResource ContentDialogBorderWidth}" />
|
||||||
|
<Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
|
||||||
|
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Border Name="Container">
|
||||||
|
<Panel Name="LayoutRoot" Background="{DynamicResource ContentDialogSmokeFill}">
|
||||||
|
<Border Name="BackgroundElement"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderThickness="{StaticResource ContentDialogBorderWidth}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
CornerRadius="{TemplateBinding CornerRadius}"
|
||||||
|
MinWidth="{DynamicResource ContentDialogMinWidth}"
|
||||||
|
MaxWidth="{DynamicResource ContentDialogMaxWidth}"
|
||||||
|
MinHeight="{DynamicResource ContentDialogMinHeight}"
|
||||||
|
MaxHeight="{DynamicResource ContentDialogMaxHeight}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" Margin="100"
|
||||||
|
Effect="drop-shadow(0 8 32 #66000000)"
|
||||||
|
BackgroundSizing="{TemplateBinding BackgroundSizing}">
|
||||||
|
|
||||||
|
<Border ClipToBounds="True"
|
||||||
|
CornerRadius="{TemplateBinding CornerRadius}">
|
||||||
|
<Grid Name="DialogSpace" ClipToBounds="True"
|
||||||
|
RowDefinitions="*,Auto">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
|
||||||
|
<Rectangle Fill="{TemplateBinding Background}" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" />
|
||||||
|
|
||||||
|
<Image x:Name="BackgroundImageElement"
|
||||||
|
Source="{TemplateBinding BackgroundImage}"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
Opacity="{TemplateBinding BackgroundImageOpacity}"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
|
</Image>
|
||||||
|
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource ContentDialogTopOverlay}"
|
||||||
|
Padding="{DynamicResource ContentDialogPadding}"
|
||||||
|
BorderThickness="{StaticResource ContentDialogSeparatorThickness}"
|
||||||
|
BorderBrush="{DynamicResource ContentDialogSeparatorBorderBrush}">
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<Grid.Styles>
|
||||||
|
<!--Make sure text wrapping is on-->
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Styles>
|
||||||
|
<ContentControl Name="Title"
|
||||||
|
Margin="{StaticResource ContentDialogTitleMargin}"
|
||||||
|
Content="{TemplateBinding Title}"
|
||||||
|
ContentTemplate="{TemplateBinding TitleTemplate}"
|
||||||
|
FontSize="20"
|
||||||
|
FontFamily="Default"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{TemplateBinding Foreground}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<ContentControl.Template>
|
||||||
|
<ControlTemplate>
|
||||||
|
<ContentPresenter Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</ContentControl.Template>
|
||||||
|
</ContentControl>
|
||||||
|
|
||||||
|
<!-- <ScrollViewer Grid.Row="1" Name="ContentScrollViewer" -->
|
||||||
|
<!-- HorizontalScrollBarVisibility="Disabled" -->
|
||||||
|
<!-- VerticalScrollBarVisibility="Auto"> -->
|
||||||
|
|
||||||
|
<ContentPresenter Name="Content"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
FontSize="{StaticResource ControlContentThemeFontSize}"
|
||||||
|
FontFamily="{StaticResource ContentControlThemeFontFamily}"
|
||||||
|
Foreground="{TemplateBinding Foreground}"
|
||||||
|
Grid.Row="1" />
|
||||||
|
|
||||||
|
<!-- </ScrollViewer> -->
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
<Border Padding="{StaticResource ContentDialogPadding}"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Background="{TemplateBinding Background}">
|
||||||
|
<Grid Name="CommandSpace">
|
||||||
|
<!--
|
||||||
|
B/C we can't target Row/Column defs in Styles like WinUI
|
||||||
|
this still uses the old Col defs, but it works the same
|
||||||
|
way in the end...
|
||||||
|
-->
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="0.5*" />
|
||||||
|
<ColumnDefinition Width="0.5*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button Name="PrimaryButton"
|
||||||
|
Content="{TemplateBinding PrimaryButtonText}"
|
||||||
|
IsEnabled="{TemplateBinding IsPrimaryButtonEnabled}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
IsVisible="False" />
|
||||||
|
|
||||||
|
<Button Name="SecondaryButton"
|
||||||
|
Content="{TemplateBinding SecondaryButtonText}"
|
||||||
|
IsEnabled="{TemplateBinding IsSecondaryButtonEnabled}"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
IsVisible="False" />
|
||||||
|
|
||||||
|
<Button Name="CloseButton"
|
||||||
|
Content="{TemplateBinding CloseButtonText}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
IsVisible="False" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Primary Only -->
|
||||||
|
<Style Selector="ui|CustomContentDialog:primary /template/ Button#PrimaryButton">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
<Setter Property="Grid.Column" Value="2" />
|
||||||
|
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Primary + Secondary -->
|
||||||
|
<Style Selector="ui|CustomContentDialog:primary:secondary /template/ Button#PrimaryButton">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
<Setter Property="Grid.Column" Value="0" />
|
||||||
|
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||||
|
<Setter Property="Margin" Value="0 0 4 0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ui|CustomContentDialog:primary:secondary /template/ Button#SecondaryButton">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
<Setter Property="Grid.Column" Value="2" />
|
||||||
|
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||||
|
<Setter Property="Margin" Value="4 0 0 0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Primary + Close -->
|
||||||
|
<Style Selector="ui|CustomContentDialog:primary:close /template/ Button#PrimaryButton">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
<Setter Property="Grid.Column" Value="0" />
|
||||||
|
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||||
|
<Setter Property="Margin" Value="0 0 4 0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ui|CustomContentDialog:primary:close /template/ Button#CloseButton">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
<Setter Property="Grid.Column" Value="2" />
|
||||||
|
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||||
|
<Setter Property="Margin" Value="4 0 0 0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Close Only -->
|
||||||
|
<Style Selector="ui|CustomContentDialog:close /template/ Button#CloseButton">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
<Setter Property="Grid.Column" Value="2" />
|
||||||
|
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</Styles>
|
||||||
|
|
@ -80,12 +80,15 @@ public class Session{
|
||||||
public byte[] GetLicenseRequest(){
|
public byte[] GetLicenseRequest(){
|
||||||
dynamic licenseRequest;
|
dynamic licenseRequest;
|
||||||
|
|
||||||
|
var random = new Random();
|
||||||
|
uint keyControlNonceId = (uint)(random.NextDouble() * Math.Pow(2, 31));
|
||||||
|
|
||||||
if (InitData is WidevineCencHeader){
|
if (InitData is WidevineCencHeader){
|
||||||
licenseRequest = new SignedLicenseRequest{
|
licenseRequest = new SignedLicenseRequest{
|
||||||
Type = SignedLicenseRequest.MessageType.LicenseRequest,
|
Type = SignedLicenseRequest.MessageType.LicenseRequest,
|
||||||
Msg = new LicenseRequest{
|
Msg = new LicenseRequest{
|
||||||
Type = LicenseRequest.RequestType.New,
|
Type = LicenseRequest.RequestType.New,
|
||||||
KeyControlNonce = 1093602366,
|
KeyControlNonce = keyControlNonceId,
|
||||||
ProtocolVersion = ProtocolVersion.Current,
|
ProtocolVersion = ProtocolVersion.Current,
|
||||||
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
|
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
|
||||||
ContentId = new LicenseRequest.ContentIdentification{
|
ContentId = new LicenseRequest.ContentIdentification{
|
||||||
|
|
@ -102,7 +105,7 @@ public class Session{
|
||||||
Type = SignedLicenseRequestRaw.MessageType.LicenseRequest,
|
Type = SignedLicenseRequestRaw.MessageType.LicenseRequest,
|
||||||
Msg = new LicenseRequestRaw{
|
Msg = new LicenseRequestRaw{
|
||||||
Type = LicenseRequestRaw.RequestType.New,
|
Type = LicenseRequestRaw.RequestType.New,
|
||||||
KeyControlNonce = 1093602366,
|
KeyControlNonce = keyControlNonceId,
|
||||||
ProtocolVersion = ProtocolVersion.Current,
|
ProtocolVersion = ProtocolVersion.Current,
|
||||||
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
|
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
|
||||||
ContentId = new LicenseRequestRaw.ContentIdentification{
|
ContentId = new LicenseRequestRaw.ContentIdentification{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.IsolatedStorage;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -111,7 +112,28 @@ public class Widevine{
|
||||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
||||||
playbackRequest2.Content = content;
|
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){
|
if (!response.IsOk){
|
||||||
Console.Error.WriteLine("Failed to get Keys!");
|
Console.Error.WriteLine("Failed to get Keys!");
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ namespace CRD.Utils.Files;
|
||||||
|
|
||||||
public class CfgManager{
|
public class CfgManager{
|
||||||
private static string workingDirectory = AppContext.BaseDirectory;
|
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 PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
|
||||||
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
||||||
|
|
||||||
|
|
@ -182,73 +179,6 @@ public class CfgManager{
|
||||||
return properties;
|
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<CrDownloadOptionsYaml>(new StringReader(input));
|
|
||||||
var instanceOptions = options;
|
|
||||||
|
|
||||||
foreach (PropertyInfo property in typeof(CrDownloadOptionsYaml).GetProperties()){
|
|
||||||
var yamlMemberAttribute = property.GetCustomAttribute<YamlMemberAttribute>();
|
|
||||||
// var jsonMemberAttribute = property.GetCustomAttribute<JsonPropertyAttribute>();
|
|
||||||
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<string> GetTopLevelPropertiesInYaml(string yamlContent){
|
|
||||||
var reader = new StringReader(yamlContent);
|
|
||||||
var yamlStream = new YamlStream();
|
|
||||||
yamlStream.Load(reader);
|
|
||||||
|
|
||||||
var properties = new HashSet<string>();
|
|
||||||
|
|
||||||
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(){
|
public static void UpdateHistoryFile(){
|
||||||
if (!CrunchyrollManager.Instance.CrunOptions.History){
|
if (!CrunchyrollManager.Instance.CrunOptions.History){
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
@ -16,6 +17,7 @@ using Avalonia.Media.Imaging;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
using CRD.Utils.Ffmpeg_Encoding;
|
using CRD.Utils.Ffmpeg_Encoding;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
|
using CRD.Utils.HLS;
|
||||||
using CRD.Utils.JsonConv;
|
using CRD.Utils.JsonConv;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.Crunchyroll;
|
using CRD.Utils.Structs.Crunchyroll;
|
||||||
|
|
@ -39,6 +41,22 @@ public class Helpers{
|
||||||
return default;
|
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>(T obj){
|
public static T DeepCopy<T>(T obj){
|
||||||
var settings = new JsonSerializerSettings{
|
var settings = new JsonSerializerSettings{
|
||||||
ContractResolver = new DefaultContractResolver{
|
ContractResolver = new DefaultContractResolver{
|
||||||
|
|
@ -626,7 +644,7 @@ public class Helpers{
|
||||||
group.Add(descriptionMedia[0]);
|
group.Add(descriptionMedia[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return languageGroups;
|
return languageGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -765,94 +783,27 @@ public class Helpers{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CrDownloadOptions MigrateSettings(CrDownloadOptionsYaml yaml){
|
public static void MergePlaylistData(
|
||||||
if (yaml == null){
|
Dictionary<string, ServerData> target,
|
||||||
throw new ArgumentNullException(nameof(yaml));
|
Dictionary<string, ServerData> 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<AudioPlaylist>(kvp.Value.audio) : new List<AudioPlaylist>(),
|
||||||
|
video = kvp.Value.video != null ? new List<VideoPlaylist>(kvp.Value.video) : new List<VideoPlaylist>()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +163,7 @@ public class HttpClientReq{
|
||||||
cookieStore[domain].Add(cookie);
|
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;
|
string content = string.Empty;
|
||||||
try{
|
try{
|
||||||
AttachCookies(request);
|
AttachCookies(request);
|
||||||
|
|
@ -178,14 +178,14 @@ public class HttpClientReq{
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
return (IsOk: true, ResponseContent: content);
|
return (IsOk: true, ResponseContent: content,error:"");
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||||
if (!suppressError){
|
if (!suppressError){
|
||||||
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ public class MPDParsed{
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ServerData{
|
public class ServerData{
|
||||||
public List<AudioPlaylist> audio{ get; set; }
|
public List<AudioPlaylist> audio{ get; set; } =[];
|
||||||
public List<VideoPlaylist> video{ get; set; }
|
public List<VideoPlaylist> video{ get; set; } =[];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MPDParser{
|
public static class MPDParser{
|
||||||
|
|
|
||||||
|
|
@ -274,261 +274,12 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint")]
|
[JsonProperty("stream_endpoint")]
|
||||||
public string? StreamEndpoint{ get; set; }
|
public string? StreamEndpoint{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("stream_endpoint_secondary")]
|
||||||
|
public string? StreamEndpointSecondary { get; set; }
|
||||||
|
|
||||||
[JsonProperty("search_fetch_featured_music")]
|
[JsonProperty("search_fetch_featured_music")]
|
||||||
public bool SearchFetchFeaturedMusic{ get; set; }
|
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<string> 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<string> 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<string> FfmpegOptions{ get; set; } =[];
|
|
||||||
|
|
||||||
[YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)]
|
|
||||||
public List<string> 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<string> 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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ public class StreamDetails{
|
||||||
[JsonProperty("hardsub_locale")]
|
[JsonProperty("hardsub_locale")]
|
||||||
public Locale? HardsubLocale{ get; set; }
|
public Locale? HardsubLocale{ get; set; }
|
||||||
|
|
||||||
public string? Url{ get; set; }
|
public List<string?> Url{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("hardsub_lang")]
|
[JsonProperty("hardsub_lang")]
|
||||||
public required LanguageItem HardsubLang{ get; set; }
|
public required LanguageItem HardsubLang{ get; set; }
|
||||||
|
|
@ -28,7 +28,7 @@ public class StreamDetails{
|
||||||
|
|
||||||
public class StreamDetailsPop{
|
public class StreamDetailsPop{
|
||||||
public Locale? HardsubLocale{ get; set; }
|
public Locale? HardsubLocale{ get; set; }
|
||||||
public string? Url{ get; set; }
|
public List<string?> Url{ get; set; }
|
||||||
public required LanguageItem HardsubLang{ get; set; }
|
public required LanguageItem HardsubLang{ get; set; }
|
||||||
|
|
||||||
public bool IsHardsubbed{ get; set; }
|
public bool IsHardsubbed{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
|
using CRD.Utils.Sonarr.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CRD.Utils.Structs.History;
|
namespace CRD.Utils.Structs.History;
|
||||||
|
|
@ -58,6 +59,18 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
||||||
[JsonProperty("sonarr_absolut_number")]
|
[JsonProperty("sonarr_absolut_number")]
|
||||||
public string? SonarrAbsolutNumber{ get; set; }
|
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")]
|
[JsonProperty("history_episode_available_soft_subs")]
|
||||||
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
|
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
|
||||||
|
|
||||||
|
|
@ -118,7 +131,16 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
||||||
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
|
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
|
||||||
break;
|
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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
55
CRD/Utils/UI/CustomContentDialog.cs
Normal file
55
CRD/Utils/UI/CustomContentDialog.cs
Normal file
|
|
@ -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<IImage> BackgroundImageProperty =
|
||||||
|
AvaloniaProperty.Register<CustomContentDialog, IImage>(nameof(BackgroundImage));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> BackgroundImageOpacityProperty =
|
||||||
|
AvaloniaProperty.Register<CustomContentDialog, double>(nameof(BackgroundImageOpacity), 0.5);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> BackgroundImageBlurRadiusProperty =
|
||||||
|
AvaloniaProperty.Register<CustomContentDialog, double>(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<Image>("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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -285,8 +285,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string locale, string id)? ExtractLocaleAndIdFromUrl(){
|
private (string locale, string id)? ExtractLocaleAndIdFromUrl(){
|
||||||
var match = Regex.Match(UrlInput, "/([^/]+)/(?:artist|watch|series)(?:/(?:musicvideo|concert))?/([^/]+)/?");
|
var match = Regex.Match(UrlInput, @"^(?:https?:\/\/[^/]+)?(?:\/([a-z]{2}))?\/(?:[^/]+\/)?(artist|watch|series)(?:\/(musicvideo|concert))?\/([^/]+)(?:\/[^/]*)?$");
|
||||||
return match.Success ? (match.Groups[1].Value, match.Groups[2].Value) : null;
|
|
||||||
|
return match.Success
|
||||||
|
? (match.Groups[1].Value ?? "", match.Groups[4].Value)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CrunchyUrlType GetUrlType(){
|
private CrunchyUrlType GetUrlType(){
|
||||||
|
|
@ -341,12 +344,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
||||||
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true);
|
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true);
|
||||||
|
|
||||||
if (musicList != null){
|
if (musicList != null){
|
||||||
currentMusicVideoList = musicList;
|
currentMusicVideoList = musicList;
|
||||||
PopulateItemsFromMusicVideoList();
|
PopulateItemsFromMusicVideoList();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SetLoadingState(false);
|
SetLoadingState(false);
|
||||||
|
|
@ -477,7 +479,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
|
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
|
||||||
|
|
||||||
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
|
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
|
||||||
|
|
||||||
if (CurrentSeasonFullySelected){
|
if (CurrentSeasonFullySelected){
|
||||||
|
|
@ -581,18 +582,17 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
||||||
episodesBySeason[seasonKey].Add(episodeModel);
|
episodesBySeason[seasonKey].Add(episodeModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
||||||
var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
|
var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
|
||||||
? CrunchyrollManager.Instance.DefaultLocale
|
? CrunchyrollManager.Instance.DefaultLocale
|
||||||
: CrunchyrollManager.Instance.CrunOptions.HistoryLang;
|
: CrunchyrollManager.Instance.CrunOptions.HistoryLang;
|
||||||
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(seriesId, DetermineLocale(locale), true);
|
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(seriesId, DetermineLocale(locale), true);
|
||||||
|
|
||||||
if (musicList != null){
|
if (musicList != null){
|
||||||
currentMusicVideoList = musicList;
|
currentMusicVideoList = musicList;
|
||||||
PopulateItemsFromMusicVideoList();
|
PopulateItemsFromMusicVideoList();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentSelectedSeason = SeasonList.First();
|
CurrentSelectedSeason = SeasonList.First();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -11,7 +10,6 @@ using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.CustomList;
|
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.Crunchyroll;
|
using CRD.Utils.Structs.Crunchyroll;
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private static string _progressText;
|
private static string _progressText;
|
||||||
|
|
||||||
|
public Vector LastScrollOffset { get; set; } = Vector.Zero;
|
||||||
|
|
||||||
public HistoryPageViewModel(){
|
public HistoryPageViewModel(){
|
||||||
ProgramManager = ProgramManager.Instance;
|
ProgramManager = ProgramManager.Instance;
|
||||||
|
|
@ -343,7 +345,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
NavToSeries();
|
NavToSeries();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
|
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();
|
CfgManager.UpdateHistoryFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
|
@ -8,10 +7,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
|
using CRD.Utils.UI;
|
||||||
using CRD.ViewModels.Utils;
|
using CRD.ViewModels.Utils;
|
||||||
using CRD.Views;
|
using CRD.Views;
|
||||||
using CRD.Views.Utils;
|
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]
|
[RelayCommand]
|
||||||
public async Task DownloadSeasonAll(HistorySeason season){
|
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]
|
[RelayCommand]
|
||||||
public async Task UpdateData(string? season){
|
public async Task UpdateData(string? season){
|
||||||
var result = await SelectedSeries.FetchData(season);
|
var result = await SelectedSeries.FetchData(season);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ using CRD.Views.Utils;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Image = Avalonia.Controls.Image;
|
using Image = Avalonia.Controls.Image;
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
|
||||||
|
|
||||||
namespace CRD.ViewModels;
|
namespace CRD.ViewModels;
|
||||||
|
|
||||||
public class SettingsPageViewModel : ViewModelBase{
|
public class SettingsPageViewModel : ViewModelBase{
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,30 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils.Updater;
|
using CRD.Utils.Updater;
|
||||||
using Markdig;
|
using Markdig;
|
||||||
|
using Markdig.Syntax;
|
||||||
|
using Markdig.Syntax.Inlines;
|
||||||
|
using Inline = Markdig.Syntax.Inlines.Inline;
|
||||||
|
|
||||||
namespace CRD.ViewModels;
|
namespace CRD.ViewModels;
|
||||||
|
|
||||||
public partial class UpdateViewModel : ViewModelBase{
|
public partial class UpdateViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _updateAvailable;
|
private bool _updateAvailable;
|
||||||
|
|
||||||
|
|
@ -27,24 +36,22 @@ public partial class UpdateViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _failed;
|
private bool _failed;
|
||||||
|
|
||||||
private AccountPageViewModel accountPageViewModel;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
private AccountPageViewModel accountPageViewModel;
|
||||||
private string _changelogText = "<p><strong>No changelog found.</strong></p>";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _currentVersion;
|
private string _currentVersion;
|
||||||
|
|
||||||
|
public ObservableCollection<Control> ChangelogBlocks{ get; } = new();
|
||||||
|
|
||||||
public UpdateViewModel(){
|
public UpdateViewModel(){
|
||||||
|
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
|
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
|
||||||
|
|
||||||
LoadChangelog();
|
LoadChangelog();
|
||||||
|
|
||||||
UpdateAvailable = ProgramManager.Instance.UpdateAvailable;
|
UpdateAvailable = ProgramManager.Instance.UpdateAvailable;
|
||||||
|
|
||||||
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
|
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +59,6 @@ public partial class UpdateViewModel : ViewModelBase{
|
||||||
public void StartUpdate(){
|
public void StartUpdate(){
|
||||||
Updating = true;
|
Updating = true;
|
||||||
ProgramManager.Instance.NavigationLock = true;
|
ProgramManager.Instance.NavigationLock = true;
|
||||||
// Title = "Updating";
|
|
||||||
_ = Updater.Instance.DownloadAndUpdateAsync();
|
_ = 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";
|
string changelogPath = "CHANGELOG.md";
|
||||||
|
|
||||||
if (!File.Exists(changelogPath)){
|
if (!File.Exists(changelogPath)){
|
||||||
ChangelogText = "<p><strong>No changelog found.</strong></p>";
|
ChangelogBlocks.Clear();
|
||||||
|
ChangelogBlocks.Add(new TextBlock{
|
||||||
|
Text = "No changelog found",
|
||||||
|
FontSize = 16,
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,104 +92,206 @@ public partial class UpdateViewModel : ViewModelBase{
|
||||||
|
|
||||||
markdownText = PreprocessMarkdown(markdownText);
|
markdownText = PreprocessMarkdown(markdownText);
|
||||||
|
|
||||||
var pipeline = new MarkdownPipelineBuilder()
|
var pipeline = new MarkdownPipelineBuilder().Build();
|
||||||
.UseAdvancedExtensions()
|
var document = Markdown.Parse(markdownText, pipeline);
|
||||||
.UseSoftlineBreakAsHardlineBreak()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
string htmlContent = Markdown.ToHtml(markdownText, pipeline);
|
ChangelogBlocks.Clear();
|
||||||
|
|
||||||
htmlContent = MakeIssueLinksClickable(htmlContent);
|
try{
|
||||||
htmlContent = ModifyImages(htmlContent);
|
foreach (var block in document){
|
||||||
|
switch (block){
|
||||||
Color themeTextColor = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark ? Colors.White : Color.Parse("#E4000000");
|
case HeadingBlock heading:
|
||||||
string cssColor = $"#{themeTextColor.R:X2}{themeTextColor.G:X2}{themeTextColor.B:X2}";
|
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 = $@"
|
case ParagraphBlock paragraph:
|
||||||
<html>
|
var inlineControls = BuildInlineControls(paragraph.Inline?.FirstChild);
|
||||||
<head>
|
var container = new WrapPanel{
|
||||||
<style type=""text/css"">
|
Margin = new Thickness(0, 5, 0, 5)
|
||||||
body {{
|
};
|
||||||
color: {cssColor};
|
foreach (var ctrl in inlineControls)
|
||||||
background: transparent;
|
container.Children.Add(ctrl);
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}}
|
|
||||||
img {{
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
margin: 10px auto;
|
|
||||||
max-height: 300px;
|
|
||||||
object-fit: contain;
|
|
||||||
}}
|
|
||||||
li {{
|
|
||||||
margin-bottom: 10px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}}
|
|
||||||
code {{
|
|
||||||
background: #f0f0f0;
|
|
||||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: middle;
|
|
||||||
display: inline-block;
|
|
||||||
}}
|
|
||||||
pre code {{
|
|
||||||
background: #f5f5f5;
|
|
||||||
display: block;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{htmlContent}
|
|
||||||
</body>
|
|
||||||
</html>";
|
|
||||||
|
|
||||||
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){
|
IEnumerable<Control> BuildInlineControls(Inline? inline){
|
||||||
// Match GitHub issue links
|
var controls = new List<Control>();
|
||||||
string issuePattern = @"<a href=['""](https:\/\/github\.com\/Crunchy-DL\/Crunchy-Downloader\/issues\/(\d+))['""][^>]*>[^<]+<\/a>";
|
var urlRegex = new Regex(@"https?://[^\s]+", RegexOptions.Compiled);
|
||||||
|
var githubRefRegex = new Regex(
|
||||||
|
@"https://github\.com/[^/]+/[^/]+/(issues|discussions)/(\d+)",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
// Match GitHub discussion links
|
while (inline != null){
|
||||||
string discussionPattern = @"<a href=['""](https:\/\/github\.com\/Crunchy-DL\/Crunchy-Downloader\/discussions\/(\d+))['""][^>]*>[^<]+<\/a>";
|
switch (inline){
|
||||||
|
case LiteralInline lit:
|
||||||
|
var text = lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length);
|
||||||
|
|
||||||
htmlContent = Regex.Replace(htmlContent, issuePattern, match => {
|
var lastIndex = 0;
|
||||||
string fullUrl = match.Groups[1].Value;
|
foreach (Match match in urlRegex.Matches(text)){
|
||||||
string issueNumber = match.Groups[2].Value;
|
if (match.Index > lastIndex){
|
||||||
return $"<a href='{fullUrl}' target='_blank'>#{issueNumber}</a>";
|
controls.Add(new TextBlock{
|
||||||
});
|
Text = text.Substring(lastIndex, match.Index - lastIndex),
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
FontSize = textSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
htmlContent = Regex.Replace(htmlContent, discussionPattern, match => {
|
string url = match.Value;
|
||||||
string fullUrl = match.Groups[1].Value;
|
string buttonText = url;
|
||||||
string discussionNumber = match.Groups[2].Value;
|
|
||||||
return $"<a href='{fullUrl}' target='_blank'>#{discussionNumber}</a>";
|
|
||||||
});
|
|
||||||
|
|
||||||
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){
|
string ConvertInlinesToText(Inline? inline){
|
||||||
// Regex to match <img> tags
|
var result = new StringBuilder();
|
||||||
string imgPattern = @"<img\s+src=['""]([^'""]+)['""]( alt=['""]([^'""]+)['""])?\s*\/?>";
|
|
||||||
|
|
||||||
return Regex.Replace(htmlContent, imgPattern, match => {
|
while (inline != null){
|
||||||
string imgUrl = match.Groups[1].Value;
|
switch (inline){
|
||||||
string altText = "View Image"; // match.Groups[3].Success ? match.Groups[3].Value : "View Image";
|
case LiteralInline lit:
|
||||||
|
result.Append(lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length));
|
||||||
|
break;
|
||||||
|
|
||||||
return $"<a href='{imgUrl}' target='_blank'>{altText}</a>";
|
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){
|
private string PreprocessMarkdown(string markdownText){
|
||||||
// Regex to match <details> blocks containing an image
|
|
||||||
string detailsPattern = @"<details>\s*<summary>.*?<\/summary>\s*<img\s+src=['""]([^'""]+)['""]\s+alt=['""]([^'""]+)['""]\s*\/?>\s*<\/details>";
|
string detailsPattern = @"<details>\s*<summary>.*?<\/summary>\s*<img\s+src=['""]([^'""]+)['""]\s+alt=['""]([^'""]+)['""]\s*\/?>\s*<\/details>";
|
||||||
|
|
||||||
return Regex.Replace(markdownText, detailsPattern, match => {
|
return Regex.Replace(markdownText, detailsPattern, match => {
|
||||||
|
|
@ -184,5 +301,6 @@ public partial class UpdateViewModel : ViewModelBase{
|
||||||
return $"";
|
return $"";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ using System.Linq;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Utils;
|
|
||||||
using CRD.Utils.Ffmpeg_Encoding;
|
using CRD.Utils.Ffmpeg_Encoding;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
|
|
|
||||||
135
CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs
Normal file
135
CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs
Normal file
|
|
@ -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<SeasonsItem> _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<List<SeasonsItem>> PopulateSeriesList(string sonarrSeriesId){
|
||||||
|
List<SonarrEpisode> episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(sonarrSeriesId));
|
||||||
|
|
||||||
|
var seasonsDict = new Dictionary<int, SeasonsItem>();
|
||||||
|
|
||||||
|
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<SonarrEpisode>()
|
||||||
|
};
|
||||||
|
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<SonarrEpisode> Episodes{ get; set; }
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
xmlns:structs="clr-namespace:CRD.Utils.Structs"
|
xmlns:structs="clr-namespace:CRD.Utils.Structs"
|
||||||
x:DataType="vm:HistoryPageViewModel"
|
x:DataType="vm:HistoryPageViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="CRD.Views.HistoryPageView">
|
x:Class="CRD.Views.HistoryPageView"
|
||||||
|
Unloaded="OnUnloaded"
|
||||||
|
Loaded="Control_OnLoaded">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
|
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
|
||||||
|
|
@ -235,7 +237,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding FilteredItems}"
|
<ListBox x:Name="SeriesListBox" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding FilteredItems}"
|
||||||
SelectedItem="{Binding SelectedSeries}"
|
SelectedItem="{Binding SelectedSeries}"
|
||||||
Margin="5" IsVisible="{Binding IsPosterViewSelected}">
|
Margin="5" IsVisible="{Binding IsPosterViewSelected}">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using CRD.ViewModels;
|
||||||
|
|
||||||
namespace CRD.Views;
|
namespace CRD.Views;
|
||||||
|
|
||||||
public partial class HistoryPageView : UserControl{
|
public partial class HistoryPageView : UserControl{
|
||||||
|
|
||||||
public HistoryPageView(){
|
public HistoryPageView(){
|
||||||
InitializeComponent();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +152,8 @@
|
||||||
|
|
||||||
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
|
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsChecked="{Binding SelectedSeries.IsInactive}" Command="{Binding ToggleInactive}">
|
IsChecked="{Binding SelectedSeries.IsInactive}"
|
||||||
|
Command="{Binding ToggleInactive}">
|
||||||
<ToolTip.Tip>
|
<ToolTip.Tip>
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Text="Set Active"
|
<TextBlock Text="Set Active"
|
||||||
|
|
@ -323,12 +324,33 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel IsVisible="{Binding EditMode}">
|
<StackPanel IsVisible="{Binding EditMode}">
|
||||||
<Button Width="30" Height="30" Margin="0 0 10 0"
|
<Button Width="30" Height="30" Margin="0 0 5 0"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
IsVisible="{Binding SonarrConnected}"
|
IsVisible="{Binding SonarrConnected}"
|
||||||
Command="{Binding MatchSonarrSeries_Button}">
|
Command="{Binding MatchSonarrSeries_Button}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
|
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
|
||||||
|
<ToolTip.Tip>
|
||||||
|
<TextBlock Text="Match Sonarr Series" FontSize="15" />
|
||||||
|
</ToolTip.Tip>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel IsVisible="{Binding EditMode}">
|
||||||
|
<Button 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>
|
||||||
|
|
||||||
|
<ToolTip.Tip>
|
||||||
|
<TextBlock Text="Rematch all Sonarr Episodes" FontSize="15" />
|
||||||
|
</ToolTip.Tip>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
@ -366,20 +388,21 @@
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
|
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
|
||||||
Margin="0 0 5 0 "
|
Margin="0 0 5 0 "
|
||||||
Symbol="AlertOn"
|
Symbol="AlertOn"
|
||||||
FontSize="18"
|
FontSize="18"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<ToolTip.Tip>
|
<ToolTip.Tip>
|
||||||
<TextBlock Text="Episode unavailable — it might not be available on the streaming service" FontSize="15" />
|
<TextBlock Text="Episode unavailable — it might not be available on the streaming service or got moved to a different season" FontSize="15" />
|
||||||
</ToolTip.Tip>
|
</ToolTip.Tip>
|
||||||
|
|
||||||
</controls:SymbolIcon>
|
</controls:SymbolIcon>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
|
||||||
|
|
||||||
<ui:EpisodeHighlightTextBlock
|
<ui:EpisodeHighlightTextBlock
|
||||||
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
|
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
|
||||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||||
|
|
@ -425,9 +448,13 @@
|
||||||
|
|
||||||
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
|
<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"
|
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
|
||||||
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
|
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}"
|
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ShowMonitoredBookmark}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|
@ -449,13 +476,21 @@
|
||||||
</controls:SymbolIcon>
|
</controls:SymbolIcon>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
|
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
|
||||||
Source="../Assets/sonarr.png" Width="25"
|
BorderThickness="0" CornerRadius="50"
|
||||||
Height="25" />
|
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).MatchSonarrEpisode_Button}"
|
||||||
|
CommandParameter="{Binding }">
|
||||||
|
<StackPanel>
|
||||||
|
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
|
||||||
|
Source="../Assets/sonarr.png" Width="25"
|
||||||
|
Height="25" />
|
||||||
|
|
||||||
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
|
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
|
||||||
Source="../Assets/sonarr_inactive.png" Width="25"
|
Source="../Assets/sonarr_inactive.png" Width="25"
|
||||||
Height="25" />
|
Height="25" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,5 @@ public partial class SettingsPageView : UserControl{
|
||||||
SonarrClient.Instance.RefreshSonarr();
|
SonarrClient.Instance.RefreshSonarr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ListBox_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){
|
|
||||||
var listBox = sender as ListBox;
|
|
||||||
var scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:CRD.ViewModels"
|
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"
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
x:DataType="vm:UpdateViewModel"
|
x:DataType="vm:UpdateViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="CRD.Views.UpdateView">
|
x:Class="CRD.Views.UpdateView">
|
||||||
|
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
|
@ -59,25 +59,37 @@
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1"
|
||||||
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" IsVisible="{Binding !Updating}" MaxWidth="700"
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="3"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding !Updating}"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
<avalonia:HtmlLabel Name="HtmlContent"
|
|
||||||
Text="{Binding ChangelogText}"
|
<Border HorizontalAlignment="Center"
|
||||||
Margin="10"
|
MaxWidth="800">
|
||||||
VerticalAlignment="Stretch" />
|
|
||||||
|
<ItemsControl ItemsSource="{Binding ChangelogBlocks}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
</Border>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|
||||||
<!-- Update Progress Section -->
|
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Spacing="10" VerticalAlignment="Center" HorizontalAlignment="Center" IsVisible="{Binding Updating}">
|
||||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Spacing="10" IsVisible="{Binding Updating}">
|
<TextBlock IsVisible="{Binding !Failed}" FontSize="24" Text="Please wait while the update is being downloaded..." HorizontalAlignment="Center" Margin="0,10,0,20" />
|
||||||
<TextBlock IsVisible="{Binding !Failed}" Text="Please wait while the update is being downloaded..." HorizontalAlignment="Center" Margin="0,10,0,20" />
|
<TextBlock IsVisible="{Binding Failed}" FontSize="24" Foreground="IndianRed" Text="Update failed check the log for more information" HorizontalAlignment="Center" Margin="0,10,0,20" />
|
||||||
<TextBlock IsVisible="{Binding Failed}" Foreground="IndianRed" Text="Update failed check the log for more information" HorizontalAlignment="Center" Margin="0,10,0,20" />
|
<ProgressBar Minimum="0" Maximum="100" FontSize="24" Value="{Binding Progress}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="350" />
|
||||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="350" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
167
CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml
Normal file
167
CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
<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:ContentDialogSonarrMatchEpisodeViewModel"
|
||||||
|
x:Class="CRD.Views.Utils.ContentDialogSonarrMatchEpisodeView">
|
||||||
|
|
||||||
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Column="0" Grid.Row="1" CornerRadius="10" Background="{DynamicResource ButtonBackground}">
|
||||||
|
<Grid Margin="10" VerticalAlignment="Top">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Column="1" Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<!-- Takes up space as needed for the time -->
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="16" FontWeight="Bold" Text="History Episode:" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal">
|
||||||
|
<TextBlock Text="S"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentHistoryEpisode.EpisodeSeasonNum}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="E"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentHistoryEpisode.Episode}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text=" - "
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentHistoryEpisode.EpisodeTitle}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding CurrentHistoryEpisode.EpisodeCrPremiumAirDate}" FontStyle="Italic"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" FontSize="16" FontWeight="Bold" Text="Sonarr Episode:" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="3" Grid.Column="0" Orientation="Horizontal">
|
||||||
|
<TextBlock Text="S"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentSonarrEpisode.SeasonNumber}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="E"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentSonarrEpisode.EpisodeNumber}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text=" ("
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentSonarrEpisode.AbsoluteEpisodeNumber}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text=") - "
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding CurrentSonarrEpisode.Title}"
|
||||||
|
FontSize="16"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding CurrentSonarrEpisode.AirDateUtc}" FontStyle="Italic"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ItemsControl ItemsSource="{Binding SonarrSeasonList}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<controls:SettingsExpander
|
||||||
|
Header="{Binding SeasonName}"
|
||||||
|
ItemsSource="{Binding Episodes}"
|
||||||
|
IsExpanded="{Binding IsExpanded}">
|
||||||
|
<controls:SettingsExpander.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
|
||||||
|
<Grid VerticalAlignment="Center">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
|
||||||
|
<TextBlock Text="E" />
|
||||||
|
<TextBlock Text="{Binding EpisodeNumber}" />
|
||||||
|
<TextBlock Text=" (" />
|
||||||
|
<TextBlock Text="{Binding AbsoluteEpisodeNumber}" />
|
||||||
|
<TextBlock Text=") - " />
|
||||||
|
<TextBlock Text="{Binding Title}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding AirDateUtc}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="15"
|
||||||
|
Opacity="0.8"
|
||||||
|
Margin="0 0 20 0" />
|
||||||
|
|
||||||
|
<Button Margin="0 0 10 0"
|
||||||
|
Command="{Binding $parent[ScrollViewer].((vm:ContentDialogSonarrMatchEpisodeViewModel)DataContext).SetSonarrEpisodeMatch}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<controls:SymbolIcon Symbol="Accept" FontSize="18" />
|
||||||
|
</StackPanel>
|
||||||
|
<ToolTip.Tip>
|
||||||
|
<TextBlock Text="Set Sonarr Episode" FontSize="15" />
|
||||||
|
</ToolTip.Tip>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
</DataTemplate>
|
||||||
|
</controls:SettingsExpander.ItemTemplate>
|
||||||
|
|
||||||
|
</controls:SettingsExpander>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
</ui:CustomContentDialog>
|
||||||
12
CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs
Normal file
12
CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs
Normal 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 ContentDialogSonarrMatchEpisodeView : UserControl{
|
||||||
|
public ContentDialogSonarrMatchEpisodeView(){
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||||
x:DataType="vm:ContentDialogSonarrMatchViewModel"
|
x:DataType="vm:ContentDialogSonarrMatchViewModel"
|
||||||
x:Class="CRD.Views.Utils.ContentDialogSonarrMatchView">
|
x:Class="CRD.Views.Utils.ContentDialogSonarrMatchView">
|
||||||
|
|
||||||
<Grid HorizontalAlignment="Stretch" MaxHeight="500" >
|
<Grid HorizontalAlignment="Stretch" MaxHeight="600">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Border Grid.Column="0" Grid.Row="1" CornerRadius="10" Background="{DynamicResource ButtonBackground}">
|
<Border Grid.Column="0" Grid.Row="1" CornerRadius="10" Background="{DynamicResource ButtonBackground}">
|
||||||
<Grid Margin="10" VerticalAlignment="Top">
|
<Grid Margin="10" VerticalAlignment="Top">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
<Image Grid.Column="0" Margin="10" asyncImageLoader:ImageLoader.Source="{Binding CurrentSonarrSeries.ImageUrl}" MaxWidth="120" MaxHeight="180"></Image>
|
<Image Grid.Column="0" Margin="10" asyncImageLoader:ImageLoader.Source="{Binding CurrentSonarrSeries.ImageUrl}" MaxWidth="120" MaxHeight="180"></Image>
|
||||||
|
|
||||||
<Grid Grid.Column="1" Margin="10">
|
<Grid Grid.Column="1" Margin="10">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
||||||
|
|
@ -37,62 +37,64 @@
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding CurrentSonarrSeries.Title}" FontWeight="Bold"
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding CurrentSonarrSeries.Title}" FontWeight="Bold"
|
||||||
FontSize="16"
|
FontSize="16"
|
||||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding CurrentSonarrSeries.Year}" FontStyle="Italic"
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding CurrentSonarrSeries.Year}" FontStyle="Italic"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center" />
|
HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||||
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"
|
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" MaxHeight="150"
|
||||||
Text="{Binding CurrentSonarrSeries.Overview}"
|
Text="{Binding CurrentSonarrSeries.Overview}"
|
||||||
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
|
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
|
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- <Rectangle Grid.Row="2" Width="1500" Height="0" Fill="Gray" Margin="10,0" /> -->
|
<!-- <Rectangle Grid.Row="2" Width="1500" Height="0" Fill="Gray" Margin="10,0" /> -->
|
||||||
<!-- <TextBlock Grid.Column="0" Grid.Row="2" Text="Series"></TextBlock> -->
|
<!-- <TextBlock Grid.Column="0" Grid.Row="2" Text="Series"></TextBlock> -->
|
||||||
|
|
||||||
<ListBox Grid.Row="3" SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding SonarrSeriesList}">
|
<ListBox Grid.Row="3" SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding SonarrSeriesList}">
|
||||||
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type models:SonarrSeries}">
|
<DataTemplate DataType="{x:Type models:SonarrSeries}">
|
||||||
<StackPanel>
|
<StackPanel Height="220">
|
||||||
<Border Padding="10" Margin="5" BorderThickness="1">
|
<Border Padding="10" Margin="5" BorderThickness="1">
|
||||||
<Grid Margin="10" VerticalAlignment="Top">
|
<Grid VerticalAlignment="Top">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<!-- Image -->
|
|
||||||
<asyncImageLoader:AdvancedImage Grid.Column="0" MaxWidth="120" MaxHeight="180" Source="{Binding ImageUrl}"
|
<asyncImageLoader:AdvancedImage Grid.Column="0" MaxWidth="120" MaxHeight="180" Source="{Binding ImageUrl}"
|
||||||
Stretch="Fill" />
|
Stretch="Fill" />
|
||||||
|
|
||||||
<!-- Text Content -->
|
<!-- Text Content -->
|
||||||
<Grid Grid.Column="1" Margin="10" VerticalAlignment="Top">
|
<Grid Grid.Column="1" Margin="10" VerticalAlignment="Top">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<!-- Takes up space as needed for the time -->
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Title}" FontWeight="Bold"
|
<TextBlock Grid.Column="0" Text="{Binding Title}" FontWeight="Bold"
|
||||||
FontSize="16"
|
FontSize="16"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Year}" FontStyle="Italic"
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Year}" FontStyle="Italic"
|
||||||
HorizontalAlignment="Right" TextWrapping="Wrap" />
|
HorizontalAlignment="Right" TextWrapping="Wrap" />
|
||||||
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"
|
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" MaxHeight="150"
|
||||||
Text="{Binding Overview}"
|
Text="{Binding Overview}"
|
||||||
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
|
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<Border Background="LightGray" Height="1" Margin="0,5" HorizontalAlignment="Stretch" />
|
<Border Background="LightGray" Height="1" Margin="0,5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue