Add - Added fields to add mkvmerge & ffmpeg parameters to settings

Add - Added a new way to check the premium status of an account
Add - Added default audio and sub to muxing settings
Add - Added option to keep subtitles as files and not merge them
Add - Added option to download videos for all dubs selected

Chg - FFmpeg now also adds chapters to the files
Chg - Shows error if decryption files are missing
This commit is contained in:
Elwador 2024-06-19 02:16:02 +02:00
parent 8fc0812448
commit 272d59a03b
27 changed files with 735 additions and 318 deletions

View File

@ -35,7 +35,7 @@ public class CrAuth{
if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent);
} else{
Console.WriteLine("Anonymous login failed");
Console.Error.WriteLine("Anonymous login failed");
}
crunInstance.Profile = new CrProfile{
@ -104,13 +104,27 @@ public class CrAuth{
if (profileTemp != null){
crunInstance.Profile = profileTemp;
var requestSubs = HttpClientReq.CreateRequestMessage(Api.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null);
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
if (responseSubs.IsOk){
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
crunInstance.Profile.Subscription = subsc;
crunInstance.Profile.HasPremium = subsc.IsActive;
} else{
crunInstance.Profile.HasPremium = false;
Console.Error.WriteLine("Failed to check premium subscription status");
}
}
}
}
public async void LoginWithToken(){
if (crunInstance.Token?.refresh_token == null){
Console.WriteLine("Missing Refresh Token");
Console.Error.WriteLine("Missing Refresh Token");
return;
}
@ -133,7 +147,7 @@ public class CrAuth{
if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent);
} else{
Console.WriteLine("Token Auth Failed");
Console.Error.WriteLine("Token Auth Failed");
}
if (crunInstance.Token?.refresh_token != null){
@ -176,7 +190,7 @@ public class CrAuth{
if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent);
} else{
Console.WriteLine("Refresh Token Auth Failed");
Console.Error.WriteLine("Refresh Token Auth Failed");
}
await GetCmsToken();
@ -185,7 +199,7 @@ public class CrAuth{
public async Task GetCmsToken(){
if (crunInstance.Token?.access_token == null){
Console.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}");
Console.Error.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}");
return;
}
@ -196,13 +210,13 @@ public class CrAuth{
if (response.IsOk){
crunInstance.CmsToken = JsonConvert.DeserializeObject<CrCmsToken>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
} else{
Console.WriteLine("CMS Token Auth Failed");
Console.Error.WriteLine("CMS Token Auth Failed");
}
}
public async Task GetCmsData(){
if (crunInstance.CmsToken?.Cms == null){
Console.WriteLine("Missing CMS Token");
Console.Error.WriteLine("Missing CMS Token");
return;
}
@ -224,7 +238,7 @@ public class CrAuth{
if (response.IsOk){
Console.WriteLine(response.ResponseContent);
} else{
Console.WriteLine("Failed to Get CMS Index");
Console.Error.WriteLine("Failed to Get CMS Index");
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
@ -19,7 +19,7 @@ public class CrEpisode(){
public async Task<CrunchyEpisodeList?> ParseEpisodeById(string id,string locale){
if (crunInstance.CmsToken?.Cms == null){
Console.WriteLine("Missing CMS Access Token");
Console.Error.WriteLine("Missing CMS Access Token");
return null;
}
@ -33,7 +33,7 @@ public class CrEpisode(){
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.WriteLine("Series Request Failed");
Console.Error.WriteLine("Series Request Failed");
return null;
}

View File

@ -10,7 +10,9 @@ using System.Threading.Tasks;
using System.Web;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Views;
using Newtonsoft.Json;
using ReactiveUI;
namespace CRD.Downloader;
@ -45,10 +47,15 @@ public class CrSeries(){
foreach (var kvp in eps){
var key = kvp.Key;
var episode = kvp.Value;
for (int index = 0; index < episode.Items.Count; index++){
var item = episode.Items[index];
if (item.IsPremiumOnly && !crunInstance.Profile.HasPremium){
MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3));
continue;
}
if (!dubLang.Contains(episode.Langs[index].CrLocale))
continue;
@ -133,7 +140,7 @@ public class CrSeries(){
CrSeriesSearch? parsedSeries = await ParseSeriesById(id,Locale); // one piece - GRMG8ZQZR
if (parsedSeries == null){
Console.WriteLine("Parse Data Invalid");
Console.Error.WriteLine("Parse Data Invalid");
return null;
}
@ -281,7 +288,7 @@ public class CrSeries(){
CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List<CrunchyEpisode>(), Total = 0, Meta = new Meta() };
if (crunInstance.CmsToken?.Cms == null){
Console.WriteLine("Missing CMS Token");
Console.Error.WriteLine("Missing CMS Token");
return episodeList;
}
@ -291,14 +298,12 @@ public class CrSeries(){
var response = await HttpClientReq.Instance.SendHttpRequest(showRequest);
if (!response.IsOk){
Console.WriteLine("Show Request FAILED!");
Console.Error.WriteLine("Show Request FAILED!");
} else{
Console.WriteLine(response.ResponseContent);
}
}
//TODO
var episodeRequest = new HttpRequestMessage(HttpMethod.Get, $"{Api.Cms}/seasons/{seasonID}/episodes?preferred_audio_language=ja-JP");
episodeRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", crunInstance.Token?.access_token);
@ -306,13 +311,13 @@ public class CrSeries(){
var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest);
if (!episodeRequestResponse.IsOk){
Console.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}");
Console.Error.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}");
} else{
episodeList = Helpers.Deserialize<CrunchyEpisodeList>(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
}
if (episodeList.Total < 1){
Console.WriteLine("Season is empty!");
Console.Error.WriteLine("Season is empty!");
}
return episodeList;
@ -352,7 +357,7 @@ public class CrSeries(){
public async Task<CrSeriesSearch?> ParseSeriesById(string id,string? locale){
if (crunInstance.CmsToken?.Cms == null){
Console.WriteLine("Missing CMS Access Token");
Console.Error.WriteLine("Missing CMS Access Token");
return null;
}
@ -369,7 +374,7 @@ public class CrSeries(){
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.WriteLine("Series Request Failed");
Console.Error.WriteLine("Series Request Failed");
return null;
}
@ -385,7 +390,7 @@ public class CrSeries(){
public async Task<CrSeriesBase?> SeriesById(string id){
if (crunInstance.CmsToken?.Cms == null){
Console.WriteLine("Missing CMS Access Token");
Console.Error.WriteLine("Missing CMS Access Token");
return null;
}
@ -398,7 +403,7 @@ public class CrSeries(){
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.WriteLine("Series Request Failed");
Console.Error.WriteLine("Series Request Failed");
return null;
}

View File

@ -114,7 +114,8 @@ public class Crunchyroll{
Username = "???",
Avatar = "003-cr-hime-excited.png",
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE"
PreferredContentSubtitleLanguage = "de-DE",
HasPremium = false,
};
@ -132,14 +133,14 @@ public class Crunchyroll{
CrunOptions.Chapters = true;
CrunOptions.Hslang = "none";
CrunOptions.Force = "Y";
CrunOptions.FileName = "${showTitle} - S${season}E${episode} [${height}p]";
CrunOptions.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]";
CrunOptions.Partsize = 10;
CrunOptions.NoSubs = false;
CrunOptions.DlSubs = new List<string>{ "de-DE" };
CrunOptions.Skipmux = false;
CrunOptions.MkvmergeOptions = new List<string>{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" };
CrunOptions.DefaultAudio = Languages.FindLang("ja-JP");
CrunOptions.DefaultSub = Languages.FindLang("de-DE");
CrunOptions.FfmpegOptions = new();
CrunOptions.DefaultAudio = "ja-JP";
CrunOptions.DefaultSub = "de-DE";
CrunOptions.CcTag = "cc";
CrunOptions.FsRetryTime = 5;
CrunOptions.Numbers = 2;
@ -163,7 +164,7 @@ public class Crunchyroll{
RefreshSonarr();
}
if (CrunOptions.LogMode){
CfgManager.EnableLogMode();
} else{
@ -269,7 +270,7 @@ public class Crunchyroll{
week.CalendarDays.Add(calDay);
}
} else{
Console.WriteLine("No days found in the HTML document.");
Console.Error.WriteLine("No days found in the HTML document.");
}
calendar[weeksMondayDate] = week;
@ -285,7 +286,7 @@ public class Crunchyroll{
if (episodeL != null){
if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && (episodeL.Value.Data.First().StreamsLink == null || Profile.Username == "???")){
if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && !Profile.HasPremium){
MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3));
return;
}
@ -298,8 +299,13 @@ public class Crunchyroll{
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (historyEpisode != null){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
}
if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
}
}
}
@ -321,8 +327,13 @@ public class Crunchyroll{
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (historyEpisode != null){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
}
if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
}
}
}
@ -382,19 +393,19 @@ public class Crunchyroll{
await MuxStreams(res.Data,
new CrunchyMuxOptions{
FfmpegOptions = options.FfmpegOptions,
SkipSubMux = options.Skipmux,
SkipSubMux = options.SkipSubsMux,
Output = res.FileName,
Mp4 = options.Mp4,
VideoTitle = options.VideoTitle,
Novids = options.Novids,
NoCleanup = options.Nocleanup,
DefaultAudio = options.DefaultAudio,
DefaultSub = options.DefaultSub,
DefaultAudio = Languages.FindLang(options.DefaultAudio),
DefaultSub = Languages.FindLang(options.DefaultSub),
MkvmergeOptions = options.MkvmergeOptions,
ForceMuxer = options.Force,
SyncTiming = options.SyncTiming,
CcTag = options.CcTag,
KeepAllVideos = false
KeepAllVideos = true
},
res.FileName);
@ -423,8 +434,6 @@ public class Crunchyroll{
}
private async Task MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename){
var hasAudioStreams = false;
var muxToMp3 = false;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -437,10 +446,6 @@ public class Crunchyroll{
}
}
if (data.Any(a => a.Type == DownloadMediaType.Audio)){
hasAudioStreams = true;
}
var subs = data.Where(a => a.Type == DownloadMediaType.Subtitle).ToList();
var subsList = new List<SubtitleFonts>();
@ -465,15 +470,13 @@ public class Crunchyroll{
var merger = new Merger(new MergerOptions{
OnlyVid = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List<MergerInput>(),
OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
SkipSubMux = options.SkipSubMux,
OnlyAudio = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List<MergerInput>(),
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(),
Simul = false,
KeepAllVideos = options.KeepAllVideos,
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined
VideoAndAudio = hasAudioStreams ? new List<MergerInput>() : data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle,
Options = new MuxOptions(){
@ -489,11 +492,11 @@ public class Crunchyroll{
});
if (!File.Exists(CfgManager.PathFFMPEG)){
Console.WriteLine("FFmpeg not found");
Console.Error.WriteLine("FFmpeg not found");
}
if (!File.Exists(CfgManager.PathMKVMERGE)){
Console.WriteLine("MKVmerge not found");
Console.Error.WriteLine("MKVmerge not found");
}
bool isMuxed;
@ -645,6 +648,20 @@ public class Crunchyroll{
var fetchPlaybackData = await FetchPlaybackData(mediaId, mediaGuid, epMeta);
if (!fetchPlaybackData.IsOk){
if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){
var s = fetchPlaybackData.error;
var error = StreamError.FromJson(s);
if (error != null && error.IsTooManyActiveStreamsError()){
MainWindow.Instance.ShowError("Too many active streams that couldn't be stopped");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Too many active streams that couldn't be stopped"
};
}
}
MainWindow.Instance.ShowError("Couldn't get Playback Data");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
@ -1001,6 +1018,17 @@ public class Crunchyroll{
Console.WriteLine("Decryption Needed, attempting to decrypt");
if (!_widevine.canDecrypt){
dlFailed = true;
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
ErrorText = "Decryption Needed but couldn't find CDM files"
};
}
var reqBodyData = new{
accounting_id = "crunchyroll",
asset_id = assetId,
@ -1169,9 +1197,6 @@ public class Crunchyroll{
});
}
}
} else if (!options.Novids){
//TODO
MainWindow.Instance.ShowError("Requested Video with the current settings not implemented");
} else if (options.Novids){
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
Console.WriteLine("Downloading skipped!");
@ -1234,11 +1259,6 @@ public class Crunchyroll{
options.SkipSubs = true;
}
if (options.NoSubs){
Console.WriteLine("Subtitles downloading disabled from nosubs flag.");
options.SkipSubs = true;
}
if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){
await DownloadSubtitles(options, pbData, audDub, fileName, files);
} else{
@ -1486,12 +1506,25 @@ public class Crunchyroll{
var playbackRequestNonDrmResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequestNonDrm);
if (!playbackRequestNonDrmResponse.IsOk && playbackRequestNonDrmResponse.ResponseContent != string.Empty){
var s = playbackRequestNonDrmResponse.ResponseContent;
var error = StreamError.FromJson(s);
if (error != null && error.IsTooManyActiveStreamsError()){
foreach (var errorActiveStream in error.ActiveStreams){
await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token);
}
playbackRequestNonDrm = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{currentMediaId}/console/switch/play", HttpMethod.Get, true, true, null);
playbackRequestNonDrm.Headers.UserAgent.ParseAdd("Crunchyroll/1.8.0 Nintendo Switch/12.3.12.0 UE4/4.27");
playbackRequestNonDrmResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequestNonDrm);
}
}
if (playbackRequestNonDrmResponse.IsOk && playbackRequestNonDrmResponse.ResponseContent != string.Empty){
CrunchyStreamData? playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(playbackRequestNonDrmResponse.ResponseContent, SettingsJsonSerializerSettings);
CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams();
if (playStream != null){
var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{playStream.Token}/inactive", HttpMethod.Patch, true, false, null);
var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken);
if (playStream.Token != null) await HttpClientReq.DeAuthVideo(currentMediaId, playStream.Token);
if (playStream.HardSubs != null)
foreach (var hardsub in playStream.HardSubs){
@ -1514,7 +1547,7 @@ public class Crunchyroll{
}
}
private async Task<(bool IsOk, PlaybackData pbData)> FetchPlaybackData(string mediaId, string mediaGuidId, CrunchyEpMetaData epMeta){
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, CrunchyEpMetaData epMeta){
PlaybackData temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
bool ok = true;
@ -1569,64 +1602,86 @@ public class Crunchyroll{
//
// var playbackRequestResponse22 = await HttpClientReq.Instance.SendHttpRequest(playbackRequest22);
playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null);
playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/videos/{mediaId}/streams", HttpMethod.Get, true, false, null);
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
if (playbackRequestResponse.IsOk){
// var temppbData2 = Helpers.Deserialize<PlaybackData>(playbackRequestResponse22.ResponseContent, SettingsJsonSerializerSettings) ??
// new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData.Data.Add(new Dictionary<string, Dictionary<string, StreamDetails>>());
CrunchyStreamData? playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams();
if (playStream != null){
var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{mediaGuidId}/{playStream.Token}/inactive", HttpMethod.Patch, true, false, null);
var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken);
if (playStream.HardSubs != null)
foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url,
HardsubLocale = stream.Hlang
};
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
Url = playStream.Url,
HardsubLocale = Locale.DefaulT
};
if (temppbData.Data != null) temppbData.Data[0]["drm_adaptive_switch_dash"] = derivedPlayCrunchyStreams;
temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List<string>{ playStream.Bifs }, MediaId = mediaId };
temppbData.Meta.Subtitles = new Subtitles();
foreach (var playStreamSubtitle in playStream.Subtitles){
Subtitle sub = playStreamSubtitle.Value;
temppbData.Meta.Subtitles.Add(playStreamSubtitle.Key, new SubtitleInfo(){ Format = sub.Format, Locale = sub.Locale, Url = sub.Url });
if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){
var s = playbackRequestResponse.ResponseContent;
var error = StreamError.FromJson(s);
if (error != null && error.IsTooManyActiveStreamsError()){
foreach (var errorActiveStream in error.ActiveStreams){
await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token);
}
playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/videos/{mediaId}/streams", HttpMethod.Get, true, false, null);
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
}
}
if (!playbackRequestResponse.IsOk){
temppbData = Helpers.Deserialize<PlaybackData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
} else{
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.ApiBeta}{epMeta.Playback}", HttpMethod.Get, true, true, null);
playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null);
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){
var s = playbackRequestResponse.ResponseContent;
var error = StreamError.FromJson(s);
if (error != null && error.IsTooManyActiveStreamsError()){
foreach (var errorActiveStream in error.ActiveStreams){
await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token);
}
playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null);
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
}
}
if (playbackRequestResponse.IsOk){
temppbData = Helpers.Deserialize<PlaybackData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData.Data.Add(new Dictionary<string, Dictionary<string, StreamDetails>>());
CrunchyStreamData? playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams();
if (playStream != null){
if (playStream.Token != null) await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token);
if (playStream.HardSubs != null)
foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url,
HardsubLocale = stream.Hlang
};
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
Url = playStream.Url,
HardsubLocale = Locale.DefaulT
};
if (temppbData.Data != null) temppbData.Data[0]["drm_adaptive_switch_dash"] = derivedPlayCrunchyStreams;
temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List<string>{ playStream.Bifs }, MediaId = mediaId };
temppbData.Meta.Subtitles = new Subtitles();
foreach (var playStreamSubtitle in playStream.Subtitles){
Subtitle sub = playStreamSubtitle.Value;
temppbData.Meta.Subtitles.Add(playStreamSubtitle.Key, new SubtitleInfo(){ Format = sub.Format, Locale = sub.Locale, Url = sub.Url });
}
}
} else{
Console.WriteLine("'Fallback Request Stream URLs FAILED!'");
Console.Error.WriteLine("'Fallback Request Stream URLs FAILED!'");
ok = playbackRequestResponse.IsOk;
}
}
}
return (IsOk: ok, pbData: temppbData);
return (IsOk: ok, pbData: temppbData, error: ok ? "" : playbackRequestResponse.ResponseContent);
}
private async Task ParseChapters(string currentMediaId, List<string> compiledChapters){

View File

@ -25,7 +25,7 @@ public class History(){
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja");
if (parsedSeries == null){
Console.WriteLine("Parse Data Invalid");
Console.Error.WriteLine("Parse Data Invalid");
return;
}
@ -420,7 +420,7 @@ public class History(){
historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
episodes.Remove(episode2);
} else{
Console.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode");
Console.Error.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode");
}
}
}
@ -568,7 +568,7 @@ public class HistorySeries : INotifyPropertyChanged{
}
} catch (Exception ex){
// Handle exceptions
Console.WriteLine("Failed to load image: " + ex.Message);
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}

View File

@ -60,14 +60,14 @@ public class Widevine{
if (privateKey.Length != 0 && identifierBlob.Length != 0){
canDecrypt = true;
} else if (privateKey.Length == 0){
Console.WriteLine("Private key missing");
Console.Error.WriteLine("Private key missing");
canDecrypt = false;
} else if (identifierBlob.Length == 0){
Console.WriteLine("Identifier blob missing");
Console.Error.WriteLine("Identifier blob missing");
canDecrypt = false;
}
} catch (Exception e){
Console.WriteLine("Widevine: " + e);
Console.Error.WriteLine("Widevine: " + e);
canDecrypt = false;
}
}
@ -90,7 +90,7 @@ public class Widevine{
var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);
if (!response.IsOk){
Console.WriteLine("Fallback Request Stream URLs FAILED!");
Console.Error.WriteLine("Failed to get Keys!");
return new List<ContentKey>();
}

View File

@ -1,8 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using CRD.Downloader;
using CRD.Utils.Structs;
using Newtonsoft.Json;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
@ -23,9 +28,9 @@ public class CfgManager{
public static readonly string PathVIDEOS_DIR = WorkingDirectory + "/video/";
public static readonly string PathFONTS_DIR = WorkingDirectory + "/video/";
public static readonly string PathLogFile = WorkingDirectory + "/logfile.txt";
private static StreamWriter logFile;
private static bool isLogModeEnabled = false;
@ -33,23 +38,23 @@ public class CfgManager{
if (!isLogModeEnabled){
logFile = new StreamWriter(PathLogFile);
logFile.AutoFlush = true;
Console.SetOut(logFile);
Console.SetError(logFile);
isLogModeEnabled = true;
Console.WriteLine("Log mode enabled.");
Console.Error.WriteLine("Log mode enabled.");
}
}
public static void DisableLogMode(){
if (isLogModeEnabled){
logFile.Close();
StreamWriter standardOutput = new StreamWriter(Console.OpenStandardOutput());
standardOutput.AutoFlush = true;
Console.SetOut(standardOutput);
StreamWriter standardError = new StreamWriter(Console.OpenStandardError());
standardError.AutoFlush = true;
Console.SetError(standardError);
isLogModeEnabled = false;
Console.WriteLine("Log mode disabled.");
Console.Error.WriteLine("Log mode disabled.");
}
}
public static void WriteJsonResponseToYamlFile(string jsonResponse, string filePath){
// Convert JSON to an object
var deserializer = new DeserializerBuilder()
@ -122,6 +127,7 @@ public class CfgManager{
File.WriteAllText(PathCrDownloadOptions, yaml);
}
public static void UpdateSettingsFromFile(){
string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty;
@ -144,35 +150,46 @@ public class CfgManager{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.IgnoreUnmatchedProperties() // Important to ignore properties not present in YAML
.IgnoreUnmatchedProperties()
.Build();
var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input);
var loadedOptions = deserializer.Deserialize<CrDownloadOptions>(new StringReader(input));
var instanceOptions = Crunchyroll.Instance.CrunOptions;
Crunchyroll.Instance.CrunOptions.Hslang = loadedOptions.Hslang;
Crunchyroll.Instance.CrunOptions.Novids = loadedOptions.Novids;
Crunchyroll.Instance.CrunOptions.Noaudio = loadedOptions.Noaudio;
Crunchyroll.Instance.CrunOptions.FileName = loadedOptions.FileName;
Crunchyroll.Instance.CrunOptions.Numbers = loadedOptions.Numbers;
Crunchyroll.Instance.CrunOptions.DlSubs = loadedOptions.DlSubs;
Crunchyroll.Instance.CrunOptions.Mp4 = loadedOptions.Mp4;
Crunchyroll.Instance.CrunOptions.FfmpegOptions = loadedOptions.FfmpegOptions;
Crunchyroll.Instance.CrunOptions.MkvmergeOptions = loadedOptions.MkvmergeOptions;
Crunchyroll.Instance.CrunOptions.Chapters = loadedOptions.Chapters;
Crunchyroll.Instance.CrunOptions.SimultaneousDownloads = loadedOptions.SimultaneousDownloads;
Crunchyroll.Instance.CrunOptions.QualityAudio = loadedOptions.QualityAudio;
Crunchyroll.Instance.CrunOptions.QualityVideo = loadedOptions.QualityVideo;
Crunchyroll.Instance.CrunOptions.DubLang = loadedOptions.DubLang;
Crunchyroll.Instance.CrunOptions.Theme = loadedOptions.Theme;
Crunchyroll.Instance.CrunOptions.AccentColor = loadedOptions.AccentColor;
Crunchyroll.Instance.CrunOptions.History = loadedOptions.History;
Crunchyroll.Instance.CrunOptions.UseNonDrmStreams = loadedOptions.UseNonDrmStreams;
Crunchyroll.Instance.CrunOptions.SonarrProperties = loadedOptions.SonarrProperties;
Crunchyroll.Instance.CrunOptions.LogMode = loadedOptions.LogMode;
foreach (PropertyInfo property in typeof(CrDownloadOptions).GetProperties()){
var yamlMemberAttribute = property.GetCustomAttribute<YamlMemberAttribute>();
string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name;
if (propertiesPresentInYaml.Contains(yamlPropertyName)){
PropertyInfo instanceProperty = instanceOptions.GetType().GetProperty(property.Name);
if (instanceProperty != null && instanceProperty.CanWrite){
instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions));
}
}
}
}
private static HashSet<string> GetTopLevelPropertiesInYaml(string yamlContent){
var reader = new StringReader(yamlContent);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
var properties = new HashSet<string>();
if (yamlStream.Documents.Count > 0 && yamlStream.Documents[0].RootNode is YamlMappingNode rootNode){
foreach (var entry in rootNode.Children){
if (entry.Key is YamlScalarNode scalarKey){
properties.Add(scalarKey.Value);
}
}
}
return properties;
}
private static object fileLock = new object();
public static void WriteJsonToFile(string pathToFile, object obj){
try{
// Check if the directory exists; if not, create it.
@ -191,7 +208,7 @@ public class CfgManager{
}
}
} catch (Exception ex){
Console.WriteLine($"An error occurred: {ex.Message}");
Console.Error.WriteLine($"An error occurred: {ex.Message}");
}
}

View File

@ -22,7 +22,7 @@ public class FileNameManager{
var variable = overriddenVars.FirstOrDefault(v => v.Name == varName);
if (variable == null){
Console.WriteLine($"[ERROR] Found variable '{match}' in fileName but no values was internally found!");
Console.Error.WriteLine($"[ERROR] Found variable '{match}' in fileName but no values was internally found!");
continue;
}
@ -51,13 +51,13 @@ public class FileNameManager{
foreach (var item in overrides){
int index = item.IndexOf('=');
if (index == -1){
Console.WriteLine($"Error: Invalid override format '{item}'");
Console.Error.WriteLine($"Error: Invalid override format '{item}'");
continue;
}
string[] parts ={ item.Substring(0, index), item.Substring(index + 1) };
if (!(parts[1].StartsWith("'") && parts[1].EndsWith("'") && parts[1].Length >= 2)){
Console.WriteLine($"Error: Invalid value format for '{item}'");
Console.Error.WriteLine($"Error: Invalid value format for '{item}'");
continue;
}
@ -67,7 +67,7 @@ public class FileNameManager{
if (alreadyIndex > -1){
if (variables[alreadyIndex].Type == "number"){
if (!float.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float numberValue)){
Console.WriteLine($"Error: Wrong type for '{item}'");
Console.Error.WriteLine($"Error: Wrong type for '{item}'");
continue;
}

View File

@ -81,8 +81,8 @@ public class HlsDownloader{
$"Current: {{ total: {_data.M3U8Json?.Segments.Count} }}");
}
} catch (Exception e){
Console.WriteLine("Resume failed, downloading will not be resumed!");
Console.WriteLine(e.Message);
Console.Error.WriteLine("Resume failed, downloading will not be resumed!");
Console.Error.WriteLine(e.Message);
}
}
@ -169,7 +169,7 @@ public class HlsDownloader{
try{
await Task.WhenAll(keyTasks.Values);
} catch (Exception ex){
Console.WriteLine($"Error downloading keys: {ex.Message}");
Console.Error.WriteLine($"Error downloading keys: {ex.Message}");
throw;
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
@ -21,7 +22,7 @@ public class Helpers{
try{
return JsonConvert.DeserializeObject<T>(json, serializerSettings);
} catch (JsonException ex){
Console.WriteLine($"Error deserializing JSON: {ex.Message}");
Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}");
throw;
}
}
@ -41,7 +42,7 @@ public class Helpers{
if (string.IsNullOrEmpty(value)){
return Locale.DefaulT;
}
return Locale.Unknown; // Return default if not found
}
@ -58,6 +59,35 @@ public class Helpers{
return milliseconds + highResTimestamp;
}
public static void ConvertChapterFileForFFMPEG(string chapterFilePath)
{
var chapterLines = File.ReadAllLines(chapterFilePath);
var ffmpegChapterLines = new List<string> { ";FFMETADATA1" };
for (int i = 0; i < chapterLines.Length; i += 2)
{
var timeLine = chapterLines[i];
var nameLine = chapterLines[i + 1];
var timeParts = timeLine.Split('=');
var nameParts = nameLine.Split('=');
if (timeParts.Length == 2 && nameParts.Length == 2)
{
var startTime = TimeSpan.Parse(timeParts[1]).TotalMilliseconds;
var endTime = i + 2 < chapterLines.Length ? TimeSpan.Parse(chapterLines[i + 2].Split('=')[1]).TotalMilliseconds : startTime + 10000;
ffmpegChapterLines.Add("[CHAPTER]");
ffmpegChapterLines.Add("TIMEBASE=1/1000");
ffmpegChapterLines.Add($"START={startTime}");
ffmpegChapterLines.Add($"END={endTime}");
ffmpegChapterLines.Add($"title={nameParts[1]}");
}
}
File.WriteAllLines(chapterFilePath, ffmpegChapterLines);
}
public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsync(string type, string bin, string command){
using (var process = new Process()){
process.StartInfo.FileName = bin;
@ -75,7 +105,7 @@ public class Helpers{
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine($"ERROR: {e.Data}");
Console.WriteLine($"{e.Data}");
}
};
@ -137,7 +167,7 @@ public class Helpers{
return words;
}
private static double CosineSimilarity(Dictionary<string, double> vector1, Dictionary<string, double> vector2){
var intersection = vector1.Keys.Intersect(vector2.Keys);

View File

@ -97,7 +97,7 @@ public class HttpClientReq{
return (IsOk: true, ResponseContent: content);
} catch (Exception e){
Console.WriteLine($"Error: {e} \n Response: {content}");
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
return (IsOk: false, ResponseContent: content);
}
}
@ -124,6 +124,11 @@ public class HttpClientReq{
return request;
}
public static async Task DeAuthVideo(string currentMediaId, string token){
var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{token}/inactive", HttpMethod.Patch, true, false, null);
var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken);
}
public HttpClient GetHttpClient(){
return client;
}
@ -132,7 +137,7 @@ public class HttpClientReq{
public static class Api{
public static readonly string ApiBeta = "https://beta-api.crunchyroll.com";
public static readonly string ApiN = "https://crunchyroll.com";
public static readonly string ApiN = "https://www.crunchyroll.com";
public static readonly string BetaAuth = ApiBeta + "/auth/v1/token";
public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile";
@ -143,7 +148,7 @@ public static class Api{
public static readonly string BetaCms = ApiBeta + "/cms/v2";
public static readonly string DRM = ApiBeta + "/drm/v1/auth";
public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/";
public static readonly string CmsN = ApiN + "/content/v2/cms";

View File

@ -14,7 +14,7 @@ public class Merger{
public Merger(MergerOptions options){
this.options = options;
if (this.options.SkipSubMux != null && this.options.SkipSubMux == true){
this.options.Subtitles = new List<SubtitleInput>();
this.options.Subtitles = new();
}
if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
@ -32,31 +32,11 @@ public class Merger{
var hasVideo = false;
if (!options.mp3){
foreach (var vid in options.VideoAndAudio){
if (vid.Delay != null && hasVideo){
args.Add($"-itsoffset -{Math.Ceiling((double)vid.Delay * 1000)}ms");
}
args.Add($"-i \"{vid.Path}\"");
if (!hasVideo || options.KeepAllVideos == true){
metaData.Add($"-map {index}:a -map {index}:v");
metaData.Add($"-metadata:s:a:{audioIndex} language={vid.Language.Code}");
metaData.Add($"-metadata:s:v:{index} title=\"{options.VideoTitle}\"");
hasVideo = true;
} else{
metaData.Add($"-map {index}:a");
metaData.Add($"-metadata:s:a:{audioIndex} language={vid.Language.Code}");
}
audioIndex++;
index++;
}
foreach (var vid in options.OnlyVid){
if (!hasVideo || options.KeepAllVideos == true){
args.Add($"-i \"{vid.Path}\"");
metaData.Add($"-map {index} -map -{index}:a");
metaData.Add($"-metadata:s:v:{index} title=\"{options.VideoTitle}\"");
metaData.Add($"-map {index}:v");
metaData.Add($"-metadata:s:v:{index} title=\"{(options.VideoTitle ?? vid.Language.Name)}\"");
hasVideo = true;
index++;
}
@ -64,12 +44,21 @@ public class Merger{
foreach (var aud in options.OnlyAudio){
args.Add($"-i \"{aud.Path}\"");
metaData.Add($"-map {index}");
metaData.Add($"-map {index}:a");
metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}");
index++;
audioIndex++;
}
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");
@ -77,7 +66,7 @@ public class Merger{
args.Add($"-i \"{sub.value.File}\"");
}
if (options.Output.EndsWith(".mkv", StringComparison.OrdinalIgnoreCase)){
if (options.Fonts != null){
int fontIndex = 0;
@ -87,7 +76,7 @@ public class Merger{
}
}
}
args.AddRange(metaData);
args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
args.Add("-c:v copy");
@ -95,6 +84,9 @@ public class Merger{
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
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 (options.Options.ffmpeg?.Count > 0){
args.AddRange(options.Options.ffmpeg);
}
@ -112,13 +104,15 @@ public class Merger{
index++;
audioIndex++;
}
args.Add("-acodec libmp3lame");
args.Add("-ab 192k");
args.Add($"\"{options.Output}\"");
return string.Join(" ", args);
}
public string MkvMerge(){
List<string> args = new List<string>();
@ -135,7 +129,7 @@ public class Merger{
args.Add("--video-tracks 0");
args.Add("--no-audio");
string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}{(options.Simul == true ? " [Simulcast]" : " [Uncut]")}";
string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}";
args.Add($"--track-name 0:\"{trackName}\"");
args.Add($"--language 0:{vid.Language.Code}");
@ -144,47 +138,6 @@ public class Merger{
}
}
foreach (var vid in options.VideoAndAudio){
string audioTrackNum = options.InverseTrackOrder == true ? "0" : "1";
string videoTrackNum = options.InverseTrackOrder == true ? "1" : "0";
if (vid.Delay.HasValue){
double delay = vid.Delay ?? 0;
args.Add($"--sync {audioTrackNum}:-{Math.Ceiling(delay * 1000)}");
}
if (!hasVideo || options.KeepAllVideos == true){
args.Add($"--video-tracks {videoTrackNum}");
args.Add($"--audio-tracks {audioTrackNum}");
string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}{(options.Simul == true ? " [Simulcast]" : " [Uncut]")}";
args.Add($"--track-name 0:\"{trackName}\""); // Assuming trackName applies to video if present
args.Add($"--language {audioTrackNum}:{vid.Language.Code}");
if (options.Defaults.Audio.Code == vid.Language.Code){
args.Add($"--default-track {audioTrackNum}");
} else{
args.Add($"--default-track {audioTrackNum}:0");
}
hasVideo = true;
} else{
args.Add("--no-video");
args.Add($"--audio-tracks {audioTrackNum}");
if (options.Defaults.Audio.Code == vid.Language.Code){
args.Add($"--default-track {audioTrackNum}");
} else{
args.Add($"--default-track {audioTrackNum}:0");
}
args.Add($"--track-name {audioTrackNum}:\"{vid.Language.Name}\"");
args.Add($"--language {audioTrackNum}:{vid.Language.Code}");
}
args.Add($"\"{vid.Path}\"");
}
foreach (var aud in options.OnlyAudio){
string trackName = aud.Language.Name;
args.Add($"--track-name 0:\"{trackName}\"");
@ -245,51 +198,6 @@ public class Merger{
return string.Join(" ", args);
}
// public async Task CreateDelays(){
// // Don't bother scanning if there is only 1 vna stream
// if (options.VideoAndAudio.Count > 1){
// var bin = await YamlCfg.LoadBinCfg();
// var vnas = this.options.VideoAndAudio;
//
// // Get and set durations on each videoAndAudio Stream
// foreach (var vna in vnas){
// var streamInfo = await FFProbe(vna.Path, bin.FFProbe);
// var videoInfo = streamInfo.Streams.Where(stream => stream.CodecType == "video").FirstOrDefault();
// vna.Duration = int.Parse(videoInfo.Duration);
// }
//
// // Sort videoAndAudio streams by duration (shortest first)
// vnas.Sort((a, b) => {
// if (a.Duration == 0 || b.Duration == 0) return -1;
// return a.Duration.CompareTo(b.Duration);
// });
//
// // Set Delays
// var shortestDuration = vnas[0].Duration;
// foreach (var (vna, index) in vnas.Select((vna, index) => (vna, index))){
// // Don't calculate the shortestDuration track
// if (index == 0){
// if (!vna.IsPrimary)
// Console.WriteLine("Shortest video isn't primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.");
// continue;
// }
//
// if (vna.Duration > 0 && shortestDuration > 0){
// // Calculate the tracks delay
// vna.Delay = Math.Ceiling((vna.Duration - shortestDuration) * 1000) / 1000;
//
// var subtitles = this.options.Subtitles.Where(sub => sub.Language.Code == vna.Lang.Code).ToList();
// foreach (var (sub, subIndex) in subtitles.Select((sub, subIndex) => (sub, subIndex))){
// if (vna.IsPrimary)
// subtitles[subIndex].Delay = vna.Delay;
// else if (sub.ClosedCaption)
// subtitles[subIndex].Delay = vna.Delay;
// }
// }
// }
// }
// }
public async Task Merge(string type, string bin){
string command = type switch{
@ -299,7 +207,7 @@ public class Merger{
};
if (string.IsNullOrEmpty(command)){
Console.WriteLine("Unable to merge files.");
Console.Error.WriteLine("Unable to merge files.");
return;
}
@ -309,7 +217,7 @@ public class Merger{
if (!result.IsOk && type == "mkvmerge" && result.ErrorCode == 1){
Console.WriteLine($"[{type}] Mkvmerge finished with at least one warning");
} else if (!result.IsOk){
Console.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}");
Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}");
} else{
Console.WriteLine($"[{type} Done]");
}
@ -319,7 +227,7 @@ public class Merger{
public void CleanUp(){
// Combine all media file lists and iterate through them
var allMediaFiles = options.OnlyAudio.Concat(options.OnlyVid)
.Concat(options.VideoAndAudio).ToList();
.ToList();
allMediaFiles.ForEach(file => DeleteFile(file.Path));
allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume"));
@ -336,7 +244,7 @@ public class Merger{
File.Delete(filePath);
}
} catch (Exception ex){
Console.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}");
Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}");
// Handle exceptions if you need to log them or throw
}
}
@ -382,7 +290,6 @@ public class CrunchyMuxOptions{
}
public class MergerOptions{
public List<MergerInput> VideoAndAudio{ get; set; } = new List<MergerInput>();
public List<MergerInput> OnlyVid{ get; set; } = new List<MergerInput>();
public List<MergerInput> OnlyAudio{ get; set; } = new List<MergerInput>();
public List<SubtitleInput> Subtitles{ get; set; } = new List<SubtitleInput>();
@ -390,8 +297,6 @@ public class MergerOptions{
public string CcTag{ get; set; }
public string Output{ get; set; }
public string VideoTitle{ get; set; }
public bool? Simul{ get; set; }
public bool? InverseTrackOrder{ get; set; }
public bool? KeepAllVideos{ get; set; }
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>();
public bool? SkipSubMux{ get; set; }

View File

@ -92,7 +92,7 @@ public class SonarrClient{
series = JsonConvert.DeserializeObject<List<SonarrSeries>>(json) ?? [];
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e);
Console.WriteLine("Sonarr GetSeries error \n" + e);
Console.Error.WriteLine("Sonarr GetSeries error \n" + e);
}
return series;
@ -107,7 +107,7 @@ public class SonarrClient{
episodes = JsonConvert.DeserializeObject<List<SonarrEpisode>>(json) ?? [];
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetEpisodes error \n" + e);
Console.WriteLine("Sonarr GetEpisodes error \n" + e);
Console.Error.WriteLine("Sonarr GetEpisodes error \n" + e);
}
return episodes;
@ -121,7 +121,7 @@ public class SonarrClient{
episode = JsonConvert.DeserializeObject<SonarrEpisode>(json) ?? new SonarrEpisode();
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetEpisode error \n" + e);
Console.WriteLine("Sonarr GetEpisode error \n" + e);
Console.Error.WriteLine("Sonarr GetEpisode error \n" + e);
}
return episode;

View File

@ -63,7 +63,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
}
} catch (Exception ex){
// Handle exceptions
Console.WriteLine("Failed to load image: " + ex.Message);
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
}

View File

@ -50,9 +50,9 @@ public class CrDownloadOptions{
[YamlIgnore]
public bool SkipSubs{ get; set; }
[YamlIgnore]
public bool NoSubs{ get; set; }
[YamlMember(Alias = "mux_skip_subs", ApplyNamingConventions = false)]
public bool SkipSubsMux{ get; set; }
[YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)]
public bool Mp4{ get; set; }
@ -71,16 +71,16 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)]
public List<string> MkvmergeOptions{ get; set; }
[YamlIgnore]
public LanguageItem DefaultSub{ get; set; }
[YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)]
public string DefaultSub{ get; set; }
[YamlIgnore]
public LanguageItem DefaultAudio{ get; set; }
[YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)]
public string DefaultAudio{ get; set; }
[YamlIgnore]
public string CcTag{ get; set; }
[YamlIgnore]
[YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
public bool DlVideoOnce{ get; set; }
[YamlIgnore]

View File

@ -1,4 +1,6 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CRD.Utils.Structs;
@ -12,4 +14,44 @@ public class CrProfile{
[JsonProperty("preferred_content_subtitle_language")]
public string? PreferredContentSubtitleLanguage{ get; set; }
[JsonIgnore]
public Subscription? Subscription{ get; set; }
[JsonIgnore]
public bool HasPremium{ get; set; }
}
public class Subscription{
[JsonProperty("account_id")]
public int AccountId{ get; set; }
[JsonProperty("ctp_account_id")]
public string? CtpAccountId{ get; set; }
[JsonProperty("cycle_duration")]
public string? CycleDuration{ get; set; }
[JsonProperty("next_renewal_date")]
public DateTime NextRenewalDate{ get; set; }
[JsonProperty("currency_code")]
public string? CurrencyCode{ get; set; }
[JsonProperty("is_active")]
public bool IsActive{ get; set; }
[JsonProperty("tax_included")]
public bool TaxIncluded{ get; set; }
[JsonProperty("subscription_products")]
public List<SubscriptionProduct>? SubscriptionProducts{ get; set; }
}
public class SubscriptionProduct{
[JsonProperty("currency_code")]
public string? CurrencyCode{ get; set; }
public string? Amount{ get; set; }
[JsonProperty("is_cancelled")]
public bool IsCancelled{ get; set; }
[JsonProperty("effective_date")]
public DateTime EffectiveDate{ get; set; }
public string? Sku{ get; set; }
public string? Tier{ get; set; }
[JsonProperty("active_free_trial")]
public bool ActiveFreeTrial{ get; set; }
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public class StreamError{
[JsonPropertyName("error")]
public string Error{ get; set; }
[JsonPropertyName("activeStreams")]
public List<ActiveStream> ActiveStreams{ get; set; }
public static StreamError? FromJson(string json){
try{
return JsonConvert.DeserializeObject<StreamError>(json);
} catch (Exception e){
Console.Error.WriteLine(e);
return null;
}
}
public bool IsTooManyActiveStreamsError(){
return Error == "TOO_MANY_ACTIVE_STREAMS";
}
}
public class ActiveStream{
[JsonPropertyName("deviceSubtype")]
public string DeviceSubtype{ get; set; }
[JsonPropertyName("accountId")]
public string AccountId{ get; set; }
[JsonPropertyName("deviceType")]
public string DeviceType{ get; set; }
[JsonPropertyName("subscription")]
public string Subscription{ get; set; }
[JsonPropertyName("maxKeepAliveSeconds")]
public int MaxKeepAliveSeconds{ get; set; }
[JsonPropertyName("ttl")]
public int Ttl{ get; set; }
[JsonPropertyName("episodeIdentity")]
public string EpisodeIdentity{ get; set; }
[JsonPropertyName("tabId")]
public string TabId{ get; set; }
[JsonPropertyName("country")]
public string Country{ get; set; }
[JsonPropertyName("clientId")]
public string ClientId{ get; set; }
[JsonPropertyName("active")]
public bool Active{ get; set; }
[JsonPropertyName("deviceId")]
public string DeviceId{ get; set; }
[JsonPropertyName("token")]
public string Token{ get; set; }
[JsonPropertyName("assetId")]
public string AssetId{ get; set; }
[JsonPropertyName("sessionType")]
public string SessionType{ get; set; }
[JsonPropertyName("contentId")]
public string ContentId{ get; set; }
[JsonPropertyName("usesStreamLimits")]
public bool UsesStreamLimits{ get; set; }
[JsonPropertyName("playbackType")]
public string PlaybackType{ get; set; }
[JsonPropertyName("pk")]
public string Pk{ get; set; }
[JsonPropertyName("id")]
public string Id{ get; set; }
[JsonPropertyName("createdTimestamp")]
public long CreatedTimestamp{ get; set; }
[JsonPropertyName("lastKeepAliveTimestamp")]
public long LastKeepAliveTimestamp{ get; set; }
}

View File

@ -73,7 +73,7 @@ public class Updater : INotifyPropertyChanged{
}
}
} catch (Exception e){
Console.WriteLine("Failed to get Update information");
Console.Error.WriteLine("Failed to get Update information");
return false;
}
}
@ -119,11 +119,11 @@ public class Updater : INotifyPropertyChanged{
ApplyUpdate(extractPath);
} else{
Console.WriteLine("Failed to get Update");
Console.Error.WriteLine("Failed to get Update");
}
}
} catch (Exception e){
Console.WriteLine($"Failed to get Update: {e.Message}");
Console.Error.WriteLine($"Failed to get Update: {e.Message}");
}
}

View File

@ -1,31 +1,73 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Utils.Structs;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
namespace CRD.ViewModels;
public partial class AccountPageViewModel : ViewModelBase{
[ObservableProperty] private Bitmap? _profileImage;
[ObservableProperty]
private Bitmap? _profileImage;
[ObservableProperty] private string _profileName = "";
[ObservableProperty]
private string _profileName = "";
[ObservableProperty] private string _loginLogoutText = "";
[ObservableProperty]
private string _loginLogoutText = "";
[ObservableProperty]
private string _remainingTime = "";
private static DispatcherTimer? _timer;
private DateTime _targetTime;
private bool IsCancelled = false;
public AccountPageViewModel(){
UpdatetProfile();
}
private void Timer_Tick(object sender, EventArgs e){
var remaining = _targetTime - DateTime.UtcNow;
if (remaining <= TimeSpan.Zero){
RemainingTime = "No active Subscription";
_timer.Stop();
} else{
RemainingTime = $"{(IsCancelled ? "Subscription ending in: ":"Subscription refreshing in: ")}{remaining:dd\\:hh\\:mm\\:ss}";
}
}
public void UpdatetProfile(){
ProfileName = Crunchyroll.Instance.Profile.Username; // Default or fetched user name
LoginLogoutText = Crunchyroll.Instance.Profile.Username == "???" ? "Login" : "Logout"; // Default state
LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" + Crunchyroll.Instance.Profile.Avatar);
if (Crunchyroll.Instance.Profile.Subscription != null && Crunchyroll.Instance.Profile.Subscription?.SubscriptionProducts != null){
var sub = Crunchyroll.Instance.Profile.Subscription?.SubscriptionProducts.First();
_targetTime = Crunchyroll.Instance.Profile.Subscription.NextRenewalDate;
if (sub != null){
IsCancelled = sub.IsCancelled;
}
_timer = new DispatcherTimer{
Interval = TimeSpan.FromSeconds(1)
};
_timer.Tick += Timer_Tick;
_timer.Start();
} else{
RemainingTime = "No active Subscription";
}
}
[RelayCommand]
@ -60,7 +102,7 @@ public partial class AccountPageViewModel : ViewModelBase{
}
} catch (Exception ex){
// Handle exceptions
Console.WriteLine("Failed to load image: " + ex.Message);
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@ -156,7 +156,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
} else{
Console.WriteLine("Unnkown input");
Console.Error.WriteLine("Unnkown input");
}
}
@ -248,7 +248,7 @@ public class ItemModel(string imageUrl, string description, string time, string
}
} catch (Exception ex){
// Handle exceptions
Console.WriteLine("Failed to load image: " + ex.Message);
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
}

View File

@ -42,7 +42,7 @@ public partial class DownloadsPageViewModel : ViewModelBase{
if (downloadItem != null){
Crunchyroll.Instance.DownloadItemModels.Remove(downloadItem);
} else{
Console.WriteLine("Failed to Remove Episode from list");
Console.Error.WriteLine("Failed to Remove Episode from list");
}
}
}
@ -252,7 +252,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
}
} catch (Exception ex){
// Handle exceptions
Console.WriteLine("Failed to load image: " + ex.Message);
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
}

View File

@ -37,7 +37,7 @@ public partial class MainWindowViewModel : ViewModelBase{
File.Delete(backupFilePath);
Console.WriteLine($"Deleted old updater file: {backupFilePath}");
} catch (Exception ex) {
Console.WriteLine($"Failed to delete old updater file: {ex.Message}");
Console.Error.WriteLine($"Failed to delete old updater file: {ex.Message}");
}
} else {
Console.WriteLine("No old updater file found to delete.");

View File

@ -87,7 +87,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
UseShellExecute = true
});
} catch (Exception e){
Console.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}");
Console.Error.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}");
}
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net.Mime;
using System.Reflection;
@ -34,6 +35,12 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _muxToMp4;
[ObservableProperty]
private bool _downloadVideoForEveryDub;
[ObservableProperty]
private bool _skipSubMux;
[ObservableProperty]
private bool _history;
@ -51,10 +58,16 @@ public partial class SettingsPageViewModel : ViewModelBase{
private string _fileName = "";
[ObservableProperty]
private string _mkvMergeOptions = "";
private ObservableCollection<MuxingParam> _mkvMergeOptions = new();
[ObservableProperty]
private string _ffmpegOptions = "";
private string _mkvMergeOption = "";
[ObservableProperty]
private string _ffmpegOption = "";
[ObservableProperty]
private ObservableCollection<MuxingParam> _ffmpegOptions = new();
[ObservableProperty]
private string _selectedSubs = "all";
@ -68,6 +81,12 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedDubLang = new();
[ObservableProperty]
private ComboBoxItem _selectedDefaultDubLang;
[ObservableProperty]
private ComboBoxItem _selectedDefaultSubLang;
[ObservableProperty]
private ComboBoxItem? _selectedVideoQuality;
@ -103,7 +122,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _sonarrUseSonarrNumbering = false;
[ObservableProperty]
private bool _logMode = false;
@ -190,6 +209,14 @@ public partial class SettingsPageViewModel : ViewModelBase{
public ObservableCollection<ComboBoxItem> DubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = new(){
};
public ObservableCollection<ListBoxItem> SubLangList{ get; } = new(){
new ListBoxItem(){ Content = "all" },
new ListBoxItem(){ Content = "none" },
@ -210,6 +237,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
DubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
}
CrDownloadOptions options = Crunchyroll.Instance.CrunOptions;
@ -217,6 +246,12 @@ public partial class SettingsPageViewModel : ViewModelBase{
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null;
SelectedHSLang = hsLang ?? HardSubLangList[0];
ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null;
SelectedDefaultDubLang = defaultDubLang ?? DefaultDubLangList[0];
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear();
@ -246,8 +281,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
UseNonDrmEndpoint = options.UseNonDrmStreams;
DownloadVideo = !options.Novids;
DownloadAudio = !options.Noaudio;
DownloadVideoForEveryDub = !options.DlVideoOnce;
DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4;
SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers;
FileName = options.FileName;
SimultaneousDownloads = options.SimultaneousDownloads;
@ -268,11 +305,26 @@ public partial class SettingsPageViewModel : ViewModelBase{
History = options.History;
//TODO - Mux Options
MkvMergeOptions.Clear();
if (options.MkvmergeOptions != null){
foreach (var mkvmergeParam in options.MkvmergeOptions){
MkvMergeOptions.Add(new MuxingParam(){ ParamValue = mkvmergeParam });
}
}
FfmpegOptions.Clear();
if (options.FfmpegOptions != null){
foreach (var ffmpegParam in options.FfmpegOptions){
FfmpegOptions.Add(new MuxingParam(){ ParamValue = ffmpegParam });
}
}
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
MkvMergeOptions.CollectionChanged += Changes;
FfmpegOptions.CollectionChanged += Changes;
settingsLoaded = true;
}
@ -285,8 +337,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.Novids = !DownloadVideo;
Crunchyroll.Instance.CrunOptions.Noaudio = !DownloadAudio;
Crunchyroll.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
Crunchyroll.Instance.CrunOptions.Chapters = DownloadChapters;
Crunchyroll.Instance.CrunOptions.Mp4 = MuxToMp4;
Crunchyroll.Instance.CrunOptions.SkipSubsMux = SkipSubMux;
Crunchyroll.Instance.CrunOptions.Numbers = LeadingNumbers;
Crunchyroll.Instance.CrunOptions.FileName = FileName;
@ -302,6 +356,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang;
Crunchyroll.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
Crunchyroll.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
List<string> dubLangs = new List<string>();
foreach (var listBoxItem in SelectedDubLang){
@ -340,9 +396,20 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.SonarrProperties = props;
Crunchyroll.Instance.CrunOptions.LogMode = LogMode;
//TODO - Mux Options
List<string> mkvmergeParams = new List<string>();
foreach (var mkvmergeParam in MkvMergeOptions){
mkvmergeParams.Add(mkvmergeParam.ParamValue);
}
Crunchyroll.Instance.CrunOptions.MkvmergeOptions = mkvmergeParams;
List<string> ffmpegParams = new List<string>();
foreach (var ffmpegParam in FfmpegOptions){
ffmpegParams.Add(ffmpegParam.ParamValue);
}
Crunchyroll.Instance.CrunOptions.FfmpegOptions = ffmpegParams;
CfgManager.WriteSettingsToFile();
@ -369,6 +436,32 @@ public partial class SettingsPageViewModel : ViewModelBase{
}
}
[RelayCommand]
public void AddMkvMergeParam(){
MkvMergeOptions.Add(new MuxingParam(){ ParamValue = MkvMergeOption });
MkvMergeOption = "";
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void RemoveMkvMergeParam(MuxingParam param){
MkvMergeOptions.Remove(param);
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void AddFfmpegParam(){
FfmpegOptions.Add(new MuxingParam(){ ParamValue = FfmpegOption });
FfmpegOption = "";
RaisePropertyChanged(nameof(FfmpegOptions));
}
[RelayCommand]
public void RemoveFfmpegParam(MuxingParam param){
FfmpegOptions.Remove(param);
RaisePropertyChanged(nameof(FfmpegOptions));
}
partial void OnCurrentAppThemeChanged(ComboBoxItem? value){
if (value?.Content?.ToString() == "System"){
_faTheme.PreferSystemTheme = true;
@ -419,7 +512,6 @@ public partial class SettingsPageViewModel : ViewModelBase{
UpdateSettings();
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings();
}
@ -500,4 +592,24 @@ public partial class SettingsPageViewModel : ViewModelBase{
CfgManager.DisableLogMode();
}
}
partial void OnSelectedDefaultDubLangChanged(ComboBoxItem value){
UpdateSettings();
}
partial void OnSelectedDefaultSubLangChanged(ComboBoxItem value){
UpdateSettings();
}
partial void OnSkipSubMuxChanged(bool value){
UpdateSettings();
}
partial void OnDownloadVideoForEveryDubChanged(bool value){
UpdateSettings();
}
}
public class MuxingParam{
public string ParamValue{ get; set; }
}

View File

@ -22,10 +22,13 @@
</Image>
<!-- Profile Name -->
<TextBlock Text="{Binding ProfileName}" TextAlignment="Center" FontSize="20" Margin="10" />
<TextBlock Text="{Binding ProfileName}" HorizontalAlignment="Center" TextAlignment="Center" FontSize="20" Margin="10" />
<!-- Subscription End -->
<TextBlock x:Name="SubscriptionTextBlock" FontSize="24" Text="{Binding RemainingTime, Mode=OneWay}" TextAlignment="Center" HorizontalAlignment="Center" />
<!-- Login/Logout Button -->
<Button Content="{Binding LoginLogoutText}" Width="170" Margin="20" Command="{Binding Button_PressCommand}" />
<Button Content="{Binding LoginLogoutText}" HorizontalAlignment="Center" Width="170" Margin="20" Command="{Binding Button_PressCommand}" />
</StackPanel>

View File

@ -20,7 +20,7 @@
<controls:SettingsExpander Header="Dub language"
IconSource="Speaker2"
Description="Change the selected dub language">
Description="Change the selected dub language (with multiple dubs some can be out of sync)">
<controls:SettingsExpander.Footer>
<!-- <ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" -->
<!-- -->
@ -137,6 +137,12 @@
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Video for every dub">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadVideoForEveryDub}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Video Quality">
<controls:SettingsExpanderItem.Footer>
@ -218,18 +224,102 @@
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Keep Subtitles separate">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SkipSubMux}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Default Audio ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultDubLangList}"
SelectedItem="{Binding SelectedDefaultDubLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Default Subtitle ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultSubLangList}"
SelectedItem="{Binding SelectedDefaultSubLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Additional MKVMerge Options">
<controls:SettingsExpanderItem.Footer>
<TextBox IsEnabled="False" Name="TargetTextBox2" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding MkvMergeOptions}" />
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Name="TargetTextBox2" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding MkvMergeOption }"></TextBox>
<Button HorizontalAlignment="Center" Margin="5 0" Command="{Binding AddMkvMergeParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding MkvMergeOptions}" Margin="0,5" MaxWidth="350">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#4a4a4a" Background="#4a4a4a" BorderThickness="1" CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding ParamValue}" Margin="5,0"/>
<Button Content="X" FontSize="10" VerticalAlignment="Center" HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:SettingsPageViewModel)DataContext).RemoveMkvMergeParam}" CommandParameter="{Binding .}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Additional FFMpeg Options">
<controls:SettingsExpanderItem.Footer>
<TextBox IsEnabled="False" Name="TargetTextBox3" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FfmpegOptions}" />
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FfmpegOption }"></TextBox>
<Button HorizontalAlignment="Center" Margin="5 0" Command="{Binding AddFfmpegParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding FfmpegOptions}" Margin="0,5" MaxWidth="350">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#4a4a4a" Background="#4a4a4a" BorderThickness="1" CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding ParamValue}" Margin="5,0"/>
<Button Content="X" FontSize="10" VerticalAlignment="Center" HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:SettingsPageViewModel)DataContext).RemoveFfmpegParam}" CommandParameter="{Binding .}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>

View File

@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using CRD.Downloader;
@ -18,4 +19,5 @@ public partial class SettingsPageView : UserControl{
Crunchyroll.Instance.RefreshSonarr();
}
}
}