mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +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>
|
||||
<sty:FluentAvaloniaTheme PreferSystemTheme="True" PreferUserAccentColor="True"/>
|
||||
<StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" />
|
||||
<StyleInclude Source="avares://CRD/Styling/ContentDialogCustomStyle.axaml" />
|
||||
<StyleInclude Source="avares://CRD/Assets/Icons.axaml"></StyleInclude>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
|
|
@ -65,8 +65,8 @@ public class CalendarManager{
|
|||
return forDate;
|
||||
}
|
||||
|
||||
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "de")
|
||||
? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "de"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null)
|
||||
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us")
|
||||
? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null)
|
||||
: HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -147,64 +147,7 @@ public class CrunchyrollManager{
|
|||
|
||||
options.History = true;
|
||||
|
||||
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
|
||||
var optionsYaml = new CrDownloadOptionsYaml();
|
||||
|
||||
optionsYaml.UseCrBetaApi = true;
|
||||
optionsYaml.AutoDownload = false;
|
||||
optionsYaml.RemoveFinishedDownload = false;
|
||||
optionsYaml.Chapters = true;
|
||||
optionsYaml.Hslang = "none";
|
||||
optionsYaml.Force = "Y";
|
||||
optionsYaml.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]";
|
||||
optionsYaml.Partsize = 10;
|
||||
optionsYaml.DlSubs = new List<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);
|
||||
}
|
||||
|
||||
CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
@ -226,11 +169,6 @@ public class CrunchyrollManager{
|
|||
PreferredContentSubtitleLanguage = DefaultLocale,
|
||||
HasPremium = false,
|
||||
};
|
||||
|
||||
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
|
||||
CfgManager.WriteCrSettings();
|
||||
Helpers.DeleteFile(CfgManager.PathCrDownloadOptionsOld);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> GetBase64EncodedTokenAsync(){
|
||||
|
|
@ -290,9 +228,6 @@ public class CrunchyrollManager{
|
|||
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
|
||||
Token = CfgManager.ReadJsonFromFile<CrToken>(CfgManager.PathCrToken);
|
||||
await CrAuth.LoginWithToken();
|
||||
if (Path.Exists(CfgManager.PathCrTokenOld)){
|
||||
Helpers.DeleteFile(CfgManager.PathCrTokenOld);
|
||||
}
|
||||
} else{
|
||||
await CrAuth.AuthAnonymous();
|
||||
}
|
||||
|
|
@ -391,7 +326,6 @@ public class CrunchyrollManager{
|
|||
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
||||
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
||||
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
|
||||
|
||||
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
||||
var mergers = new List<Merger>();
|
||||
foreach (var keyValue in groupByDub){
|
||||
|
|
@ -999,9 +933,12 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
var fetchPlaybackData = await FetchPlaybackData(options, mediaId, mediaGuid, data.Music);
|
||||
|
||||
var fetchPlaybackData = await FetchPlaybackData(options.StreamEndpoint ?? "web/firefox", mediaId, mediaGuid, data.Music);
|
||||
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
|
||||
if (!string.IsNullOrEmpty(options.StreamEndpointSecondary) && !(options.StreamEndpoint ?? "web/firefox").Equals(options.StreamEndpointSecondary)){
|
||||
fetchPlaybackData2 = await FetchPlaybackData(options.StreamEndpointSecondary, mediaId, mediaGuid, data.Music);
|
||||
}
|
||||
|
||||
if (!fetchPlaybackData.IsOk){
|
||||
var errorJson = fetchPlaybackData.error;
|
||||
|
|
@ -1048,6 +985,21 @@ public class CrunchyrollManager{
|
|||
ErrorText = "Playback data not found"
|
||||
};
|
||||
}
|
||||
|
||||
if (fetchPlaybackData2.IsOk){
|
||||
if (fetchPlaybackData.pbData.Data != null)
|
||||
foreach (var keyValuePair in fetchPlaybackData.pbData.Data){
|
||||
var value = fetchPlaybackData2.pbData?.Data?[keyValuePair.Key];
|
||||
var url = value?.Url.First() ?? "";
|
||||
|
||||
var match = Regex.Match(url, @"(.*\.urlset\/)");
|
||||
var shortendUrl = match.Success ? match.Value : url;
|
||||
|
||||
if (!keyValuePair.Value.Url.Any(arrayUrl => arrayUrl != null && arrayUrl.Contains(shortendUrl))){
|
||||
keyValuePair.Value.Url.Add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var pbData = fetchPlaybackData.pbData;
|
||||
|
|
@ -1106,7 +1058,7 @@ public class CrunchyrollManager{
|
|||
streams = streams.Select(s => {
|
||||
s.AudioLang = audDub;
|
||||
s.HardsubLang = s.HardsubLang;
|
||||
s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}";
|
||||
s.Type = $"{s.Format}/{s.AudioLang.CrLocale}/{s.HardsubLang.CrLocale}";
|
||||
return s;
|
||||
}).ToList();
|
||||
|
||||
|
|
@ -1209,35 +1161,76 @@ public class CrunchyrollManager{
|
|||
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
|
||||
|
||||
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
|
||||
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
|
||||
|
||||
|
||||
Dictionary<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){
|
||||
dlFailed = true;
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = dlFailed,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "Playlist fetch problem"
|
||||
};
|
||||
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
||||
streamPlaylistsReqResponseList[streamUrl ?? ""] = streamPlaylistsReqResponse.ResponseContent;
|
||||
}
|
||||
}
|
||||
|
||||
//Use again when cr has all endpoints with new encoding
|
||||
// var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
|
||||
//
|
||||
// var streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq);
|
||||
//
|
||||
// if (!streamPlaylistsReqResponse.IsOk){
|
||||
// dlFailed = true;
|
||||
// return new DownloadResponse{
|
||||
// Data = new List<DownloadedMedia>(),
|
||||
// Error = dlFailed,
|
||||
// FileName = "./unknown",
|
||||
// ErrorText = "Playlist fetch problem"
|
||||
// };
|
||||
// }
|
||||
|
||||
if (dlFailed){
|
||||
Console.WriteLine($"CAN\'T FETCH VIDEO PLAYLISTS!");
|
||||
} else{
|
||||
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
||||
var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
|
||||
var matchedUrl = match.Success ? match.Value : null;
|
||||
//Parse MPD Playlists
|
||||
var crLocal = "";
|
||||
if (pbData.Meta != null){
|
||||
crLocal = pbData.Meta.AudioLocale.CrLocale;
|
||||
// if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
||||
// var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
|
||||
// var matchedUrl = match.Success ? match.Value : null;
|
||||
// //Parse MPD Playlists
|
||||
// var crLocal = "";
|
||||
// if (pbData.Meta != null){
|
||||
// crLocal = pbData.Meta.AudioLocale.CrLocale;
|
||||
// }
|
||||
//
|
||||
// MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl);
|
||||
//
|
||||
// List<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;
|
||||
|
||||
if (streamServers.Count == 0){
|
||||
|
|
@ -1253,8 +1246,11 @@ public class CrunchyrollManager{
|
|||
options.StreamServer = 1;
|
||||
}
|
||||
|
||||
string selectedServer = streamServers[options.StreamServer - 1];
|
||||
ServerData selectedList = streamPlaylists.Data[selectedServer];
|
||||
// string selectedServer = streamServers[options.StreamServer - 1];
|
||||
// ServerData selectedList = streamPlaylists.Data[selectedServer];
|
||||
|
||||
string selectedServer = streamServers.ToList()[options.StreamServer - 1];
|
||||
ServerData selectedList = playListData[selectedServer];
|
||||
|
||||
var videos = selectedList.video.Select(item => new VideoItem{
|
||||
segments = item.segments,
|
||||
|
|
@ -1273,8 +1269,20 @@ public class CrunchyrollManager{
|
|||
resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s"
|
||||
}).ToList();
|
||||
|
||||
videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width));
|
||||
audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
|
||||
// Video: Remove duplicates by resolution (width, height), keep highest bandwidth, then sort
|
||||
videos = videos
|
||||
.GroupBy(v => new{ v.quality.width, v.quality.height })
|
||||
.Select(g => g.OrderByDescending(v => v.bandwidth).First())
|
||||
.OrderBy(v => v.quality.width)
|
||||
.ThenBy(v => v.bandwidth)
|
||||
.ToList();
|
||||
|
||||
// Audio: Remove duplicates, then sort by bandwidth
|
||||
audios = audios
|
||||
.GroupBy(a => new{ a.bandwidth, a.language }) // Add more properties if needed
|
||||
.Select(g => g.First())
|
||||
.OrderBy(a => a.bandwidth)
|
||||
.ToList();
|
||||
|
||||
if (string.IsNullOrEmpty(data.VideoQuality)){
|
||||
Console.Error.WriteLine("Warning: VideoQuality is null or empty. Defaulting to 'best' quality.");
|
||||
|
|
@ -1478,9 +1486,11 @@ public class CrunchyrollManager{
|
|||
};
|
||||
}
|
||||
|
||||
await CrAuth.RefreshToken(true);
|
||||
|
||||
Dictionary<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);
|
||||
|
||||
if (encryptionKeys.Count == 0){
|
||||
|
|
@ -1494,25 +1504,46 @@ public class CrunchyrollManager{
|
|||
};
|
||||
}
|
||||
|
||||
if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
|
||||
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
|
||||
var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
|
||||
List<ContentKey> encryptionKeysAudio =[];
|
||||
if (!string.IsNullOrEmpty(chosenVideoSegments.pssh) && !chosenVideoSegments.pssh.Equals(chosenAudioSegments.pssh)){
|
||||
Console.WriteLine("Video and Audio PSSH different requesting Audio encryption keys");
|
||||
encryptionKeysAudio = await _widevine.getKeys(chosenAudioSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
||||
if (encryptionKeysAudio.Count == 0){
|
||||
Console.Error.WriteLine("Failed to get audio encryption keys");
|
||||
dlFailed = true;
|
||||
return new DownloadResponse{
|
||||
Data = files,
|
||||
Error = dlFailed,
|
||||
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
||||
ErrorText = "Couldn't get DRM audio encryption keys"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//mp4decrypt
|
||||
var commandBase = $"--show-progress --key {keyId}:{key}";
|
||||
if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
|
||||
var tempTsFileName = Path.GetFileName(tempTsFile);
|
||||
var tempTsFileWorkDir = Path.GetDirectoryName(tempTsFile) ?? CfgManager.PathVIDEOS_DIR;
|
||||
var commandVideo = commandBase + $" \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\"";
|
||||
var commandAudio = commandBase + $" \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\"";
|
||||
|
||||
// Use audio keys if available, otherwise fallback to video keys
|
||||
var audioKeysToUse = encryptionKeysAudio.Count > 0 ? encryptionKeysAudio : encryptionKeys;
|
||||
|
||||
// === mp4decrypt command ===
|
||||
var videoKey = encryptionKeys[0];
|
||||
var videoKeyParam = BuildMp4DecryptKeyParam(videoKey.KeyID, videoKey.Bytes);
|
||||
var commandVideo = $"--show-progress {videoKeyParam} \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\"";
|
||||
|
||||
var audioKey = audioKeysToUse[0];
|
||||
var audioKeyParam = BuildMp4DecryptKeyParam(audioKey.KeyID, audioKey.Bytes);
|
||||
var commandAudio = $"--show-progress {audioKeyParam} \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\"";
|
||||
|
||||
bool shaka = Path.Exists(CfgManager.PathShakaPackager);
|
||||
if (shaka){
|
||||
commandBase = " --enable_raw_key_decryption " +
|
||||
string.Join(" ",
|
||||
encryptionKeys.Select(kb =>
|
||||
$"--keys key_id={BitConverter.ToString(kb.KeyID).Replace("-", "").ToLower()}:key={BitConverter.ToString(kb.Bytes).Replace("-", "").ToLower()}"));
|
||||
commandVideo = $"input=\"{tempTsFileName}.video.enc.m4s\",stream=video,output=\"{tempTsFileName}.video.m4s\"" + commandBase;
|
||||
commandAudio = $"input=\"{tempTsFileName}.audio.enc.m4s\",stream=audio,output=\"{tempTsFileName}.audio.m4s\"" + commandBase;
|
||||
// === shaka-packager command ===
|
||||
var shakaVideoKeys = BuildShakaKeysParam(encryptionKeys);
|
||||
commandVideo = $"input=\"{tempTsFileName}.video.enc.m4s\",stream=video,output=\"{tempTsFileName}.video.m4s\" {shakaVideoKeys}";
|
||||
|
||||
var shakaAudioKeys = BuildShakaKeysParam(audioKeysToUse);
|
||||
commandAudio = $"input=\"{tempTsFileName}.audio.enc.m4s\",stream=audio,output=\"{tempTsFileName}.audio.m4s\" {shakaAudioKeys}";
|
||||
}
|
||||
|
||||
if (videoDownloaded){
|
||||
|
|
@ -2085,13 +2116,13 @@ public class CrunchyrollManager{
|
|||
|
||||
#region Fetch Playback Data
|
||||
|
||||
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrDownloadOptions options, string mediaId, string mediaGuidId, bool music){
|
||||
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string streamEndpoint, string mediaId, string mediaGuidId, bool music){
|
||||
var temppbData = new PlaybackData{
|
||||
Total = 0,
|
||||
Data = new Dictionary<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);
|
||||
|
||||
if (!playbackRequestResponse.IsOk){
|
||||
|
|
@ -2119,12 +2150,12 @@ public class CrunchyrollManager{
|
|||
return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent);
|
||||
}
|
||||
|
||||
private async Task<(bool IsOk, string ResponseContent)> SendPlaybackRequestAsync(string endpoint){
|
||||
private async Task<(bool IsOk, string ResponseContent, string error)> SendPlaybackRequestAsync(string endpoint){
|
||||
var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, false, null);
|
||||
return await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
}
|
||||
|
||||
private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string endpoint){
|
||||
private async Task<(bool IsOk, string ResponseContent, string error)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent, string error) response, string endpoint){
|
||||
if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response;
|
||||
|
||||
var error = StreamError.FromJson(response.ResponseContent);
|
||||
|
|
@ -2158,7 +2189,7 @@ public class CrunchyrollManager{
|
|||
foreach (var hardsub in playStream.HardSubs){
|
||||
var stream = hardsub.Value;
|
||||
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
||||
Url = stream.Url,
|
||||
Url = [stream.Url],
|
||||
IsHardsubbed = true,
|
||||
HardsubLocale = stream.Hlang,
|
||||
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
||||
|
|
@ -2167,7 +2198,7 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
||||
Url = playStream.Url,
|
||||
Url = [playStream.Url],
|
||||
IsHardsubbed = false,
|
||||
HardsubLocale = Locale.DefaulT,
|
||||
HardsubLang = Languages.DEFAULT_lang
|
||||
|
|
@ -2322,4 +2353,14 @@ public class CrunchyrollManager{
|
|||
Console.Error.WriteLine("Chapter request failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatKey(byte[] keyBytes) =>
|
||||
BitConverter.ToString(keyBytes).Replace("-", "").ToLower();
|
||||
|
||||
private static string BuildMp4DecryptKeyParam(byte[] keyId, byte[] key) =>
|
||||
$"--key {FormatKey(keyId)}:{FormatKey(key)}";
|
||||
|
||||
private static string BuildShakaKeysParam(List<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]
|
||||
private ComboBoxItem _selectedStreamEndpoint;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedStreamEndpointSecondary;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedDefaultDubLang;
|
||||
|
|
@ -202,13 +205,28 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
new(){ Content = "console/ps5" },
|
||||
new(){ Content = "console/xbox_one" },
|
||||
new(){ Content = "web/edge" },
|
||||
// new (){ Content = "web/safari" },
|
||||
new(){ Content = "web/chrome" },
|
||||
new(){ Content = "web/fallback" },
|
||||
// new (){ Content = "ios/iphone" },
|
||||
// new (){ Content = "ios/ipad" },
|
||||
new(){ Content = "android/phone" },
|
||||
new(){ Content = "tv/samsung" }
|
||||
new(){ Content = "android/tablet" },
|
||||
new(){ Content = "tv/samsung" },
|
||||
new(){ Content = "tv/vidaa" }
|
||||
];
|
||||
|
||||
public ObservableCollection<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; } =[];
|
||||
|
|
@ -282,6 +300,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
|
||||
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
||||
|
||||
ComboBoxItem? streamEndpointSecondary = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondary ?? "")) ?? null;
|
||||
SelectedStreamEndpointSecondary = streamEndpointSecondary ?? StreamEndpointsSecondary[0];
|
||||
|
||||
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||
|
||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
||||
|
|
@ -424,6 +445,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
|
||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondary = SelectedStreamEndpointSecondary.Content + "";
|
||||
|
||||
List<string> dubLangs = new List<string>();
|
||||
foreach (var listBoxItem in SelectedDubLang){
|
||||
|
|
|
|||
|
|
@ -233,8 +233,16 @@
|
|||
</ComboBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</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.Footer>
|
||||
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
|
||||
|
|
|
|||
|
|
@ -614,7 +614,7 @@ public class History{
|
|||
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
public async Task MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){
|
||||
public async Task MatchHistoryEpisodesWithSonarr(bool rematchAll, HistorySeries historySeries){
|
||||
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
|
||||
return;
|
||||
}
|
||||
|
|
@ -630,22 +630,38 @@ public class History{
|
|||
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
|
||||
}
|
||||
|
||||
if (!rematchAll){
|
||||
var historyEpisodesWithSonarrIds = allHistoryEpisodes
|
||||
.Where(e => !string.IsNullOrEmpty(e.SonarrEpisodeId))
|
||||
.ToList();
|
||||
|
||||
Parallel.ForEach(historyEpisodesWithSonarrIds, historyEpisode => {
|
||||
var sonarrEpisode = episodes.FirstOrDefault(e => e.Id.ToString().Equals(historyEpisode.SonarrEpisodeId));
|
||||
|
||||
if (sonarrEpisode != null){
|
||||
historyEpisode.AssignSonarrEpisodeData(sonarrEpisode);
|
||||
}
|
||||
});
|
||||
|
||||
var historyEpisodeIds = new HashSet<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 =[];
|
||||
|
||||
Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
|
||||
if (updateAll || string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
|
||||
if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
|
||||
// Create a copy of the episodes list for each thread
|
||||
var episodesCopy = new List<SonarrEpisode>(episodes);
|
||||
|
||||
var episode = FindClosestMatchEpisodes(episodesCopy, historyEpisode.EpisodeTitle ?? string.Empty);
|
||||
if (episode != null){
|
||||
historyEpisode.SonarrEpisodeId = episode.Id + "";
|
||||
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
|
||||
historyEpisode.SonarrHasFile = episode.HasFile;
|
||||
historyEpisode.SonarrIsMonitored = episode.Monitored;
|
||||
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
|
||||
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
|
||||
|
||||
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||
lock (_lock){
|
||||
episodes.Remove(episode);
|
||||
}
|
||||
|
|
@ -669,12 +685,8 @@ public class History{
|
|||
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
|
||||
});
|
||||
if (episode != null){
|
||||
historyEpisode.SonarrEpisodeId = episode.Id + "";
|
||||
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
|
||||
historyEpisode.SonarrHasFile = episode.HasFile;
|
||||
historyEpisode.SonarrIsMonitored = episode.Monitored;
|
||||
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
|
||||
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
|
||||
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||
|
||||
lock (_lock){
|
||||
episodes.Remove(episode);
|
||||
}
|
||||
|
|
@ -688,12 +700,8 @@ public class History{
|
|||
});
|
||||
|
||||
if (episode1 != null){
|
||||
historyEpisode.SonarrEpisodeId = episode1.Id + "";
|
||||
historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + "";
|
||||
historyEpisode.SonarrHasFile = episode1.HasFile;
|
||||
historyEpisode.SonarrIsMonitored = episode1.Monitored;
|
||||
historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + "";
|
||||
historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + "";
|
||||
historyEpisode.AssignSonarrEpisodeData(episode1);
|
||||
|
||||
lock (_lock){
|
||||
episodes.Remove(episode1);
|
||||
}
|
||||
|
|
@ -706,12 +714,8 @@ public class History{
|
|||
return ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode;
|
||||
});
|
||||
if (episode2 != null){
|
||||
historyEpisode.SonarrEpisodeId = episode2.Id + "";
|
||||
historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + "";
|
||||
historyEpisode.SonarrHasFile = episode2.HasFile;
|
||||
historyEpisode.SonarrIsMonitored = episode2.Monitored;
|
||||
historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + "";
|
||||
historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
|
||||
historyEpisode.AssignSonarrEpisodeData(episode2);
|
||||
|
||||
lock (_lock){
|
||||
episodes.Remove(episode2);
|
||||
}
|
||||
|
|
|
|||
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(){
|
||||
dynamic licenseRequest;
|
||||
|
||||
var random = new Random();
|
||||
uint keyControlNonceId = (uint)(random.NextDouble() * Math.Pow(2, 31));
|
||||
|
||||
if (InitData is WidevineCencHeader){
|
||||
licenseRequest = new SignedLicenseRequest{
|
||||
Type = SignedLicenseRequest.MessageType.LicenseRequest,
|
||||
Msg = new LicenseRequest{
|
||||
Type = LicenseRequest.RequestType.New,
|
||||
KeyControlNonce = 1093602366,
|
||||
KeyControlNonce = keyControlNonceId,
|
||||
ProtocolVersion = ProtocolVersion.Current,
|
||||
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
|
||||
ContentId = new LicenseRequest.ContentIdentification{
|
||||
|
|
@ -102,7 +105,7 @@ public class Session{
|
|||
Type = SignedLicenseRequestRaw.MessageType.LicenseRequest,
|
||||
Msg = new LicenseRequestRaw{
|
||||
Type = LicenseRequestRaw.RequestType.New,
|
||||
KeyControlNonce = 1093602366,
|
||||
KeyControlNonce = keyControlNonceId,
|
||||
ProtocolVersion = ProtocolVersion.Current,
|
||||
RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture).Split('.')[0]),
|
||||
ContentId = new LicenseRequestRaw.ContentIdentification{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.IsolatedStorage;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -111,7 +112,28 @@ public class Widevine{
|
|||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
||||
playbackRequest2.Content = content;
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);
|
||||
// var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);
|
||||
|
||||
var response = (IsOk: false, ResponseContent: "", error: "");
|
||||
for (var attempt = 0; attempt < 3 + 1; attempt++){
|
||||
using (var request = Helpers.CloneHttpRequestMessage(playbackRequest2)){
|
||||
response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (response.IsOk){
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.error.Contains("System.Net.Sockets.SocketException (10054)")){
|
||||
Console.Error.WriteLine($"Key Request Attempt {attempt + 1} failed.");
|
||||
if (attempt == 3)
|
||||
break;
|
||||
|
||||
await Task.Delay(1000);
|
||||
} else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Failed to get Keys!");
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ namespace CRD.Utils.Files;
|
|||
|
||||
public class CfgManager{
|
||||
private static string workingDirectory = AppContext.BaseDirectory;
|
||||
|
||||
public static readonly string PathCrTokenOld = Path.Combine(workingDirectory, "config", "cr_token.yml");
|
||||
public static readonly string PathCrDownloadOptionsOld = Path.Combine(workingDirectory, "config", "settings.yml");
|
||||
|
||||
|
||||
public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
|
||||
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
||||
|
||||
|
|
@ -182,73 +179,6 @@ public class CfgManager{
|
|||
return properties;
|
||||
}
|
||||
|
||||
|
||||
#region YAML OLD
|
||||
|
||||
public static void UpdateSettingsFromFileYAML(CrDownloadOptionsYaml options){
|
||||
string dirPath = Path.GetDirectoryName(PathCrDownloadOptionsOld) ?? string.Empty;
|
||||
|
||||
if (!Directory.Exists(dirPath)){
|
||||
Directory.CreateDirectory(dirPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(PathCrDownloadOptionsOld)){
|
||||
using (var fileStream = File.Create(PathCrDownloadOptionsOld)){
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var input = File.ReadAllText(PathCrDownloadOptionsOld);
|
||||
|
||||
if (input.Length <= 0){
|
||||
return;
|
||||
}
|
||||
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
|
||||
var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input);
|
||||
var loadedOptions = deserializer.Deserialize<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(){
|
||||
if (!CrunchyrollManager.Instance.CrunOptions.History){
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
|
@ -16,6 +17,7 @@ using Avalonia.Media.Imaging;
|
|||
using CRD.Downloader;
|
||||
using CRD.Utils.Ffmpeg_Encoding;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.HLS;
|
||||
using CRD.Utils.JsonConv;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll;
|
||||
|
|
@ -39,6 +41,22 @@ public class Helpers{
|
|||
return default;
|
||||
}
|
||||
|
||||
public static HttpRequestMessage CloneHttpRequestMessage(HttpRequestMessage originalRequest){
|
||||
var clone = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri){
|
||||
Content = originalRequest.Content?.Clone(),
|
||||
Version = originalRequest.Version
|
||||
};
|
||||
foreach (var header in originalRequest.Headers){
|
||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
foreach (var property in originalRequest.Properties){
|
||||
clone.Properties.Add(property);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public static T DeepCopy<T>(T obj){
|
||||
var settings = new JsonSerializerSettings{
|
||||
ContractResolver = new DefaultContractResolver{
|
||||
|
|
@ -626,7 +644,7 @@ public class Helpers{
|
|||
group.Add(descriptionMedia[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return languageGroups;
|
||||
}
|
||||
|
||||
|
|
@ -765,94 +783,27 @@ public class Helpers{
|
|||
}
|
||||
}
|
||||
|
||||
public static CrDownloadOptions MigrateSettings(CrDownloadOptionsYaml yaml){
|
||||
if (yaml == null){
|
||||
throw new ArgumentNullException(nameof(yaml));
|
||||
public static void MergePlaylistData(
|
||||
Dictionary<string, ServerData> target,
|
||||
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);
|
||||
}
|
||||
|
||||
public async Task<(bool IsOk, string ResponseContent)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false){
|
||||
public async Task<(bool IsOk, string ResponseContent,string error)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false){
|
||||
string content = string.Empty;
|
||||
try{
|
||||
AttachCookies(request);
|
||||
|
|
@ -178,14 +178,14 @@ public class HttpClientReq{
|
|||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return (IsOk: true, ResponseContent: content);
|
||||
return (IsOk: true, ResponseContent: content,error:"");
|
||||
} catch (Exception e){
|
||||
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||
if (!suppressError){
|
||||
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||
}
|
||||
|
||||
return (IsOk: false, ResponseContent: content);
|
||||
return (IsOk: false, ResponseContent: content,error: e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ public class MPDParsed{
|
|||
}
|
||||
|
||||
public class ServerData{
|
||||
public List<AudioPlaylist> audio{ get; set; }
|
||||
public List<VideoPlaylist> video{ get; set; }
|
||||
public List<AudioPlaylist> audio{ get; set; } =[];
|
||||
public List<VideoPlaylist> video{ get; set; } =[];
|
||||
}
|
||||
|
||||
public static class MPDParser{
|
||||
|
|
|
|||
|
|
@ -274,261 +274,12 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("stream_endpoint")]
|
||||
public string? StreamEndpoint{ get; set; }
|
||||
|
||||
[JsonProperty("stream_endpoint_secondary")]
|
||||
public string? StreamEndpointSecondary { get; set; }
|
||||
|
||||
[JsonProperty("search_fetch_featured_music")]
|
||||
public bool SearchFetchFeaturedMusic{ get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class CrDownloadOptionsYaml{
|
||||
#region General Settings
|
||||
|
||||
[YamlMember(Alias = "auto_download", ApplyNamingConventions = false)]
|
||||
public bool AutoDownload{ get; set; }
|
||||
|
||||
[YamlMember(Alias = "remove_finished_downloads", ApplyNamingConventions = false)]
|
||||
public bool RemoveFinishedDownload{ get; set; }
|
||||
|
||||
[YamlIgnore]
|
||||
public int Timeout{ get; set; }
|
||||
|
||||
[YamlIgnore]
|
||||
public int FsRetryTime{ get; set; }
|
||||
|
||||
[YamlIgnore]
|
||||
public string Force{ get; set; } = "";
|
||||
|
||||
[YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)]
|
||||
public int SimultaneousDownloads{ get; set; }
|
||||
|
||||
[YamlMember(Alias = "theme", ApplyNamingConventions = false)]
|
||||
public string Theme{ get; set; } = "";
|
||||
|
||||
[YamlMember(Alias = "accent_color", ApplyNamingConventions = false)]
|
||||
public string? AccentColor{ get; set; }
|
||||
|
||||
[YamlMember(Alias = "background_image_path", ApplyNamingConventions = false)]
|
||||
public string? BackgroundImagePath{ get; set; }
|
||||
|
||||
[YamlMember(Alias = "background_image_opacity", ApplyNamingConventions = false)]
|
||||
public double BackgroundImageOpacity{ get; set; }
|
||||
|
||||
[YamlMember(Alias = "background_image_blur_radius", ApplyNamingConventions = false)]
|
||||
public double BackgroundImageBlurRadius{ get; set; }
|
||||
|
||||
|
||||
[YamlIgnore]
|
||||
public List<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
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ public class StreamDetails{
|
|||
[JsonProperty("hardsub_locale")]
|
||||
public Locale? HardsubLocale{ get; set; }
|
||||
|
||||
public string? Url{ get; set; }
|
||||
public List<string?> Url{ get; set; }
|
||||
|
||||
[JsonProperty("hardsub_lang")]
|
||||
public required LanguageItem HardsubLang{ get; set; }
|
||||
|
|
@ -28,7 +28,7 @@ public class StreamDetails{
|
|||
|
||||
public class StreamDetailsPop{
|
||||
public Locale? HardsubLocale{ get; set; }
|
||||
public string? Url{ get; set; }
|
||||
public List<string?> Url{ get; set; }
|
||||
public required LanguageItem HardsubLang{ get; set; }
|
||||
|
||||
public bool IsHardsubbed{ get; set; }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Sonarr.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CRD.Utils.Structs.History;
|
||||
|
|
@ -58,6 +59,18 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
[JsonProperty("sonarr_absolut_number")]
|
||||
public string? SonarrAbsolutNumber{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SonarrSeasonEpisodeText{
|
||||
get{
|
||||
if (int.TryParse(SonarrSeasonNumber, out int season) &&
|
||||
int.TryParse(SonarrEpisodeNumber, out int episode)){
|
||||
return $"S{season:D2}E{episode:D2}";
|
||||
}
|
||||
|
||||
return $"S{SonarrSeasonNumber}E{SonarrEpisodeNumber}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("history_episode_available_soft_subs")]
|
||||
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
|
||||
|
||||
|
|
@ -118,7 +131,16 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void AssignSonarrEpisodeData(SonarrEpisode episode) {
|
||||
SonarrEpisodeId = episode.Id.ToString();
|
||||
SonarrEpisodeNumber = episode.EpisodeNumber.ToString();
|
||||
SonarrHasFile = episode.HasFile;
|
||||
SonarrIsMonitored = episode.Monitored;
|
||||
SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber.ToString();
|
||||
SonarrSeasonNumber = episode.SeasonNumber.ToString();
|
||||
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText)));
|
||||
}
|
||||
}
|
||||
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(){
|
||||
var match = Regex.Match(UrlInput, "/([^/]+)/(?:artist|watch|series)(?:/(?:musicvideo|concert))?/([^/]+)/?");
|
||||
return match.Success ? (match.Groups[1].Value, match.Groups[2].Value) : null;
|
||||
var match = Regex.Match(UrlInput, @"^(?:https?:\/\/[^/]+)?(?:\/([a-z]{2}))?\/(?:[^/]+\/)?(artist|watch|series)(?:\/(musicvideo|concert))?\/([^/]+)(?:\/[^/]*)?$");
|
||||
|
||||
return match.Success
|
||||
? (match.Groups[1].Value ?? "", match.Groups[4].Value)
|
||||
: null;
|
||||
}
|
||||
|
||||
private CrunchyUrlType GetUrlType(){
|
||||
|
|
@ -341,12 +344,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
||||
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true);
|
||||
|
||||
|
||||
if (musicList != null){
|
||||
currentMusicVideoList = musicList;
|
||||
PopulateItemsFromMusicVideoList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SetLoadingState(false);
|
||||
|
|
@ -477,7 +479,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
|
||||
|
||||
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
|
||||
|
||||
if (CurrentSeasonFullySelected){
|
||||
|
|
@ -581,18 +582,17 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
episodesBySeason[seasonKey].Add(episodeModel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
||||
var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
|
||||
? CrunchyrollManager.Instance.DefaultLocale
|
||||
: CrunchyrollManager.Instance.CrunOptions.HistoryLang;
|
||||
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(seriesId, DetermineLocale(locale), true);
|
||||
|
||||
|
||||
if (musicList != null){
|
||||
currentMusicVideoList = musicList;
|
||||
PopulateItemsFromMusicVideoList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using DynamicData;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -11,7 +10,6 @@ using CommunityToolkit.Mvvm.Input;
|
|||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.CustomList;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private static string _progressText;
|
||||
|
||||
public Vector LastScrollOffset { get; set; } = Vector.Zero;
|
||||
|
||||
public HistoryPageViewModel(){
|
||||
ProgramManager = ProgramManager.Instance;
|
||||
|
|
@ -343,7 +345,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
NavToSeries();
|
||||
|
||||
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
|
||||
if (SelectedSeries != null) _ = CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
|
||||
if (SelectedSeries != null) _ = CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(false, SelectedSeries);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform.Storage;
|
||||
|
|
@ -8,10 +7,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
using CRD.Utils.UI;
|
||||
using CRD.ViewModels.Utils;
|
||||
using CRD.Views;
|
||||
using CRD.Views.Utils;
|
||||
|
|
@ -133,6 +132,42 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task MatchSonarrEpisode_Button(HistoryEpisode episode){
|
||||
var dialog = new CustomContentDialog(){
|
||||
Name = "CustomDialog",
|
||||
Title = "Sonarr Episode Matching",
|
||||
PrimaryButtonText = "Save",
|
||||
CloseButtonText = "Close",
|
||||
FullSizeDesired = true,
|
||||
};
|
||||
|
||||
var viewModel = new ContentDialogSonarrMatchEpisodeViewModel(dialog, SelectedSeries, episode);
|
||||
dialog.Content = new ContentDialogSonarrMatchEpisodeView(){
|
||||
DataContext = viewModel
|
||||
};
|
||||
|
||||
var dialogResult = await dialog.ShowAsync();
|
||||
|
||||
if (dialogResult == ContentDialogResult.Primary){
|
||||
var sonarrEpisode = viewModel.CurrentSonarrEpisode;
|
||||
|
||||
foreach (var selectedSeriesSeason in SelectedSeries.Seasons){
|
||||
foreach (var historyEpisode in selectedSeriesSeason.EpisodesList.Where(historyEpisode => historyEpisode.SonarrEpisodeId == sonarrEpisode.Id.ToString())){
|
||||
historyEpisode.SonarrEpisodeId = string.Empty;
|
||||
historyEpisode.SonarrAbsolutNumber = string.Empty;
|
||||
historyEpisode.SonarrSeasonNumber = string.Empty;
|
||||
historyEpisode.SonarrEpisodeNumber = string.Empty;
|
||||
historyEpisode.SonarrHasFile = false;
|
||||
historyEpisode.SonarrIsMonitored = false;
|
||||
}
|
||||
}
|
||||
|
||||
episode.AssignSonarrEpisodeData(sonarrEpisode);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public async Task DownloadSeasonAll(HistorySeason season){
|
||||
|
|
@ -173,6 +208,12 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshSonarrEpisodeMatch(){
|
||||
await CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task UpdateData(string? season){
|
||||
var result = await SelectedSeries.FetchData(season);
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ using CRD.Views.Utils;
|
|||
using FluentAvalonia.UI.Controls;
|
||||
using Image = Avalonia.Controls.Image;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace CRD.ViewModels;
|
||||
|
||||
public class SettingsPageViewModel : ViewModelBase{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Updater;
|
||||
using Markdig;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using Inline = Markdig.Syntax.Inlines.Inline;
|
||||
|
||||
namespace CRD.ViewModels;
|
||||
|
||||
public partial class UpdateViewModel : ViewModelBase{
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _updateAvailable;
|
||||
|
||||
|
|
@ -27,24 +36,22 @@ public partial class UpdateViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _failed;
|
||||
|
||||
private AccountPageViewModel accountPageViewModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _changelogText = "<p><strong>No changelog found.</strong></p>";
|
||||
private AccountPageViewModel accountPageViewModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _currentVersion;
|
||||
|
||||
public ObservableCollection<Control> ChangelogBlocks{ get; } = new();
|
||||
|
||||
public UpdateViewModel(){
|
||||
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
|
||||
|
||||
|
||||
LoadChangelog();
|
||||
|
||||
|
||||
UpdateAvailable = ProgramManager.Instance.UpdateAvailable;
|
||||
|
||||
|
||||
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +59,6 @@ public partial class UpdateViewModel : ViewModelBase{
|
|||
public void StartUpdate(){
|
||||
Updating = true;
|
||||
ProgramManager.Instance.NavigationLock = true;
|
||||
// Title = "Updating";
|
||||
_ = Updater.Instance.DownloadAndUpdateAsync();
|
||||
}
|
||||
|
||||
|
|
@ -65,11 +71,20 @@ public partial class UpdateViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
private void LoadChangelog(){
|
||||
#region Changelog Builder
|
||||
|
||||
private int textSize = 16;
|
||||
|
||||
public void LoadChangelog(){
|
||||
string changelogPath = "CHANGELOG.md";
|
||||
|
||||
if (!File.Exists(changelogPath)){
|
||||
ChangelogText = "<p><strong>No changelog found.</strong></p>";
|
||||
ChangelogBlocks.Clear();
|
||||
ChangelogBlocks.Add(new TextBlock{
|
||||
Text = "No changelog found",
|
||||
FontSize = 16,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -77,104 +92,206 @@ public partial class UpdateViewModel : ViewModelBase{
|
|||
|
||||
markdownText = PreprocessMarkdown(markdownText);
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.UseSoftlineBreakAsHardlineBreak()
|
||||
.Build();
|
||||
var pipeline = new MarkdownPipelineBuilder().Build();
|
||||
var document = Markdown.Parse(markdownText, pipeline);
|
||||
|
||||
string htmlContent = Markdown.ToHtml(markdownText, pipeline);
|
||||
ChangelogBlocks.Clear();
|
||||
|
||||
htmlContent = MakeIssueLinksClickable(htmlContent);
|
||||
htmlContent = ModifyImages(htmlContent);
|
||||
|
||||
Color themeTextColor = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark ? Colors.White : Color.Parse("#E4000000");
|
||||
string cssColor = $"#{themeTextColor.R:X2}{themeTextColor.G:X2}{themeTextColor.B:X2}";
|
||||
try{
|
||||
foreach (var block in document){
|
||||
switch (block){
|
||||
case HeadingBlock heading:
|
||||
string headingText = string.Concat(heading.Inline.Select(i => i.ToString()));
|
||||
ChangelogBlocks.Add(new TextBlock{
|
||||
Text = headingText,
|
||||
FontSize = heading.Level switch{ 1 => textSize + 10, 2 => textSize + 6, _ => textSize + 4 },
|
||||
FontWeight = FontWeight.Bold,
|
||||
Margin = new Thickness(0, 20, 0, 5),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
break;
|
||||
|
||||
string styledHtml = $@"
|
||||
<html>
|
||||
<head>
|
||||
<style type=""text/css"">
|
||||
body {{
|
||||
color: {cssColor};
|
||||
background: transparent;
|
||||
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>";
|
||||
case ParagraphBlock paragraph:
|
||||
var inlineControls = BuildInlineControls(paragraph.Inline?.FirstChild);
|
||||
var container = new WrapPanel{
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
foreach (var ctrl in inlineControls)
|
||||
container.Children.Add(ctrl);
|
||||
|
||||
ChangelogText = styledHtml;
|
||||
ChangelogBlocks.Add(container);
|
||||
break;
|
||||
|
||||
case ListBlock list:
|
||||
foreach (ListItemBlock item in list){
|
||||
foreach (var blocki in item){
|
||||
if (blocki is ParagraphBlock para){
|
||||
var container1 = new WrapPanel{ Margin = new Thickness(10, 2, 0, 2) };
|
||||
container1.Children.Add(new TextBlock{ Text = "• ", FontWeight = FontWeight.Bold, FontSize = textSize});
|
||||
|
||||
foreach (var ctrl in BuildInlineControls(para.Inline?.FirstChild))
|
||||
container1.Children.Add(ctrl);
|
||||
|
||||
ChangelogBlocks.Add(container1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
private string MakeIssueLinksClickable(string htmlContent){
|
||||
// Match GitHub issue links
|
||||
string issuePattern = @"<a href=['""](https:\/\/github\.com\/Crunchy-DL\/Crunchy-Downloader\/issues\/(\d+))['""][^>]*>[^<]+<\/a>";
|
||||
IEnumerable<Control> BuildInlineControls(Inline? inline){
|
||||
var controls = new List<Control>();
|
||||
var urlRegex = new Regex(@"https?://[^\s]+", RegexOptions.Compiled);
|
||||
var githubRefRegex = new Regex(
|
||||
@"https://github\.com/[^/]+/[^/]+/(issues|discussions)/(\d+)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// Match GitHub discussion links
|
||||
string discussionPattern = @"<a href=['""](https:\/\/github\.com\/Crunchy-DL\/Crunchy-Downloader\/discussions\/(\d+))['""][^>]*>[^<]+<\/a>";
|
||||
while (inline != null){
|
||||
switch (inline){
|
||||
case LiteralInline lit:
|
||||
var text = lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length);
|
||||
|
||||
htmlContent = Regex.Replace(htmlContent, issuePattern, match => {
|
||||
string fullUrl = match.Groups[1].Value;
|
||||
string issueNumber = match.Groups[2].Value;
|
||||
return $"<a href='{fullUrl}' target='_blank'>#{issueNumber}</a>";
|
||||
});
|
||||
var lastIndex = 0;
|
||||
foreach (Match match in urlRegex.Matches(text)){
|
||||
if (match.Index > lastIndex){
|
||||
controls.Add(new TextBlock{
|
||||
Text = text.Substring(lastIndex, match.Index - lastIndex),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = textSize
|
||||
});
|
||||
}
|
||||
|
||||
htmlContent = Regex.Replace(htmlContent, discussionPattern, match => {
|
||||
string fullUrl = match.Groups[1].Value;
|
||||
string discussionNumber = match.Groups[2].Value;
|
||||
return $"<a href='{fullUrl}' target='_blank'>#{discussionNumber}</a>";
|
||||
});
|
||||
string url = match.Value;
|
||||
string buttonText = url;
|
||||
|
||||
return htmlContent;
|
||||
var ghMatch = githubRefRegex.Match(url);
|
||||
if (ghMatch.Success && ghMatch.Groups.Count > 2){
|
||||
buttonText = $"#{ghMatch.Groups[2].Value}";
|
||||
}
|
||||
|
||||
controls.Add(CreateLinkButton(buttonText, url));
|
||||
lastIndex = match.Index + match.Length;
|
||||
}
|
||||
|
||||
if (lastIndex < text.Length){
|
||||
controls.Add(new TextBlock{
|
||||
Text = text.Substring(lastIndex),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = textSize
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EmphasisInline emph:
|
||||
var emphControls = BuildInlineControls(emph.FirstChild);
|
||||
foreach (var ec in emphControls){
|
||||
if (ec is TextBlock tb){
|
||||
tb.FontWeight = emph.DelimiterChar == '*' ? FontWeight.Bold : FontWeight.Normal;
|
||||
tb.FontStyle = emph.DelimiterChar == '_' ? FontStyle.Italic : FontStyle.Normal;
|
||||
}
|
||||
|
||||
controls.Add(ec);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LinkInline link:
|
||||
var linkText = ConvertInlinesToText(link.FirstChild);
|
||||
controls.Add(CreateLinkButton(linkText, link.Url));
|
||||
break;
|
||||
}
|
||||
|
||||
inline = inline.NextSibling;
|
||||
}
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
|
||||
private string ModifyImages(string htmlContent){
|
||||
// Regex to match <img> tags
|
||||
string imgPattern = @"<img\s+src=['""]([^'""]+)['""]( alt=['""]([^'""]+)['""])?\s*\/?>";
|
||||
string ConvertInlinesToText(Inline? inline){
|
||||
var result = new StringBuilder();
|
||||
|
||||
return Regex.Replace(htmlContent, imgPattern, match => {
|
||||
string imgUrl = match.Groups[1].Value;
|
||||
string altText = "View Image"; // match.Groups[3].Success ? match.Groups[3].Value : "View Image";
|
||||
while (inline != null){
|
||||
switch (inline){
|
||||
case LiteralInline lit:
|
||||
result.Append(lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length));
|
||||
break;
|
||||
|
||||
return $"<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){
|
||||
// Regex to match <details> blocks containing an image
|
||||
string detailsPattern = @"<details>\s*<summary>.*?<\/summary>\s*<img\s+src=['""]([^'""]+)['""]\s+alt=['""]([^'""]+)['""]\s*\/?>\s*<\/details>";
|
||||
|
||||
return Regex.Replace(markdownText, detailsPattern, match => {
|
||||
|
|
@ -184,5 +301,6 @@ public partial class UpdateViewModel : ViewModelBase{
|
|||
return $"";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ using System.Linq;
|
|||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Ffmpeg_Encoding;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
|
|
|
|||
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"
|
||||
x:DataType="vm:HistoryPageViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.HistoryPageView">
|
||||
x:Class="CRD.Views.HistoryPageView"
|
||||
Unloaded="OnUnloaded"
|
||||
Loaded="Control_OnLoaded">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
|
||||
|
|
@ -235,7 +237,7 @@
|
|||
</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}"
|
||||
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;
|
||||
|
||||
public partial class HistoryPageView : UserControl{
|
||||
|
||||
public HistoryPageView(){
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnUnloaded(object? sender, RoutedEventArgs e){
|
||||
|
||||
if (DataContext is HistoryPageViewModel viewModel){
|
||||
viewModel.LastScrollOffset = SeriesListBox.Scroll?.Offset ?? Vector.Zero;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void Control_OnLoaded(object? sender, RoutedEventArgs e){
|
||||
if (DataContext is HistoryPageViewModel viewModel){
|
||||
if (SeriesListBox.Scroll != null) SeriesListBox.Scroll.Offset = viewModel.LastScrollOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,7 +152,8 @@
|
|||
|
||||
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding SelectedSeries.IsInactive}" Command="{Binding ToggleInactive}">
|
||||
IsChecked="{Binding SelectedSeries.IsInactive}"
|
||||
Command="{Binding ToggleInactive}">
|
||||
<ToolTip.Tip>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Set Active"
|
||||
|
|
@ -323,12 +324,33 @@
|
|||
</StackPanel>
|
||||
|
||||
<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"
|
||||
IsVisible="{Binding SonarrConnected}"
|
||||
Command="{Binding MatchSonarrSeries_Button}">
|
||||
<Grid>
|
||||
<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>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
|
@ -366,20 +388,21 @@
|
|||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
|
||||
Margin="0 0 5 0 "
|
||||
Symbol="AlertOn"
|
||||
FontSize="18"
|
||||
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
|
||||
Margin="0 0 5 0 "
|
||||
Symbol="AlertOn"
|
||||
FontSize="18"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Episode unavailable — it might not be available on the streaming service" 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>
|
||||
|
||||
</controls:SymbolIcon>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
|
||||
|
||||
<ui:EpisodeHighlightTextBlock
|
||||
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
|
||||
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>
|
||||
|
||||
|
||||
|
||||
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
|
||||
|
||||
|
||||
<TextBlock Text="{Binding SonarrSeasonEpisodeText}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
|
||||
|
||||
|
||||
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ShowMonitoredBookmark}"
|
||||
HorizontalAlignment="Center"
|
||||
|
|
@ -449,13 +476,21 @@
|
|||
</controls:SymbolIcon>
|
||||
</StackPanel>
|
||||
|
||||
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
|
||||
Source="../Assets/sonarr.png" Width="25"
|
||||
Height="25" />
|
||||
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
|
||||
BorderThickness="0" CornerRadius="50"
|
||||
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).MatchSonarrEpisode_Button}"
|
||||
CommandParameter="{Binding }">
|
||||
<StackPanel>
|
||||
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
|
||||
Source="../Assets/sonarr.png" Width="25"
|
||||
Height="25" />
|
||||
|
||||
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
|
||||
Source="../Assets/sonarr_inactive.png" Width="25"
|
||||
Height="25" />
|
||||
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
|
||||
Source="../Assets/sonarr_inactive.png" Width="25"
|
||||
Height="25" />
|
||||
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
|
||||
</StackPanel>
|
||||
|
|
|
|||
|
|
@ -17,19 +17,5 @@ public partial class SettingsPageView : UserControl{
|
|||
SonarrClient.Instance.RefreshSonarr();
|
||||
}
|
||||
}
|
||||
|
||||
private void ListBox_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){
|
||||
var listBox = sender as ListBox;
|
||||
var scrollViewer = listBox?.GetVisualDescendants().OfType<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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:CRD.ViewModels"
|
||||
xmlns:avalonia="clr-namespace:TheArtOfDev.HtmlRenderer.Avalonia;assembly=Avalonia.HtmlRenderer"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
x:DataType="vm:UpdateViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.UpdateView">
|
||||
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
|
|
@ -59,25 +59,37 @@
|
|||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" IsVisible="{Binding !Updating}" MaxWidth="700"
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding !Updating}"
|
||||
Margin="10"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<avalonia:HtmlLabel Name="HtmlContent"
|
||||
Text="{Binding ChangelogText}"
|
||||
Margin="10"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<Border HorizontalAlignment="Center"
|
||||
MaxWidth="800">
|
||||
|
||||
<ItemsControl ItemsSource="{Binding ChangelogBlocks}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
<!-- Update Progress Section -->
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Spacing="10" IsVisible="{Binding Updating}">
|
||||
<TextBlock IsVisible="{Binding !Failed}" Text="Please wait while the update is being downloaded..." 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" Value="{Binding Progress}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="350" />
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Spacing="10" VerticalAlignment="Center" HorizontalAlignment="Center" 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}" FontSize="24" 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" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
</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"
|
||||
x:DataType="vm:ContentDialogSonarrMatchViewModel"
|
||||
x:Class="CRD.Views.Utils.ContentDialogSonarrMatchView">
|
||||
|
||||
<Grid HorizontalAlignment="Stretch" MaxHeight="500" >
|
||||
|
||||
<Grid HorizontalAlignment="Stretch" MaxHeight="600">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<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>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
</Grid.ColumnDefinitions>
|
||||
<!-- 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.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
||||
|
|
@ -37,62 +37,64 @@
|
|||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding CurrentSonarrSeries.Title}" FontWeight="Bold"
|
||||
FontSize="16"
|
||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding CurrentSonarrSeries.Year}" FontStyle="Italic"
|
||||
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}"
|
||||
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
|
||||
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
<!-- <Rectangle Grid.Row="2" Width="1500" Height="0" Fill="Gray" Margin="10,0" /> -->
|
||||
<!-- <TextBlock Grid.Column="0" Grid.Row="2" Text="Series"></TextBlock> -->
|
||||
|
||||
|
||||
<ListBox Grid.Row="3" SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding SonarrSeriesList}">
|
||||
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type models:SonarrSeries}">
|
||||
<StackPanel>
|
||||
<StackPanel Height="220">
|
||||
<Border Padding="10" Margin="5" BorderThickness="1">
|
||||
<Grid Margin="10" VerticalAlignment="Top">
|
||||
<Grid VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Image -->
|
||||
|
||||
<asyncImageLoader:AdvancedImage Grid.Column="0" MaxWidth="120" MaxHeight="180" Source="{Binding ImageUrl}"
|
||||
Stretch="Fill" />
|
||||
|
||||
<!-- Text Content -->
|
||||
<Grid Grid.Column="1" Margin="10" VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- Takes up space as needed for the time -->
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{Binding Title}" FontWeight="Bold"
|
||||
FontSize="16"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Year}" FontStyle="Italic"
|
||||
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}"
|
||||
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Background="LightGray" Height="1" Margin="0,5" HorizontalAlignment="Stretch" />
|
||||
<Border Background="LightGray" Height="1" Margin="0,5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
|
|
|
|||
Loading…
Reference in a new issue