diff --git a/CRD/CRD.csproj b/CRD/CRD.csproj
deleted file mode 100644
index 7128825..0000000
--- a/CRD/CRD.csproj
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
- WinExe
- net8.0
- enable
- true
- app.manifest
- true
- en
- Assets\app_icon.ico
- 1.5.2.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ContentDialogInputLoginView.axaml
- Code
-
-
- ContentDialogInputLoginView.axaml
- Code
-
-
-
-
-
diff --git a/CRD/Downloader/CrEpisode.cs b/CRD/Downloader/CrEpisode.cs
index 03e5263..5bb50df 100644
--- a/CRD/Downloader/CrEpisode.cs
+++ b/CRD/Downloader/CrEpisode.cs
@@ -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 GetNewEpisodes(string? crLocale, int requestAmount){
+ public async Task 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 + "";
diff --git a/CRD/Downloader/CrSeries.cs b/CRD/Downloader/CrSeries.cs
index 4530b4a..c6fdf2c 100644
--- a/CRD/Downloader/CrSeries.cs
+++ b/CRD/Downloader/CrSeries.cs
@@ -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 Search(string searchString,string? crLocale){
-
+ public async Task 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(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
-
+
return series;
}
diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs
index ee2c1a8..52cc000 100644
--- a/CRD/Downloader/Crunchyroll.cs
+++ b/CRD/Downloader/Crunchyroll.cs
@@ -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>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
- }
+ var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
+ if (!string.IsNullOrEmpty(decompressedJson)){
+ HistoryList = JsonConvert.DeserializeObject>(decompressedJson) ?? new ObservableCollection();
- 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 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();
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{ "all" };
+ if (data.DownloadSubs.IndexOf("all") > -1){
+ data.DownloadSubs = new List{ "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 files, string fileDir){
+ private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir, CrunchyEpMeta data, bool needsDelay,
+ DownloadedMedia videoDownloadMedia){
if (pbData.Meta != null && pbData.Meta.Subtitles != null && pbData.Meta.Subtitles.Count > 0){
List subsData = pbData.Meta.Subtitles.Values.ToList();
List capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List();
@@ -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}");
diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs
index e39ac88..8e99e70 100644
--- a/CRD/Downloader/History.cs
+++ b/CRD/Downloader/History.cs
@@ -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 dublist, string downloadDirPath) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId, string episodeId){
+ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
+
+ var downloadDirPath = "";
+ List 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 GetDubList(string? seriesId, string? seasonId){
+ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
+
+ List 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{
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);
}
}
\ No newline at end of file
diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs
index 36c7c23..7353475 100644
--- a/CRD/Utils/Enums/EnumCollection.cs
+++ b/CRD/Utils/Enums/EnumCollection.cs
@@ -154,6 +154,9 @@ public enum MediaType{
public enum DownloadMediaType{
[EnumMember(Value = "Video")]
Video,
+
+ [EnumMember(Value = "SyncVideo")]
+ SyncVideo,
[EnumMember(Value = "Audio")]
Audio,
diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs
index 5a1e761..3eb2bb8 100644
--- a/CRD/Utils/Files/CfgManager.cs
+++ b/CRD/Utils/Files/CfgManager.cs
@@ -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;
diff --git a/CRD/Utils/Files/FileNameManager.cs b/CRD/Utils/Files/FileNameManager.cs
index 7d37422..1b804a6 100644
--- a/CRD/Utils/Files/FileNameManager.cs
+++ b/CRD/Utils/Files/FileNameManager.cs
@@ -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);
}
diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs
index 3979a10..7af37a7 100644
--- a/CRD/Utils/Helpers.cs
+++ b/CRD/Utils/Helpers.cs
@@ -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()){
diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs
index 34dee2e..4cd9dfa 100644
--- a/CRD/Utils/Muxing/Merger.cs
+++ b/CRD/Utils/Muxing/Merger.cs
@@ -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 args = new List();
@@ -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 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{
diff --git a/CRD/Utils/Muxing/SyncingHelper.cs b/CRD/Utils/Muxing/SyncingHelper.cs
new file mode 100644
index 0000000..9e0b79d
--- /dev/null
+++ b/CRD/Utils/Muxing/SyncingHelper.cs
@@ -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 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 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(imagePath1))
+ using (var image2 = Image.Load(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 baseFrames, List 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;
+ }
+}
\ No newline at end of file
diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs
index 271e3d0..0deeb80 100644
--- a/CRD/Utils/Structs/CrDownloadOptions.cs
+++ b/CRD/Utils/Structs/CrDownloadOptions.cs
@@ -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]
diff --git a/CRD/Utils/Structs/EpisodeStructs.cs b/CRD/Utils/Structs/EpisodeStructs.cs
index e74ac7c..44850ac 100644
--- a/CRD/Utils/Structs/EpisodeStructs.cs
+++ b/CRD/Utils/Structs/EpisodeStructs.cs
@@ -250,7 +250,8 @@ public class CrunchyEpMeta{
public List? AvailableSubs{ get; set; }
public string? DownloadPath{ get; set; }
-
+ public List DownloadSubs{ get; set; } =[];
+
}
public class DownloadProgress{
diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs
index 8c752f7..6d35462 100644
--- a/CRD/Utils/Structs/History/HistoryEpisode.cs
+++ b/CRD/Utils/Structs/History/HistoryEpisode.cs
@@ -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);
}
}
\ No newline at end of file
diff --git a/CRD/Utils/Structs/History/HistorySeason.cs b/CRD/Utils/Structs/History/HistorySeason.cs
index 6711300..e5eaf4a 100644
--- a/CRD/Utils/Structs/History/HistorySeason.cs
+++ b/CRD/Utils/Structs/History/HistorySeason.cs
@@ -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 HistorySeasonSoftSubsOverride{ get; set; } =[];
+
+ [JsonProperty("history_season_dub_lang_override")]
+ public List 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 SelectedSubLang{ get; set; } = new();
+
+ [JsonIgnore]
+ public ObservableCollection SelectedDubLang{ get; set; } = new();
+
+ [JsonIgnore]
+ public ObservableCollection DubLangList{ get; } = new(){
+ };
+
+ [JsonIgnore]
+ public ObservableCollection 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;
-}
diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs
index ffaf00a..0f758f2 100644
--- a/CRD/Utils/Structs/History/HistorySeries.cs
+++ b/CRD/Utils/Structs/History/HistorySeries.cs
@@ -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 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 HistorySeriesSoftSubsOverride{ get; set; } =[];
+
+ [JsonProperty("history_series_dub_lang_override")]
+ public List 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 SelectedSubLang{ get; set; } = new();
+
+ [JsonIgnore]
+ public ObservableCollection 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 DubLangList{ get; } = new(){
+ };
+
+ [JsonIgnore]
+ public ObservableCollection 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(){
diff --git a/CRD/Utils/Structs/Languages.cs b/CRD/Utils/Structs/Languages.cs
index b521dca..6c26f7e 100644
--- a/CRD/Utils/Structs/Languages.cs
+++ b/CRD/Utils/Structs/Languages.cs
@@ -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{
diff --git a/CRD/Utils/Structs/Structs.cs b/CRD/Utils/Structs/Structs.cs
index d9952ca..ed25488 100644
--- a/CRD/Utils/Structs/Structs.cs
+++ b/CRD/Utils/Structs/Structs.cs
@@ -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 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>? Fonts{ get; set; }
}
+public class FrameData{
+ public string FilePath{ get; set; }
+ public double Time{ get; set; }
+}
+
+public class StringItem{
+ public string stringValue{ get; set; }
+}
\ No newline at end of file
diff --git a/CRD/Utils/UI/UiUpdateDownloadedHistorySeasonConverter.cs b/CRD/Utils/UI/UiUpdateDownloadedHistorySeasonConverter.cs
deleted file mode 100644
index 11a7df6..0000000
--- a/CRD/Utils/UI/UiUpdateDownloadedHistorySeasonConverter.cs
+++ /dev/null
@@ -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