Add - Added Dub & Sub override to history series and seasons

Add - Timing Sync - to sync dubs to the video
Chg - History Lang is now default language for episode title, description...
Chg - Renamed "Fetch Series" to "Refresh Series" to prevent confusion
Fix - Fixed Crash with search when episodes had X.X numbering
This commit is contained in:
Elwador 2024-07-17 01:52:46 +02:00
parent 53158b261d
commit 90ee2221cb
32 changed files with 1345 additions and 338 deletions

View File

@ -1,51 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<ApplicationIcon>Assets\app_icon.ico</ApplicationIcon>
<Version>1.5.2.0</Version>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.0-beta2" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="ExtendedXmlSerializer" Version="3.7.18" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0-preview5" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.60" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="protobuf-net" Version="3.2.30" />
<PackageReference Include="YamlDotNet" Version="15.1.2" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Styling\ControlsGalleryStyles.axaml" />
<UpToDateCheckInput Remove="Styling\ControlThemes.axaml" />
<UpToDateCheckInput Remove="Views\Utils\ErrorWindow.axaml" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\Utils\ContentDialogUpdateView.axaml.cs">
<DependentUpon>ContentDialogInputLoginView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\Utils\ContentDialogInputLoginView.axaml.cs">
<DependentUpon>ContentDialogInputLoginView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@ -68,6 +68,12 @@ public class CrEpisode(){
if (crunInstance.CrunOptions.History && updateHistory){
await crunInstance.CrHistory.UpdateWithEpisode(dlEpisode);
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId);
if (historySeries != null){
Crunchyroll.Instance.CrHistory.MatchHistorySeriesWithSonarr(false);
await Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile();
}
}
var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier) ? dlEpisode.Identifier.Split('|')[1] : $"S{dlEpisode.SeasonNumber}";
@ -102,7 +108,7 @@ public class CrEpisode(){
int epIndex = 1;
var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special).
var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special).
string newKey;
if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){
newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id);
@ -239,7 +245,7 @@ public class CrEpisode(){
return retMeta;
}
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount){
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount , bool forcedLang = false){
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
complete.Data =[];
@ -250,6 +256,9 @@ public class CrEpisode(){
if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale;
if (forcedLang){
query["force_locale"] = crLocale;
}
}
query["start"] = i + "";

View File

@ -56,6 +56,13 @@ public class CrSeries(){
continue;
}
if (crunInstance.CrunOptions.History){
var dubLangList = crunInstance.CrHistory.GetDubList(item.SeriesId, item.SeasonId);
if (dubLangList.Count > 0){
dubLang = dubLangList;
}
}
if (!dubLang.Contains(episode.Langs[index].CrLocale))
continue;
@ -157,7 +164,7 @@ public class CrSeries(){
var seasonData = await GetSeasonDataById(s.Id, "");
if (seasonData.Data != null){
if (crunInstance.CrunOptions.History){
crunInstance.CrHistory.UpdateWithSeasonData(seasonData);
crunInstance.CrHistory.UpdateWithSeasonData(seasonData,false);
}
foreach (var episode in seasonData.Data){
@ -204,6 +211,15 @@ public class CrSeries(){
}
}
if (crunInstance.CrunOptions.History){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == id);
if (historySeries != null){
crunInstance.CrHistory.MatchHistorySeriesWithSonarr(false);
await crunInstance.CrHistory.MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile();
}
}
int specialIndex = 1;
int epIndex = 1;
@ -212,7 +228,7 @@ public class CrSeries(){
foreach (var key in keys){
EpisodeAndLanguage item = episodes[key];
var episode = item.Items[0].Episode;
var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+$"); // Checking if the episode is not a number (i.e., special).
var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special).
// var newKey = $"{(isSpecial ? 'S' : 'E')}{(isSpecial ? specialIndex : epIndex).ToString()}";
string newKey;
@ -431,13 +447,13 @@ public class CrSeries(){
}
public async Task<CrSearchSeriesBase?> Search(string searchString,string? crLocale){
public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale){
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale;
}
query["q"] = searchString;
query["n"] = "6";
query["type"] = "top_results";
@ -452,7 +468,7 @@ public class CrSeries(){
}
CrSearchSeriesBase? series = Helpers.Deserialize<CrSearchSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
return series;
}

View File

@ -81,6 +81,7 @@ public class Crunchyroll{
#endregion
public string DefaultLocale = "en-US";
public JsonSerializerSettings? SettingsJsonSerializerSettings = new(){
@ -120,24 +121,7 @@ public class Crunchyroll{
Queue.CollectionChanged += UpdateItemListOnRemove;
}
public async Task Init(){
_widevine = Widevine.Instance;
CrAuth = new CrAuth();
CrEpisode = new CrEpisode();
CrSeries = new CrSeries();
CrHistory = new History();
Profile = new CrProfile{
Username = "???",
Avatar = "003-cr-hime-excited.png",
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE",
HasPremium = false,
};
Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}");
public void InitOptions(){
CrunOptions.AutoDownload = false;
CrunOptions.RemoveFinishedDownload = false;
CrunOptions.Chapters = true;
@ -164,12 +148,32 @@ public class Crunchyroll{
CrunOptions.DlVideoOnce = true;
CrunOptions.StreamEndpoint = "web/firefox";
CrunOptions.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
CrunOptions.HistoryLang = "";
CrunOptions.HistoryLang = DefaultLocale;
CrunOptions.History = true;
CfgManager.UpdateSettingsFromFile();
_widevine = Widevine.Instance;
CrAuth = new CrAuth();
CrEpisode = new CrEpisode();
CrSeries = new CrSeries();
CrHistory = new History();
Profile = new CrProfile{
Username = "???",
Avatar = "003-cr-hime-excited.png",
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE",
HasPremium = false,
};
Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}");
}
public async Task Init(){
if (CrunOptions.LogMode){
CfgManager.EnableLogMode();
@ -186,10 +190,22 @@ public class Crunchyroll{
if (CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
}
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
if (!string.IsNullOrEmpty(decompressedJson)){
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(decompressedJson) ?? new ObservableCollection<HistorySeries>();
RefreshSonarr();
foreach (var historySeries in HistoryList){
historySeries.Init();
foreach (var historySeriesSeason in historySeries.Seasons){
historySeriesSeason.Init();
}
}
} else{
HistoryList =[];
}
RefreshSonarr();
}
}
}
@ -340,6 +356,18 @@ public class Crunchyroll{
}
var sList = await CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory);
(HistoryEpisode? historyEpisode, List<string> dublist, string downloadDirPath) historyEpisode = (null, [], "");
if (CrunOptions.History){
var episode = sList.EpisodeAndLanguages.Items.First();
historyEpisode = CrHistory.GetHistoryEpisodeWithDubListAndDownloadDir(episode.SeriesId, episode.SeasonId, episode.Id);
if (historyEpisode.dublist.Count > 0){
dubLang = historyEpisode.dublist;
}
}
var selected = CrEpisode.EpisodeMeta(sList, dubLang);
if (CrunOptions.IncludeVideoDescription){
@ -351,7 +379,7 @@ public class Crunchyroll{
if (selected.Data is{ Count: > 0 }){
if (CrunOptions.History){
var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(selected.ShowId, selected.SeasonId, selected.Data.First().MediaId);
// var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(selected.ShowId, selected.SeasonId, selected.Data.First().MediaId);
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
if (historyEpisode.historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){
@ -369,6 +397,7 @@ public class Crunchyroll{
}
}
selected.DownloadSubs = CrunOptions.DlSubs;
Queue.Add(selected);
@ -419,6 +448,7 @@ public class Crunchyroll{
}
}
crunchyEpMeta.DownloadSubs = CrunOptions.DlSubs;
Queue.Add(crunchyEpMeta);
} else{
failed = true;
@ -569,7 +599,8 @@ public class Crunchyroll{
SkipSubMux = options.SkipSubMux,
OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc, Signs = a.Signs }).ToList(),
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc, Signs = a.Signs, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos,
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
@ -584,7 +615,7 @@ public class Crunchyroll{
},
CcTag = options.CcTag,
mp3 = muxToMp3,
Description = data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList(),
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
});
if (!File.Exists(CfgManager.PathFFMPEG)){
@ -597,9 +628,31 @@ public class Crunchyroll{
bool isMuxed;
// if (options.SyncTiming){
// await Merger.CreateDelays();
// }
if (options.SyncTiming && CrunOptions.DlVideoOnce){
var basePath = merger.options.OnlyVid.First().Path;
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
if (!string.IsNullOrEmpty(basePath) && syncVideosList.Count > 0){
foreach (var syncVideo in syncVideosList){
if (!string.IsNullOrEmpty(syncVideo.Path)){
var delay = await merger.ProcessVideo(basePath, syncVideo.Path);
var audio = merger.options.OnlyAudio.FirstOrDefault(audio => audio.Language.CrLocale == syncVideo.Lang.CrLocale);
if (audio != null){
audio.Delay = (int)delay * 1000;
}
var subtitles = merger.options.Subtitles.Where(a => a.RelatedVideoDownloadMedia == syncVideo).ToList();
if (subtitles.Count > 0){
foreach (var subMergerInput in subtitles){
subMergerInput.Delay = (int)delay * 1000;
}
}
}
}
}
syncVideosList.ForEach(syncVideo => Helpers.DeleteFile(syncVideo.Path));
}
if (!options.Mp4 && !muxToMp3){
await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE);
@ -726,8 +779,8 @@ public class Crunchyroll{
if (epMeta.Versions != null){
if (epMeta.Lang != null){
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale);
} else if (options.DubLang.Count == 1){
LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == options.DubLang[0]);
} else if (data.SelectedDubs is{ Count: 1 }){
LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]);
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang.CrLocale);
} else if (epMeta.Versions.Count == 1){
currentVersion = epMeta.Versions[0];
@ -804,7 +857,8 @@ public class Crunchyroll{
var streams = new List<StreamDetailsPop>();
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
variables.Add(new Variable("episode", (int.TryParse(data.EpisodeNumber, out int episodeNum) ? (object)episodeNum : data.AbsolutEpisodeNumberE) ?? string.Empty, false));
variables.Add(new Variable("episode",
(double.TryParse(data.EpisodeNumber, NumberStyles.Any, CultureInfo.InvariantCulture, out double episodeNum) ? (object)Math.Round(episodeNum, 1) : data.AbsolutEpisodeNumberE) ?? string.Empty, false));
variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
variables.Add(new Variable("showTitle", data.SeasonTitle ?? string.Empty, true));
variables.Add(new Variable("season", data.Season != null ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false));
@ -915,6 +969,7 @@ public class Crunchyroll{
}
string tsFile = "";
var videoDownloadMedia = new DownloadedMedia();
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
@ -985,7 +1040,9 @@ public class Crunchyroll{
audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
int chosenVideoQuality;
if (options.QualityVideo == "best"){
if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){
chosenVideoQuality = 1;
} else if (options.QualityVideo == "best"){
chosenVideoQuality = videos.Count;
} else if (options.QualityVideo == "worst"){
chosenVideoQuality = 1;
@ -1075,9 +1132,10 @@ public class Crunchyroll{
.ToArray());
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile);
bool audioDownloaded = false, videoDownloaded = false;
bool audioDownloaded = false, videoDownloaded = false, syncTimingDownload = false;
if (options.DlVideoOnce && dlVideoOnce){
if (options.DlVideoOnce && dlVideoOnce && !options.SyncTiming){
Console.WriteLine("Already downloaded video, skipping video download...");
} else if (options.Novids){
Console.WriteLine("Skipping video download...");
@ -1091,6 +1149,10 @@ public class Crunchyroll{
dlFailed = true;
}
if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){
syncTimingDownload = true;
}
dlVideoOnce = true;
videoDownloaded = true;
}
@ -1256,12 +1318,13 @@ public class Crunchyroll{
Console.WriteLine($"An error occurred: {ex.Message}");
}
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Video,
videoDownloadMedia = new DownloadedMedia{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s",
Lang = lang.Value,
IsPrimary = isPrimary
});
};
files.Add(videoDownloadMedia);
} else{
Console.WriteLine("No Video downloaded");
}
@ -1335,12 +1398,13 @@ public class Crunchyroll{
}
} else{
if (videoDownloaded){
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Video,
videoDownloadMedia = new DownloadedMedia{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s",
Lang = lang.Value,
IsPrimary = isPrimary
});
};
files.Add(videoDownloadMedia);
}
if (audioDownloaded){
@ -1405,8 +1469,8 @@ public class Crunchyroll{
}
}
if (options.DlSubs.IndexOf("all") > -1){
options.DlSubs = new List<string>{ "all" };
if (data.DownloadSubs.IndexOf("all") > -1){
data.DownloadSubs = new List<string>{ "all" };
}
if (options.Hslang != "none"){
@ -1414,8 +1478,8 @@ public class Crunchyroll{
options.SkipSubs = true;
}
if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){
await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir);
if (!options.SkipSubs && data.DownloadSubs.IndexOf("none") == -1){
await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir, data, (options.DlVideoOnce && dlVideoOnce && options.SyncTiming), videoDownloadMedia);
} else{
Console.WriteLine("Subtitles downloading skipped!");
}
@ -1472,7 +1536,8 @@ public class Crunchyroll{
};
}
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir){
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data, bool needsDelay,
DownloadedMedia videoDownloadMedia){
if (pbData.Meta != null && pbData.Meta.Subtitles != null && pbData.Meta.Subtitles.Count > 0){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles.Values.ToList();
List<SubtitleInfo> capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List<SubtitleInfo>();
@ -1510,7 +1575,7 @@ public class Crunchyroll{
var isSigns = langItem.Code == audDub && !subsItem.isCC;
var isCc = subsItem.isCC;
sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(options.DlSubs.Count == 1 && !options.DlSubs.Contains("all")));
sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all")));
sxData.Path = Path.Combine(fileDir, sxData.File);
Helpers.EnsureDirectoriesExist(sxData.Path);
@ -1523,7 +1588,7 @@ public class Crunchyroll{
continue;
}
if (options.DlSubs.Contains("all") || options.DlSubs.Contains(langItem.CrLocale)){
if (data.DownloadSubs.Contains("all") || data.DownloadSubs.Contains(langItem.CrLocale)){
var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url ?? string.Empty, HttpMethod.Get, false, false, null);
var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq);
@ -1567,7 +1632,8 @@ public class Crunchyroll{
Title = sxData.Title,
Fonts = sxData.Fonts,
Language = sxData.Language,
Lang = sxData.Language
Lang = sxData.Language,
RelatedVideoDownloadMedia = videoDownloadMedia
});
} else{
Console.WriteLine($"Failed to download subtitle: ${sxData.File}");

View File

@ -1,15 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models;
@ -17,7 +11,6 @@ using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData;
using Newtonsoft.Json;
using ReactiveUI;
namespace CRD.Downloader;
@ -60,11 +53,17 @@ public class History(){
await UpdateWithSeasonData(seasonData);
}
}
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile();
}
}
private void UpdateHistoryFile(){
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, crunInstance.HistoryList);
}
public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
@ -129,6 +128,59 @@ public class History(){
return (null, downloadDirPath);
}
public (HistoryEpisode? historyEpisode, List<string> dublist, string downloadDirPath) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
var downloadDirPath = "";
List<string> dublist = [];
if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesDubLangOverride.Count > 0){
dublist = historySeries.HistorySeriesDubLangOverride;
}
if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){
downloadDirPath = historySeries.SeriesDownloadPath;
}
if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historySeason.HistorySeasonDubLangOverride.Count > 0){
dublist = historySeason.HistorySeasonDubLangOverride;
}
if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){
downloadDirPath = historySeason.SeasonDownloadPath;
}
if (historyEpisode != null){
return (historyEpisode, dublist,downloadDirPath);
}
}
}
return (null, dublist,downloadDirPath);
}
public List<string> GetDubList(string? seriesId, string? seasonId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
List<string> dublist = [];
if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesDubLangOverride.Count > 0){
dublist = historySeries.HistorySeriesDubLangOverride;
}
if (historySeason is{ HistorySeasonDubLangOverride.Count: > 0 }){
dublist = historySeason.HistorySeasonDubLangOverride;
}
}
return dublist;
}
public async Task UpdateWithEpisode(CrunchyEpisode episodeParam){
@ -160,6 +212,7 @@ public class History(){
if (historySeason != null){
historySeason.SeasonTitle = episode.SeasonTitle;
historySeason.SeasonNum = Helpers.ExtractNumberAfterS(episode.Identifier) ?? episode.SeasonNumber + "";
historySeason.SpecialSeason = CheckStringForSpecial(episode.Identifier);
if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title,
@ -178,6 +231,7 @@ public class History(){
var newSeason = NewHistorySeason(episode);
historySeries.Seasons.Add(newSeason);
newSeason.Init();
}
historySeries.UpdateNewEpisodes();
@ -195,18 +249,33 @@ public class History(){
historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes();
historySeries.Init();
newSeason.Init();
}
SortItems();
SortSeasons(historySeries);
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
UpdateHistoryFile();
}
public async Task UpdateWithSeasonData(CrunchyEpisodeList seasonData){
public async Task UpdateWithSeasonData(CrunchyEpisodeList seasonData,bool skippVersionCheck = true){
if (seasonData.Data != null){
if (!skippVersionCheck){
if (seasonData.Data.First().Versions != null){
var version = seasonData.Data.First().Versions.Find(a => a.Original);
if (version.AudioLocale != seasonData.Data.First().AudioLocale){
UpdateSeries(seasonData.Data.First().SeriesId, version.SeasonGuid);
return;
}
} else{
UpdateSeries(seasonData.Data.First().SeriesId, "");
return;
}
}
var firstEpisode = seasonData.Data.First();
var seriesId = firstEpisode.SeriesId;
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
@ -220,6 +289,7 @@ public class History(){
if (historySeason != null){
historySeason.SeasonTitle = firstEpisode.SeasonTitle;
historySeason.SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "";
historySeason.SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier);
foreach (var crunchyEpisode in seasonData.Data){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id);
@ -252,6 +322,7 @@ public class History(){
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
historySeries.Seasons.Add(newSeason);
newSeason.Init();
}
historySeries.UpdateNewEpisodes();
@ -272,16 +343,13 @@ public class History(){
historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes();
historySeries.Init();
newSeason.Init();
}
SortItems();
SortSeasons(historySeries);
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
UpdateHistoryFile();
}
}
@ -358,19 +426,19 @@ public class History(){
return;
case SortingType.HistorySeriesAddDate:
var sortedSeriesAddDates = Crunchyroll.Instance.HistoryList
.OrderBy(s => sortingDir
? -(s.HistorySeriesAddDate?.Date.Ticks ?? DateTime.MinValue.Ticks)
: s.HistorySeriesAddDate?.Date.Ticks ?? DateTime.MaxValue.Ticks)
.ThenBy(s => s.SeriesTitle)
.ToList();
Crunchyroll.Instance.HistoryList.Clear();
Crunchyroll.Instance.HistoryList.AddRange(sortedSeriesAddDates);
return;
}
}
@ -388,7 +456,6 @@ public class History(){
}
private string GetSeriesThumbnail(CrSeriesBase series){
// var series = await crunInstance.CrSeries.SeriesById(seriesId);
@ -400,6 +467,10 @@ public class History(){
}
private static bool CheckStringForSpecial(string identifier){
if (string.IsNullOrEmpty(identifier)){
return false;
}
// Regex pattern to match any sequence that does NOT contain "|S" followed by one or more digits immediately after
string pattern = @"^(?!.*\|S\d+).*";
@ -564,6 +635,9 @@ public class History(){
}
}
});
CfgManager.UpdateHistoryFile();
}
}
@ -675,11 +749,12 @@ public class History(){
public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
public int Compare(HistoryEpisode x, HistoryEpisode y){
if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){
return xInt.CompareTo(yInt);
if (double.TryParse(x.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) &&
double.TryParse(y.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){
return xDouble.CompareTo(yDouble);
}
// Fall back to string comparison if not parseable as integers
return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal);
// Fall back to string comparison if not parseable as doubles
return string.Compare(x.Episode, y.Episode, StringComparison.Ordinal);
}
}

View File

@ -154,6 +154,9 @@ public enum MediaType{
public enum DownloadMediaType{
[EnumMember(Value = "Video")]
Video,
[EnumMember(Value = "SyncVideo")]
SyncVideo,
[EnumMember(Value = "Audio")]
Audio,

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using CRD.Downloader;
using CRD.Utils.Structs;
@ -34,24 +35,41 @@ public class CfgManager{
private static StreamWriter logFile;
private static bool isLogModeEnabled = false;
static CfgManager(){
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
}
private static void OnProcessExit(object? sender, EventArgs e){
DisableLogMode();
}
public static void EnableLogMode(){
if (!isLogModeEnabled){
logFile = new StreamWriter(PathLogFile);
logFile.AutoFlush = true;
Console.SetError(logFile);
isLogModeEnabled = true;
Console.Error.WriteLine("Log mode enabled.");
try{
var fileStream = new FileStream(PathLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
logFile = new StreamWriter(fileStream);
logFile.AutoFlush = true;
Console.SetError(logFile);
isLogModeEnabled = true;
Console.Error.WriteLine("Log mode enabled.");
} catch (Exception e){
Console.Error.WriteLine($"Couldn't enable logging: {e}");
}
}
}
public static void DisableLogMode(){
if (isLogModeEnabled){
logFile.Close();
StreamWriter standardError = new StreamWriter(Console.OpenStandardError());
standardError.AutoFlush = true;
Console.SetError(standardError);
isLogModeEnabled = false;
Console.Error.WriteLine("Log mode disabled.");
try{
logFile.Close();
StreamWriter standardError = new StreamWriter(Console.OpenStandardError());
standardError.AutoFlush = true;
Console.SetError(standardError);
isLogModeEnabled = false;
Console.Error.WriteLine("Log mode disabled.");
} catch (Exception e){
Console.Error.WriteLine($"Couldn't disable logging: {e}");
}
}
}
@ -127,7 +145,7 @@ public class CfgManager{
File.WriteAllText(PathCrDownloadOptions, yaml);
}
public static void UpdateSettingsFromFile(){
string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty;
@ -188,6 +206,10 @@ public class CfgManager{
return properties;
}
public static void UpdateHistoryFile(){
WriteJsonToFile(PathCrHistory, Crunchyroll.Instance.HistoryList);
}
private static object fileLock = new object();
public static void WriteJsonToFile(string pathToFile, object obj){
@ -199,9 +221,9 @@ public class CfgManager{
}
lock (fileLock){
// Write the JSON string to file using a streaming approach.
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write))
using (var streamWriter = new StreamWriter(fileStream))
using (var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal))
using (var streamWriter = new StreamWriter(gzipStream))
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
var serializer = new JsonSerializer();
serializer.Serialize(jsonWriter, obj);
@ -212,6 +234,39 @@ public class CfgManager{
}
}
public static string DecompressJsonFile(string pathToFile){
try{
using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read)){
// Check if the file is compressed
if (IsFileCompressed(fileStream)){
// Reset the stream position to the beginning
fileStream.Position = 0;
using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress))
using (var streamReader = new StreamReader(gzipStream)){
return streamReader.ReadToEnd();
}
}
// If not compressed, read the file as is
fileStream.Position = 0;
using (var streamReader = new StreamReader(fileStream)){
return streamReader.ReadToEnd();
}
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred: {ex.Message}");
return null;
}
}
private static bool IsFileCompressed(FileStream fileStream){
// Check the first two bytes for the GZip header
var buffer = new byte[2];
fileStream.Read(buffer, 0, 2);
return buffer[0] == 0x1F && buffer[1] == 0x8B;
}
public static bool CheckIfFileExists(string filePath){
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;

View File

@ -34,6 +34,7 @@ public class FileNameManager{
string[] parts = replacement.Split(',');
string formattedIntegerPart = parts[0].PadLeft(numbers, '0');
replacement = formattedIntegerPart + (parts.Length > 1 ? "," + parts[1] : "");
replacement = replacement.Replace(",", ".");
} else if (variable.Sanitize){
replacement = CleanupFilename(replacement);
}

View File

@ -196,6 +196,21 @@ public class Helpers{
}
}
public static void DeleteFile(string filePath){
if (string.IsNullOrEmpty(filePath)){
return;
}
try{
if (File.Exists(filePath)){
File.Delete(filePath);
}
} catch (Exception ex){
Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}");
// Handle exceptions if you need to log them or throw
}
}
public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){
try{
using (var process = new Process()){

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using CRD.Utils.Structs;
@ -11,7 +12,7 @@ using DynamicData;
namespace CRD.Utils.Muxing;
public class Merger{
private MergerOptions options;
public MergerOptions options;
public Merger(MergerOptions options){
this.options = options;
@ -45,6 +46,10 @@ public class Merger{
}
foreach (var aud in options.OnlyAudio){
if (aud.Delay != null && aud.Delay != 0){
args.Add($"-itsoffset {aud.Delay}");
}
args.Add($"-i \"{aud.Path}\"");
metaData.Add($"-map {index}:a");
metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}");
@ -53,14 +58,13 @@ public class Merger{
}
if (options.Chapters != null && options.Chapters.Count > 0){
Helpers.ConvertChapterFileForFFMPEG(options.Chapters[0].Path);
args.Add($"-i \"{options.Chapters[0].Path}\"");
metaData.Add($"-map_metadata {index}");
index++;
}
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null){
args.Add($"-itsoffset -{Math.Ceiling((double)sub.value.Delay * 1000)}ms");
@ -68,7 +72,7 @@ public class Merger{
args.Add($"-i \"{sub.value.File}\"");
}
if (options.Output.EndsWith(".mkv", StringComparison.OrdinalIgnoreCase)){
if (options.Fonts != null){
int fontIndex = 0;
@ -78,7 +82,7 @@ public class Merger{
}
}
}
args.AddRange(metaData);
args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
args.Add("-c:v copy");
@ -87,9 +91,8 @@ public class Merger{
args.AddRange(options.Subtitles.Select((sub, subindex) =>
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
if (!string.IsNullOrEmpty(options.VideoTitle)){
if (!string.IsNullOrEmpty(options.VideoTitle)){
args.Add($"-metadata title=\"{options.VideoTitle}\"");
}
@ -125,7 +128,6 @@ public class Merger{
}
public string MkvMerge(){
List<string> args = new List<string>();
@ -157,8 +159,7 @@ public class Merger{
args.Add("--no-video");
args.Add($"--track-name 0:\"{trackName}\"");
args.Add($"--language 0:{aud.Language.Code}");
if (options.Defaults.Audio.Code == aud.Language.Code){
args.Add("--default-track 0");
@ -166,6 +167,10 @@ public class Merger{
args.Add("--default-track 0:0");
}
if (aud.Delay != null && aud.Delay != 0){
args.Add($"--sync 0:{aud.Delay}");
}
args.Add($"\"{aud.Path}\"");
}
@ -216,14 +221,65 @@ public class Merger{
if (options.Description is{ Count: > 0 }){
args.Add($"--global-tags \"{options.Description[0].Path}\"");
}
return string.Join(" ", args);
}
public async Task<double> ProcessVideo(string baseVideoPath, string compareVideoPath){
var tempDir = Path.GetTempPath(); //TODO - maybe move this out of temp
var baseFramesDir = Path.Combine(tempDir, "base_frames");
var compareFramesDir = Path.Combine(tempDir, "compare_frames");
Directory.CreateDirectory(baseFramesDir);
Directory.CreateDirectory(compareFramesDir);
var extractFramesBase = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 60);
var extractFramesCompare = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 60);
if (!extractFramesBase.IsOk || !extractFramesCompare.IsOk){
Console.Error.WriteLine("Failed to extract Frames to Compare");
return 0;
}
var baseFrames = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData
{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesBase.frameRate)
}).ToList();
var compareFrames = Directory.GetFiles(compareFramesDir).Select(fp => new FrameData
{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesBase.frameRate)
}).ToList();
var offset = SyncingHelper.CalculateOffset(baseFrames, compareFrames);
Console.WriteLine($"Calculated offset: {offset} seconds");
CleanupDirectory(baseFramesDir);
CleanupDirectory(compareFramesDir);
return offset;
}
private static void CleanupDirectory(string dirPath){
if (Directory.Exists(dirPath)){
Directory.Delete(dirPath, true);
}
}
private static double GetTimeFromFileName(string fileName, double frameRate){
var match = Regex.Match(Path.GetFileName(fileName), @"frame(\d+)");
if (match.Success){
return int.Parse(match.Groups[1].Value) / frameRate; // Assuming 30 fps
}
return 0;
}
public async Task Merge(string type, string bin){
string command = type switch{
"ffmpeg" => FFmpeg(),
@ -253,28 +309,19 @@ public class Merger{
// Combine all media file lists and iterate through them
var allMediaFiles = options.OnlyAudio.Concat(options.OnlyVid)
.ToList();
allMediaFiles.ForEach(file => DeleteFile(file.Path));
allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume"));
options.Description?.ForEach(chapter => DeleteFile(chapter.Path));
allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path));
allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".resume"));
options.Description?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
// Delete chapter files if any
options.Chapters?.ForEach(chapter => DeleteFile(chapter.Path));
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
// Delete subtitle files
options.Subtitles.ForEach(subtitle => DeleteFile(subtitle.File));
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
}
private void DeleteFile(string filePath){
try{
if (File.Exists(filePath)){
File.Delete(filePath);
}
} catch (Exception ex){
Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}");
// Handle exceptions if you need to log them or throw
}
}
}
public class MergerInput{
@ -291,6 +338,8 @@ public class SubtitleInput{
public bool? ClosedCaption{ get; set; }
public bool? Signs{ get; set; }
public int? Delay{ get; set; }
public DownloadedMedia? RelatedVideoDownloadMedia;
}
public class ParsedFont{

View File

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Utils.Structs;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Image = SixLabors.ImageSharp.Image;
namespace CRD.Utils.Muxing;
public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
var ffmpegPath = CfgManager.PathFFMPEG;
var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%03d.png\"";
var output = "";
try{
using (var process = new Process()){
process.StartInfo.FileName = ffmpegPath;
process.StartInfo.Arguments = arguments;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.OutputDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine($"{e.Data}");
output += e.Data;
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
bool isSuccess = process.ExitCode == 0;
double frameRate = ExtractFrameRate(output);
return (IsOk: isSuccess, ErrorCode: process.ExitCode, frameRate);
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred: {ex.Message}");
return (IsOk: false, ErrorCode: -1, 0);
}
}
public static double ExtractFrameRate(string ffmpegOutput){
var match = Regex.Match(ffmpegOutput, @"Stream #0:0.*?(\d+(?:\.\d+)?) fps");
if (match.Success){
return double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
}
Console.Error.WriteLine("Failed to extract frame rate from FFmpeg output.");
return 0;
}
private static double CalculateSSIM(float[] pixels1, float[] pixels2, int width, int height){
double mean1 = pixels1.Average();
double mean2 = pixels2.Average();
double var1 = 0, var2 = 0, covariance = 0;
int count = pixels1.Length;
for (int i = 0; i < count; i++){
var1 += (pixels1[i] - mean1) * (pixels1[i] - mean1);
var2 += (pixels2[i] - mean2) * (pixels2[i] - mean2);
covariance += (pixels1[i] - mean1) * (pixels2[i] - mean2);
}
var1 /= count - 1;
var2 /= count - 1;
covariance /= count - 1;
double c1 = 0.01 * 0.01 * 255 * 255;
double c2 = 0.03 * 0.03 * 255 * 255;
double ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
((mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2));
return ssim;
}
private static float[] ExtractPixels(Image<Rgba32> image, int width, int height){
float[] pixels = new float[width * height];
int index = 0;
image.ProcessPixelRows(accessor => {
for (int y = 0; y < accessor.Height; y++){
Span<Rgba32> row = accessor.GetRowSpan(y);
for (int x = 0; x < row.Length; x++){
pixels[index++] = row[x].R;
}
}
});
return pixels;
}
public static double ComputeSSIM(string imagePath1, string imagePath2, int targetWidth, int targetHeight){
using (var image1 = Image.Load<Rgba32>(imagePath1))
using (var image2 = Image.Load<Rgba32>(imagePath2)){
// Preprocess images (resize and convert to grayscale)
image1.Mutate(x => x.Resize(new ResizeOptions{
Size = new Size(targetWidth, targetHeight),
Mode = ResizeMode.Max
}).Grayscale());
image2.Mutate(x => x.Resize(new ResizeOptions{
Size = new Size(targetWidth, targetHeight),
Mode = ResizeMode.Max
}).Grayscale());
// Extract pixel values into arrays
float[] pixels1 = ExtractPixels(image1, targetWidth, targetHeight);
float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight);
// Compute SSIM
return CalculateSSIM(pixels1, pixels2, targetWidth, targetHeight);
}
}
public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){
double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256);
Console.WriteLine($"SSIM: {ssim}");
return ssim > ssimThreshold;
}
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames, double ssimThreshold = 0.9){
foreach (var baseFrame in baseFrames){
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
if (matchingFrame != null){
Console.WriteLine($"Matched Frame: Base Frame Time: {baseFrame.Time}, Compare Frame Time: {matchingFrame.Time}");
return baseFrame.Time - matchingFrame.Time;
} else{
// Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
Debug.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
}
}
return 0;
}
}

View File

@ -108,7 +108,7 @@ public class CrDownloadOptions{
[YamlIgnore]
public bool? Skipmux{ get; set; }
[YamlIgnore]
[YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)]
public bool SyncTiming{ get; set; }
[YamlIgnore]

View File

@ -250,7 +250,8 @@ public class CrunchyEpMeta{
public List<string>? AvailableSubs{ get; set; }
public string? DownloadPath{ get; set; }
public List<string> DownloadSubs{ get; set; } =[];
}
public class DownloadProgress{

View File

@ -6,6 +6,7 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_title")]
public string? EpisodeTitle{ get; set; }
@ -48,7 +49,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
WasDownloaded = !WasDownloaded;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
}
public void ToggleWasDownloadedSeries(HistorySeries? series){
WasDownloaded = !WasDownloaded;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
@ -57,14 +58,15 @@ public class HistoryEpisode : INotifyPropertyChanged{
foreach (var historySeason in series.Seasons){
historySeason.UpdateDownloadedSilent();
}
series.UpdateNewEpisodes();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
CfgManager.UpdateHistoryFile();
}
public async Task DownloadEpisode(){
await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang);
await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang,
Crunchyroll.Instance.CrunOptions.DubLang);
}
}

View File

@ -1,6 +1,9 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using CRD.Downloader;
using Newtonsoft.Json;
@ -19,9 +22,6 @@ public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("season_special_season")]
public bool? SpecialSeason{ get; set; }
[JsonIgnore]
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
[JsonProperty("season_downloaded_episodes")]
public int DownloadedEpisodes{ get; set; }
@ -30,36 +30,126 @@ public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("series_download_path")]
public string? SeasonDownloadPath{ get; set; }
[JsonProperty("history_season_soft_subs_override")]
public List<string> HistorySeasonSoftSubsOverride{ get; set; } =[];
[JsonProperty("history_season_dub_lang_override")]
public List<string> HistorySeasonDubLangOverride{ get; set; } =[];
[JsonIgnore]
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
[JsonIgnore]
public bool IsExpanded{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
#region Language Override
[JsonIgnore]
public string SelectedSubs{ get; set; } = "";
[JsonIgnore]
public string SelectedDubs{ get; set; } = "";
[JsonIgnore]
public ObservableCollection<StringItem> SelectedSubLang{ get; set; } = new();
[JsonIgnore]
public ObservableCollection<StringItem> SelectedDubLang{ get; set; } = new();
[JsonIgnore]
public ObservableCollection<StringItem> DubLangList{ get; } = new(){
};
[JsonIgnore]
public ObservableCollection<StringItem> SubLangList{ get; } = new(){
new StringItem(){ stringValue = "all" },
new StringItem(){ stringValue = "none" },
};
private void UpdateSubAndDubString(){
HistorySeasonSoftSubsOverride.Clear();
HistorySeasonDubLangOverride.Clear();
if (SelectedSubLang.Count != 0){
for (var i = 0; i < SelectedSubLang.Count; i++){
HistorySeasonSoftSubsOverride.Add(SelectedSubLang[i].stringValue);
}
}
if (SelectedDubLang.Count != 0){
for (var i = 0; i < SelectedDubLang.Count; i++){
HistorySeasonDubLangOverride.Add(SelectedDubLang[i].stringValue);
}
}
SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs)));
CfgManager.UpdateHistoryFile();
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSubAndDubString();
}
public void Init(){
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
foreach (var languageItem in Languages.languages){
SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
}
}
var softSubLang = SubLangList.Where(a => HistorySeasonSoftSubsOverride.Contains(a.stringValue)).ToList();
var dubLang = DubLangList.Where(a => HistorySeasonDubLangOverride.Contains(a.stringValue)).ToList();
SelectedSubLang.Clear();
foreach (var listBoxItem in softSubLang){
SelectedSubLang.Add(listBoxItem);
}
SelectedDubLang.Clear();
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
}
#endregion
public void UpdateDownloaded(string? EpisodeId){
if (!string.IsNullOrEmpty(EpisodeId)){
EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded();
var episode = EpisodesList.First(e => e.EpisodeId == EpisodeId);
episode.ToggleWasDownloaded();
}
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
CfgManager.UpdateHistoryFile();
}
public void UpdateDownloaded(){
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
CfgManager.UpdateHistoryFile();
}
public void UpdateDownloadedSilent(){
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
}
}
public class UpdateDownloadedHistorySeason{
public string? EpisodeId;
public HistorySeries? HistorySeries;
}

View File

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using CRD.Downloader;
using CRD.Utils.CustomList;
@ -40,9 +42,6 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_new_episodes")]
public int NewEpisodes{ get; set; }
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonProperty("series_season_list")]
public required RefreshableObservableCollection<HistorySeason> Seasons{ get; set; }
@ -51,9 +50,18 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("history_series_add_date")]
public DateTime? HistorySeriesAddDate{ get; set; }
[JsonProperty("history_series_soft_subs_override")]
public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
[JsonProperty("history_series_dub_lang_override")]
public List<string> HistorySeriesDubLangOverride{ get; set; } =[];
public event PropertyChangedEventHandler? PropertyChanged;
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonIgnore]
public bool FetchingData{ get; set; }
@ -74,6 +82,90 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore]
private bool _editModeEnabled;
#region Language Override
[JsonIgnore]
public string SelectedSubs{ get; set; } = "";
[JsonIgnore]
public string SelectedDubs{ get; set; } = "";
[JsonIgnore]
public ObservableCollection<StringItem> SelectedSubLang{ get; set; } = new();
[JsonIgnore]
public ObservableCollection<StringItem> SelectedDubLang{ get; set; } = new();
private void UpdateSubAndDubString(){
HistorySeriesSoftSubsOverride.Clear();
HistorySeriesDubLangOverride.Clear();
if (SelectedSubLang.Count != 0){
for (var i = 0; i < SelectedSubLang.Count; i++){
HistorySeriesSoftSubsOverride.Add(SelectedSubLang[i].stringValue);
}
}
if (SelectedDubLang.Count != 0){
for (var i = 0; i < SelectedDubLang.Count; i++){
HistorySeriesDubLangOverride.Add(SelectedDubLang[i].stringValue);
}
}
SelectedDubs = string.Join(", ", HistorySeriesDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeriesSoftSubsOverride) ?? "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs)));
CfgManager.UpdateHistoryFile();
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSubAndDubString();
}
[JsonIgnore]
public ObservableCollection<StringItem> DubLangList{ get; } = new(){
};
[JsonIgnore]
public ObservableCollection<StringItem> SubLangList{ get; } = new(){
new StringItem(){ stringValue = "all" },
new StringItem(){ stringValue = "none" },
};
public void Init(){
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
foreach (var languageItem in Languages.languages){
SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
}
}
var softSubLang = SubLangList.Where(a => HistorySeriesSoftSubsOverride.Contains(a.stringValue)).ToList();
var dubLang = DubLangList.Where(a => HistorySeriesDubLangOverride.Contains(a.stringValue)).ToList();
SelectedSubLang.Clear();
foreach (var listBoxItem in softSubLang){
SelectedSubLang.Add(listBoxItem);
}
SelectedDubLang.Clear();
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
SelectedDubs = string.Join(", ", HistorySeriesDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeriesSoftSubsOverride) ?? "";
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
}
#endregion
public async Task LoadImage(){
try{
using (var client = new HttpClient()){
@ -165,9 +257,8 @@ public class HistorySeries : INotifyPropertyChanged{
if (objectToRemove != null){
Seasons.Remove(objectToRemove);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Seasons)));
CfgManager.UpdateHistoryFile();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
public void OpenSonarrPage(){

View File

@ -111,7 +111,7 @@ public class Languages{
public static LanguageItem Locale2language(string locale){
LanguageItem? filteredLocale = languages.FirstOrDefault(l => { return l.Locale == locale; });
LanguageItem? filteredLocale = languages.FirstOrDefault(l => { return l.Locale == locale || l.CrLocale == locale; });
if (filteredLocale != null){
return (LanguageItem)filteredLocale;
} else{

View File

@ -9,7 +9,9 @@ public struct AuthData{
}
public class DrmAuthData{
[JsonProperty("custom_data")] public string? CustomData{ get; set; }
[JsonProperty("custom_data")]
public string? CustomData{ get; set; }
public string? Token{ get; set; }
}
@ -21,6 +23,7 @@ public struct Meta{
public struct LanguageItem{
[JsonProperty("cr_locale")]
public string CrLocale{ get; set; }
public string Locale{ get; set; }
public string Code{ get; set; }
public string Name{ get; set; }
@ -62,7 +65,7 @@ public struct Episode{
public struct DownloadResponse{
public List<DownloadedMedia> Data{ get; set; }
public string FileName{ get; set; }
public string VideoTitle{ get; set; }
public bool Error{ get; set; }
public string ErrorText{ get; set; }
@ -75,6 +78,8 @@ public class DownloadedMedia : SxItem{
public bool? Cc{ get; set; }
public bool? Signs{ get; set; }
public DownloadedMedia? RelatedVideoDownloadMedia;
}
public class SxItem{
@ -85,3 +90,11 @@ public class SxItem{
public Dictionary<string, List<string>>? Fonts{ get; set; }
}
public class FrameData{
public string FilePath{ get; set; }
public double Time{ get; set; }
}
public class StringItem{
public string stringValue{ get; set; }
}

View File

@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data.Converters;
using CRD.Utils.Structs.History;
namespace CRD.Utils.UI;
public class UiUpdateDownloadedHistorySeasonConverter : IMultiValueConverter{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture){
if (values[0] is string stringValue1){
Console.WriteLine(stringValue1);
}
if (values is[string stringValue, HistorySeries historySeries]){
return new UpdateDownloadedHistorySeason{
EpisodeId = stringValue,
HistorySeries = historySeries
};
}
return new UpdateDownloadedHistorySeason{
EpisodeId = "",
HistorySeries = null
};
}
}

View File

@ -1,16 +1,11 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using CRD.Utils.HLS;
using CRD.ViewModels;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using Newtonsoft.Json;
namespace CRD.Utils.Updater;

View File

@ -89,7 +89,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
episode.LoadImage(imageUrl);
}
}
SearchItems.Add(episode);
}
@ -103,7 +103,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
RaisePropertyChanged(nameof(SearchVisible));
SearchItems.Clear();
}
partial void OnUrlInputChanged(string value){
if (SearchEnabled){
UpdateSearch(value);
@ -186,7 +186,10 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (match.Success){
var locale = match.Groups[1].Value; // Capture the locale part
var id = match.Groups[2].Value; // Capture the ID part
Crunchyroll.Instance.AddEpisodeToQue(id, Languages.Locale2language(locale).CrLocale, Crunchyroll.Instance.CrunOptions.DubLang, true);
Crunchyroll.Instance.AddEpisodeToQue(id,
string.IsNullOrEmpty(locale)
? string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang
: Languages.Locale2language(locale).CrLocale, Crunchyroll.Instance.CrunOptions.DubLang, true);
UrlInput = "";
selectedEpisodes.Clear();
SelectedItems.Clear();
@ -209,7 +212,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonEnabled = false;
ShowLoading = true;
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id, Languages.Locale2language(locale).CrLocale, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id, string.IsNullOrEmpty(locale)
? string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang
: Languages.Locale2language(locale).CrLocale, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
ShowLoading = false;
if (list != null){
currentSeriesList = list;
@ -275,8 +280,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
async partial void OnSelectedSearchItemChanged(CrBrowseSeries value){
if (value == null){
async partial void OnSelectedSearchItemChanged(CrBrowseSeries? value){
if (value == null || string.IsNullOrEmpty(value.Id)){
return;
}
@ -286,7 +291,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
SearchVisible = false;
ButtonEnabled = false;
ShowLoading = true;
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id,
string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang,
new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
ShowLoading = false;
if (list != null){
currentSeriesList = list;
@ -296,7 +303,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
episode.Lang));
} else{
episodesBySeason.Add("S" + episode.Season, new List<ItemModel>{
new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E, episode.Lang)
new(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E, episode.Lang)
});
SeasonList.Add(new ComboBoxItem{ Content = "S" + episode.Season });
}
@ -351,11 +358,11 @@ public class ItemModel(string imageUrl, string description, string time, string
public string TitleFull{ get; set; } = season + episode + " - " + title;
public List<string> AvailableAudios{ get; set; } = availableAudios;
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}

View File

@ -258,7 +258,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
private async void BuildCustomCalendar(){
ShowLoading = true;
var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang, 200);
var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang, 200,true);
CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>();
@ -283,7 +283,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
foreach (var crBrowseEpisode in newEpisodes){
var targetDate = FilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic;
if (HideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)")){
if (HideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && (crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){
continue;
}
@ -304,8 +304,8 @@ public partial class CalendarPageViewModel : ViewModelBase{
calEpisode.DateTime = targetDate;
calEpisode.HasPassed = DateTime.Now > targetDate;
calEpisode.EpisodeName = crBrowseEpisode.Title;
calEpisode.SeriesUrl = "https://www.crunchyroll.com/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/de/watch/{crBrowseEpisode.Id}/";
calEpisode.SeriesUrl = $"https://www.crunchyroll.com/{Crunchyroll.Instance.CrunOptions.HistoryLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/{Crunchyroll.Instance.CrunOptions.HistoryLang}/watch/{crBrowseEpisode.Id}/";
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail.First().First().Source;
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";

View File

@ -104,7 +104,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
var softSubs = "Softsub: ";
if (Crunchyroll.Instance.CrunOptions.DlSubs.Contains("all")){
if (epMeta.DownloadSubs.Contains("all")){
if (epMeta.AvailableSubs != null){
foreach (var epMetaAvailableSub in epMeta.AvailableSubs){
softSubs += epMetaAvailableSub + " ";
@ -114,7 +114,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
}
}
foreach (var crunOptionsDlSub in Crunchyroll.Instance.CrunOptions.DlSubs){
foreach (var crunOptionsDlSub in epMeta.DownloadSubs){
if (epMeta.AvailableSubs != null && epMeta.AvailableSubs.Contains(crunOptionsDlSub)){
softSubs += crunOptionsDlSub + " ";
}

View File

@ -84,7 +84,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _sortDir = false;
public HistoryPageViewModel(){
Items = Crunchyroll.Instance.HistoryList;
@ -217,7 +217,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
@ -230,9 +229,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (objectToRemove != null){
Crunchyroll.Instance.HistoryList.Remove(objectToRemove);
Items.Remove(objectToRemove);
CfgManager.UpdateHistoryFile();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
@ -291,9 +289,9 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (season != null){
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
CfgManager.UpdateHistoryFile();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}
@ -316,9 +314,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (series != null){
series.SeriesDownloadPath = selectedFolder.Path.LocalPath;
CfgManager.UpdateHistoryFile();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}

View File

@ -48,8 +48,8 @@ public partial class MainWindowViewModel : ViewModelBase{
public async void Init(){
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
await Crunchyroll.Instance.Init();
Crunchyroll.Instance.InitOptions();
if (Crunchyroll.Instance.CrunOptions.AccentColor != null){
_faTheme.CustomAccentColor = Color.Parse(Crunchyroll.Instance.CrunOptions.AccentColor);
}
@ -63,5 +63,8 @@ public partial class MainWindowViewModel : ViewModelBase{
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
}
await Crunchyroll.Instance.Init();
}
}

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -21,28 +23,34 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _editMode;
[ObservableProperty]
public static bool _sonarrAvailable;
private IStorageProvider _storageProvider;
private IStorageProvider? _storageProvider;
public SeriesPageViewModel(){
_selectedSeries = Crunchyroll.Instance.SelectedSeries;
if (_selectedSeries.ThumbnailImage == null){
_selectedSeries.LoadImage();
}
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
}else{
} else{
SonarrAvailable = false;
}
}
[RelayCommand]
public async Task OpenFolderDialogAsync(HistorySeason? season){
if (_storageProvider == null){
@ -62,25 +70,25 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (season != null){
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
CfgManager.UpdateHistoryFile();
} else{
SelectedSeries.SeriesDownloadPath = selectedFolder.Path.LocalPath;
CfgManager.UpdateHistoryFile();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
[RelayCommand]
public async Task UpdateData(string? season){
await SelectedSeries.FetchData(season);
SelectedSeries.Seasons.Refresh();
// MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
}
@ -89,9 +97,8 @@ public partial class SeriesPageViewModel : ViewModelBase{
HistorySeason? objectToRemove = SelectedSeries.Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null;
if (objectToRemove != null){
SelectedSeries.Seasons.Remove(objectToRemove);
CfgManager.UpdateHistoryFile();
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
}
@ -101,7 +108,4 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.UpdateNewEpisodes();
MessageBus.Current.SendMessage(new NavigationMessage(null, true, false));
}
}

View File

@ -16,6 +16,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Utils;
using CRD.Utils.CustomList;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using FluentAvalonia.Styling;
@ -51,6 +52,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _muxToMp4;
[ObservableProperty]
private bool _syncTimings;
[ObservableProperty]
private bool _includeEpisodeDescription;
@ -102,8 +107,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedDescriptionLang;
[ObservableProperty]
private string _selectedDubs = "ja-JP";
@ -268,7 +272,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
new ComboBoxItem(){ Content = "ar-SA" },
};
public ObservableCollection<ComboBoxItem> DubLangList{ get; } = new(){
public ObservableCollection<ListBoxItem> DubLangList{ get; } = new(){
};
@ -318,7 +322,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
foreach (var languageItem in Languages.languages){
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
DubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
DubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
}
@ -358,9 +362,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
UpdateSubAndDubString();
var props = options.SonarrProperties;
if (props != null){
@ -383,6 +385,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
DownloadVideoForEveryDub = !options.DlVideoOnce;
DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4;
SyncTimings = options.SyncTiming;
SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers;
FileName = options.FileName;
@ -417,6 +420,12 @@ public partial class SettingsPageViewModel : ViewModelBase{
FfmpegOptions.Add(new MuxingParam(){ ParamValue = ffmpegParam });
}
}
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
SelectedDubs = string.Join(", ", dubs) ?? "";
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
SelectedSubs = string.Join(", ", subs) ?? "";
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
@ -431,9 +440,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
if (!settingsLoaded){
return;
}
UpdateSubAndDubString();
Crunchyroll.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription;
Crunchyroll.Instance.CrunOptions.VideoTitle = FileTitle;
Crunchyroll.Instance.CrunOptions.Novids = !DownloadVideo;
@ -441,6 +448,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
Crunchyroll.Instance.CrunOptions.Chapters = DownloadChapters;
Crunchyroll.Instance.CrunOptions.Mp4 = MuxToMp4;
Crunchyroll.Instance.CrunOptions.SyncTiming = SyncTimings;
Crunchyroll.Instance.CrunOptions.SkipSubsMux = SkipSubMux;
Crunchyroll.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0),0,10);
Crunchyroll.Instance.CrunOptions.FileName = FileName;
@ -459,11 +467,11 @@ public partial class SettingsPageViewModel : ViewModelBase{
string descLang = SelectedDescriptionLang.Content + "";
Crunchyroll.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : "";
Crunchyroll.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : Crunchyroll.Instance.DefaultLocale;
string historyLang = SelectedHistoryLang.Content + "";
Crunchyroll.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : "";
Crunchyroll.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : Crunchyroll.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + "";
@ -481,10 +489,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
}
Crunchyroll.Instance.CrunOptions.DubLang = dubLangs;
Crunchyroll.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + "";
Crunchyroll.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
Crunchyroll.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + "";
@ -556,28 +561,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
return ScaledBorderAndShadow[0];
}
}
private void UpdateSubAndDubString(){
if (SelectedSubLang.Count == 0){
SelectedSubs = "none";
} else{
SelectedSubs = SelectedSubLang[0].Content.ToString();
for (var i = 1; i < SelectedSubLang.Count; i++){
SelectedSubs += "," + SelectedSubLang[i].Content;
}
}
if (SelectedDubLang.Count == 0){
SelectedDubs = "none";
} else{
SelectedDubs = SelectedDubLang[0].Content.ToString();
for (var i = 1; i < SelectedDubLang.Count; i++){
SelectedDubs += "," + SelectedDubLang[i].Content;
}
}
}
[RelayCommand]
public void AddMkvMergeParam(){
MkvMergeOptions.Add(new MuxingParam(){ ParamValue = MkvMergeOption });
@ -681,13 +665,21 @@ public partial class SettingsPageViewModel : ViewModelBase{
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings();
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
SelectedDubs = string.Join(", ", dubs) ?? "";
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
SelectedSubs = string.Join(", ", subs) ?? "";
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
base.OnPropertyChanged(e);
if (e.PropertyName is nameof(SelectedSubs) or nameof(SelectedDubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){
if (e.PropertyName is nameof(SelectedDubs) or nameof(SelectedSubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){
return;
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel;
using System;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace CRD.ViewModels;

View File

@ -6,6 +6,7 @@
xmlns:ui="clr-namespace:CRD.Utils.UI"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:history="clr-namespace:CRD.Utils.Structs.History"
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">
@ -13,7 +14,6 @@
<UserControl.Resources>
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
<ui:UiUpdateDownloadedHistorySeasonConverter x:Key="UiUpdateDownloadedHistorySeasonConverter" />
</UserControl.Resources>
<Grid>
@ -120,8 +120,8 @@
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<controls:SymbolIcon IsVisible="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).SortDir}" Symbol="ChevronUp" FontSize="12" Margin="0 0 10 0"/>
<controls:SymbolIcon IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SortDir}" Symbol="ChevronDown" FontSize="12" Margin="0 0 10 0"/>
<controls:SymbolIcon IsVisible="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).SortDir}" Symbol="ChevronUp" FontSize="12" Margin="0 0 10 0" />
<controls:SymbolIcon IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SortDir}" Symbol="ChevronDown" FontSize="12" Margin="0 0 10 0" />
<TextBlock Text="{Binding SortingTitle}"></TextBlock>
</StackPanel>
</DataTemplate>
@ -338,7 +338,7 @@
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding FetchData}" Margin="0 0 5 10">Fetch Series</Button>
<Button Command="{Binding FetchData}" Margin="0 0 5 10">Refresh Series</Button>
<ToggleButton x:Name="SeriesEditModeToggle" IsChecked="{Binding EditModeEnabled}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic"
@ -354,6 +354,115 @@
</StackPanel>
</Button>
<StackPanel>
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
IsVisible="{Binding EditModeEnabled}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
MaxWidth="400"
MaxHeight="600"
IsOpen="{Binding IsChecked, ElementName=SeriesOverride, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeriesOverride}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedDubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonDub, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonDub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList, Mode=OneWay}"
SelectedItems="{Binding SelectedDubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonSub, Mode=TwoWay}" Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonSub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SubLangList, Mode=OneWay}"
SelectedItems="{Binding SelectedSubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
</StackPanel>
</Border>
</Popup>
</StackPanel>
</StackPanel>
@ -481,7 +590,7 @@
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).FetchData}"
CommandParameter="{Binding SeasonId}">
<ToolTip.Tip>
<TextBlock Text="Fetch Season" FontSize="15" />
<TextBlock Text="Refresh Season" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Refresh"
@ -503,6 +612,117 @@
</StackPanel>
</Button>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
MaxWidth="400"
MaxHeight="600"
IsOpen="{Binding IsChecked, ElementName=SeasonOverride, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverride}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedDubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=SeasonOverrideDropdownButtonDub, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverrideDropdownButtonDub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList}"
SelectedItems="{Binding SelectedDubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=SeasonOverrideDropdownButtonSub, Mode=TwoWay}" Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverrideDropdownButtonSub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SubLangList}"
SelectedItems="{Binding SelectedSubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<Button Margin="10 0 0 0" FontStyle="Italic"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}"
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).RemoveSeason}"

View File

@ -43,7 +43,7 @@ public partial class MainWindow : AppWindow{
public MainWindow(){
AvaloniaXamlLoader.Load(this);
InitializeComponent();
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;

View File

@ -5,10 +5,13 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:history="clr-namespace:CRD.Utils.Structs.History"
xmlns:ui="clr-namespace:CRD.Utils.UI"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
x:DataType="vm:SeriesPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.SeriesPageView">
<Grid>
<Grid Margin="10">
<Grid.RowDefinitions>
@ -64,7 +67,7 @@
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Fetch Series</Button>
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Refresh Series</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic"
@ -78,6 +81,116 @@
</StackPanel>
</Button>
<StackPanel>
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
IsVisible="{Binding EditMode}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
MaxWidth="400"
MaxHeight="600"
IsOpen="{Binding IsChecked, ElementName=SeriesOverride, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeriesOverride}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedDubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonDub, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonDub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SelectedSeries.DubLangList , Mode=OneWay}"
SelectedItems="{Binding SelectedSeries.SelectedDubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedSubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonSub, Mode=TwoWay}" Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonSub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SelectedSeries.SubLangList , Mode=OneWay}"
SelectedItems="{Binding SelectedSeries.SelectedSubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
</StackPanel>
</Border>
</Popup>
</StackPanel>
</StackPanel>
@ -178,7 +291,7 @@
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).UpdateData}"
CommandParameter="{Binding SeasonId}">
<ToolTip.Tip>
<TextBlock Text="Fetch Season" FontSize="15" />
<TextBlock Text="Refresh Season" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Refresh" FontSize="18" />
@ -197,6 +310,116 @@
</StackPanel>
</Button>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).EditMode}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
MaxWidth="400"
MaxHeight="600"
IsOpen="{Binding IsChecked, ElementName=SeasonOverride, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverride}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedDubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=SeasonOverrideDropdownButtonDub, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverrideDropdownButtonDub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList , Mode=OneWay}"
SelectedItems="{Binding SelectedDubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSubs}"
VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=SeasonOverrideDropdownButtonSub, Mode=TwoWay}" Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverrideDropdownButtonSub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SubLangList , Mode=OneWay}"
SelectedItems="{Binding SelectedSubLang}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).RemoveSeason}"

View File

@ -12,8 +12,7 @@
<Design.DataContext>
<vm:SettingsPageViewModel />
</Design.DataContext>
<ScrollViewer Padding="20 20 20 0">
<StackPanel Spacing="8">
@ -51,7 +50,8 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList}" SelectedItems="{Binding SelectedDubLang}">
ItemsSource="{Binding DubLangList}"
SelectedItems="{Binding SelectedDubLang}">
</ListBox>
</Border>
</Popup>
@ -333,6 +333,11 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Sync Timings" Description="Does not work for all episodes but for the ones that only have a different intro">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SyncTimings}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Additional MKVMerge Options">
<controls:SettingsExpanderItem.Footer>