Add - Added **toggle to count missing history episodes** instead of just new episodes

Add - Added **Fast Add button** to the Seasons tab
Chg - Changed **"Couldn't sync dubs" message** to include the specific failed dubs and added more details to the log
Chg - Changed **history episode addition** to maintain order (slightly slower when adding but prevents queue mixing)
Chg - Changed **language sorting** for improved clarity
Chg - Changed **subtitle locale handling** to use the actual locale instead of Crunchyroll's local language tag
Chg - Changed **filename format** when downloading all dubs to separate files to include the locale in the filename
Fix - Fixed **download retries not being logged**
Fix - Fixed **dubbed episodes added from the calendar** not updating correctly in the history
Fix - Fixed **DRM authentication issue**
This commit is contained in:
Elwador 2025-05-06 18:31:44 +02:00
parent 4f6d0f2257
commit ae0f936ff5
22 changed files with 415 additions and 448 deletions

View file

@ -82,13 +82,7 @@ public class CrEpisode(){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){
// Push to arrays if there are no duplicates of the same language // Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode); episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem{ episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? Languages.DEFAULT_lang);
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
});
} }
} }
} else{ } else{
@ -98,13 +92,7 @@ public class CrEpisode(){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){
// Push to arrays if there are no duplicates of the same language // Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode); episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? new LanguageItem{ episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? Languages.DEFAULT_lang);
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
});
} }
} }

View file

@ -343,40 +343,6 @@ public class CrSeries{
return episodeList; return episodeList;
} }
public Dictionary<int, Dictionary<string, SeriesSearchItem>> ParseSeriesResult(CrSeriesSearch seasonsList){
var ret = new Dictionary<int, Dictionary<string, SeriesSearchItem>>();
int i = 0;
if (seasonsList.Data == null) return ret;
foreach (var item in seasonsList.Data){
i++;
foreach (var lang in Languages.languages){
int seasonNumber = item.SeasonNumber;
if (item.Versions != null){
seasonNumber = i;
}
if (!ret.ContainsKey(seasonNumber)){
ret[seasonNumber] = new Dictionary<string, SeriesSearchItem>();
}
if (item.Title.Contains($"({lang.Name} Dub)") || item.Title.Contains($"({lang.Name})")){
ret[seasonNumber][lang.Code] = item;
} else if (item.IsSubbed && !item.IsDubbed && lang.Code == "jpn"){
ret[seasonNumber][lang.Code] = item;
} else if (item.IsDubbed && lang.Code == "eng" && !Languages.languages.Any(a => (item.Title.Contains($"({a.Name})") || item.Title.Contains($"({a.Name} Dub)")))){
// Dubbed with no more infos will be treated as eng dubs
ret[seasonNumber][lang.Code] = item;
} else if (item.AudioLocale == lang.CrLocale){
ret[seasonNumber][lang.Code] = item;
}
}
}
return ret;
}
public async Task<CrSeriesSearch?> ParseSeriesById(string id, string? crLocale, bool forced = false){ public async Task<CrSeriesSearch?> ParseSeriesById(string id, string? crLocale, bool forced = false){
await crunInstance.CrAuth.RefreshToken(true); await crunInstance.CrAuth.RefreshToken(true);
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);

View file

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -296,6 +297,7 @@ public class CrunchyrollManager{
await CrAuth.AuthAnonymous(); await CrAuth.AuthAnonymous();
} }
if (CrunOptions.History){ if (CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){ if (File.Exists(CfgManager.PathCrHistory)){
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory); var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
@ -329,6 +331,12 @@ public class CrunchyrollManager{
await SonarrClient.Instance.RefreshSonarr(); await SonarrClient.Instance.RefreshSonarr();
} }
//Fix hslang - can be removed in a future version
var lang = Languages.Locale2language(CrunOptions.Hslang);
if (lang != Languages.DEFAULT_lang){
CrunOptions.Hslang = lang.CrLocale;
}
} }
@ -363,6 +371,7 @@ public class CrunchyrollManager{
if (options.SkipMuxing == false){ if (options.SkipMuxing == false){
bool syncError = false; bool syncError = false;
bool muxError = false; bool muxError = false;
var notSyncedDubs = "";
data.DownloadProgress = new DownloadProgress(){ data.DownloadProgress = new DownloadProgress(){
IsDownloading = true, IsDownloading = true,
@ -382,6 +391,7 @@ public class CrunchyrollManager{
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty) ? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty); : Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){ if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data); var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>(); var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){ foreach (var keyValue in groupByDub){
@ -391,7 +401,7 @@ public class CrunchyrollManager{
SubLangList = options.DlSubs, SubLangList = options.DlSubs,
FfmpegOptions = options.FfmpegOptions, FfmpegOptions = options.FfmpegOptions,
SkipSubMux = options.SkipSubsMux, SkipSubMux = options.SkipSubsMux,
Output = fileNameAndPath, Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}",
Mp4 = options.Mp4, Mp4 = options.Mp4,
MuxFonts = options.MuxFonts, MuxFonts = options.MuxFonts,
VideoTitle = res.VideoTitle, VideoTitle = res.VideoTitle,
@ -411,7 +421,7 @@ public class CrunchyrollManager{
CcSubsMuxingFlag = options.CcSubsMuxingFlag, CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced, SignsSubsAsForced = options.SignsSubsAsForced,
}, },
fileNameAndPath, data); fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", data);
if (result is{ merger: not null, isMuxed: true }){ if (result is{ merger: not null, isMuxed: true }){
mergers.Add(result.merger); mergers.Add(result.merger);
@ -479,6 +489,7 @@ public class CrunchyrollManager{
fileNameAndPath, data); fileNameAndPath, data);
syncError = result.syncError; syncError = result.syncError;
notSyncedDubs = result.notSyncedDubs;
muxError = !result.isMuxed; muxError = !result.isMuxed;
if (result is{ merger: not null, isMuxed: true }){ if (result is{ merger: not null, isMuxed: true }){
@ -512,7 +523,7 @@ public class CrunchyrollManager{
Percent = 100, Percent = 100,
Time = 0, Time = 0,
DownloadSpeed = 0, DownloadSpeed = 0,
Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? " - Couldn't sync dubs" : "") Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? $" - Couldn't sync dubs ({notSyncedDubs})" : "")
}; };
if (CrunOptions.RemoveFinishedDownload && !syncError){ if (CrunOptions.RemoveFinishedDownload && !syncError){
@ -551,7 +562,8 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){ if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){
History.SetAsDownloaded(data.SeriesId, data.SeasonId, data.Data.First().MediaId); var ids = data.Data.First().GetOriginalIds();
History.SetAsDownloaded(data.SeriesId, ids.seasonID ?? data.SeasonId, ids.guid ?? data.Data.First().MediaId);
} }
if (options.MarkAsWatched && data.Data is{ Count: > 0 }){ if (options.MarkAsWatched && data.Data is{ Count: > 0 }){
@ -648,7 +660,7 @@ public class CrunchyrollManager{
#endregion #endregion
private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename, CrunchyEpMeta crunchyEpMeta){ private async Task<(Merger? merger, bool isMuxed, bool syncError, string notSyncedDubs)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename, CrunchyEpMeta crunchyEpMeta){
var muxToMp3 = false; var muxToMp3 = false;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -657,7 +669,7 @@ public class CrunchyrollManager{
muxToMp3 = true; muxToMp3 = true;
} else{ } else{
Console.WriteLine("Skip muxing since no videos are downloaded"); Console.WriteLine("Skip muxing since no videos are downloaded");
return (null, false, false); return (null, false, false, "");
} }
} }
@ -733,6 +745,8 @@ public class CrunchyrollManager{
} }
bool isMuxed, syncError = false; bool isMuxed, syncError = false;
List<string> notSyncedDubs =[];
if (options is{ SyncTiming: true, DlVideoOnce: true }){ if (options is{ SyncTiming: true, DlVideoOnce: true }){
crunchyEpMeta.DownloadProgress = new DownloadProgress(){ crunchyEpMeta.DownloadProgress = new DownloadProgress(){
@ -755,6 +769,7 @@ public class CrunchyrollManager{
if (delay <= -100){ if (delay <= -100){
syncError = true; syncError = true;
notSyncedDubs.Add(syncVideo.Lang.CrLocale ?? syncVideo.Language.CrLocale);
continue; continue;
} }
@ -794,7 +809,7 @@ public class CrunchyrollManager{
isMuxed = await merger.Merge("ffmpeg", CfgManager.PathFFMPEG); isMuxed = await merger.Merge("ffmpeg", CfgManager.PathFFMPEG);
} }
return (merger, isMuxed, syncError); return (merger, isMuxed, syncError, string.Join(", ", notSyncedDubs));
} }
private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
@ -974,7 +989,7 @@ public class CrunchyrollManager{
List<string> compiledChapters = new List<string>(); List<string> compiledChapters = new List<string>();
if (options.Chapters){ if (options.Chapters && !data.OnlySubs){
await ParseChapters(mediaGuid, compiledChapters); await ParseChapters(mediaGuid, compiledChapters);
if (compiledChapters.Count == 0 && primaryVersion.MediaGuid != null && mediaGuid != primaryVersion.MediaGuid){ if (compiledChapters.Count == 0 && primaryVersion.MediaGuid != null && mediaGuid != primaryVersion.MediaGuid){
@ -1052,18 +1067,16 @@ public class CrunchyrollManager{
if (pbStreams?.Keys != null){ if (pbStreams?.Keys != null){
var pb = pbStreams.Select(v => { var pb = pbStreams.Select(v => {
v.Value.HardsubLang = v.Value.HardsubLocale != null if (v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
? Languages.FixAndFindCrLc(v.Value.HardsubLocale.GetEnumMemberValue()).Locale hsLangs.Add(v.Value.HardsubLang.CrLocale);
: null;
if (v.Value.HardsubLocale != null && v.Value.HardsubLang != null && !hsLangs.Contains(v.Value.HardsubLocale.GetEnumMemberValue())){
hsLangs.Add(v.Value.HardsubLang);
} }
return new StreamDetailsPop{ return new StreamDetailsPop{
Url = v.Value.Url, Url = v.Value.Url,
IsHardsubbed = v.Value.IsHardsubbed,
HardsubLocale = v.Value.HardsubLocale, HardsubLocale = v.Value.HardsubLocale,
HardsubLang = v.Value.HardsubLang, HardsubLang = v.Value.HardsubLang,
AudioLang = v.Value.AudioLang, AudioLang = pbData.Meta?.AudioLocale ?? Languages.DEFAULT_lang,
Type = v.Value.Type, Type = v.Value.Type,
Format = "drm_adaptive_dash", Format = "drm_adaptive_dash",
}; };
@ -1083,16 +1096,16 @@ public class CrunchyrollManager{
}; };
} }
var audDub = ""; var audDub = Languages.DEFAULT_lang;
if (pbData.Meta != null){ if (pbData.Meta != null){
audDub = Languages.FindLang(Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue())).Code; audDub = pbData.Meta.AudioLocale;
} }
hsLangs = Languages.SortTags(hsLangs); hsLangs = Languages.SortTags(hsLangs);
streams = streams.Select(s => { streams = streams.Select(s => {
s.AudioLang = audDub; s.AudioLang = audDub;
s.HardsubLang = string.IsNullOrEmpty(s.HardsubLang) ? "-" : s.HardsubLang; s.HardsubLang = s.HardsubLang;
s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}"; s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}";
return s; return s;
}).ToList(); }).ToList();
@ -1102,21 +1115,15 @@ public class CrunchyrollManager{
if (options.Hslang != "none"){ if (options.Hslang != "none"){
if (hsLangs.IndexOf(options.Hslang) > -1){ if (hsLangs.IndexOf(options.Hslang) > -1){
Console.WriteLine($"Selecting stream with {Languages.Locale2language(options.Hslang).Language} hardsubs"); Console.WriteLine($"Selecting stream with {Languages.Locale2language(options.Hslang).Language} hardsubs");
streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == options.Hslang).ToList(); streams = streams.Where((s) => s.IsHardsubbed && s.HardsubLang.CrLocale == options.Hslang).ToList();
} else{ } else{
Console.Error.WriteLine($"Selected stream with {Languages.Locale2language(options.Hslang).CrLocale} hardsubs not available"); Console.Error.WriteLine($"Selected stream with {options.Hslang} hardsubs not available");
if (hsLangs.Count > 0){ if (hsLangs.Count > 0){
Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs)); Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
} }
if (dlVideoOnce && options.DlVideoOnce){ if (dlVideoOnce && options.DlVideoOnce){
streams = streams.Where((s) => { streams = streams.Where((s) => !s.IsHardsubbed).ToList();
if (s.HardsubLang != "-"){
return false;
}
return true;
}).ToList();
} else{ } else{
if (hsLangs.Count > 0){ if (hsLangs.Count > 0){
var dialog = new ContentDialog(){ var dialog = new ContentDialog(){
@ -1141,7 +1148,7 @@ public class CrunchyrollManager{
if (hsLangs.IndexOf(selectedValue) > -1){ if (hsLangs.IndexOf(selectedValue) > -1){
Console.WriteLine($"Selecting stream with {Languages.Locale2language(selectedValue).Language} hardsubs"); Console.WriteLine($"Selecting stream with {Languages.Locale2language(selectedValue).Language} hardsubs");
streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == selectedValue).ToList(); streams = streams.Where((s) => s.IsHardsubbed && s.HardsubLang?.CrLocale == selectedValue).ToList();
data.Hslang = selectedValue; data.Hslang = selectedValue;
} }
} else{ } else{
@ -1167,13 +1174,7 @@ public class CrunchyrollManager{
} }
} }
} else{ } else{
streams = streams.Where((s) => { streams = streams.Where((s) => !s.IsHardsubbed).ToList();
if (s.HardsubLang != "-"){
return false;
}
return true;
}).ToList();
if (streams.Count < 1){ if (streams.Count < 1){
Console.Error.WriteLine("Raw streams not available!"); Console.Error.WriteLine("Raw streams not available!");
@ -1205,7 +1206,7 @@ public class CrunchyrollManager{
} }
string tsFile = ""; string tsFile = "";
var videoDownloadMedia = new DownloadedMedia(); var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){ if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null); var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
@ -1231,7 +1232,7 @@ public class CrunchyrollManager{
//Parse MPD Playlists //Parse MPD Playlists
var crLocal = ""; var crLocal = "";
if (pbData.Meta != null){ if (pbData.Meta != null){
crLocal = Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue()); crLocal = pbData.Meta.AudioLocale.CrLocale;
} }
MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl); MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl);
@ -1348,10 +1349,10 @@ public class CrunchyrollManager{
variables.Add(new Variable("width", chosenVideoSegments.quality.width, false)); variables.Add(new Variable("width", chosenVideoSegments.quality.width, false));
if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p"; if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p";
LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.Code == curStream.AudioLang); LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.CrLocale == curStream.AudioLang.CrLocale);
if (lang == null){ if (lang == null){
Console.Error.WriteLine($"Unable to find language for code {curStream.AudioLang}"); Console.Error.WriteLine($"Unable to find language for code {curStream.AudioLang.CrLocale}");
MainWindow.Instance.ShowError($"Unable to find language for code {curStream.AudioLang}"); MainWindow.Instance.ShowError($"Unable to find language for code {curStream.AudioLang.CrLocale}");
return new DownloadResponse{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
@ -1465,10 +1466,6 @@ public class CrunchyrollManager{
}; };
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
var assetIdRegexMatch = Regex.Match(chosenVideoSegments.segments[0].uri, @"/assets/(?:p/)?([^_,]+)");
var assetId = assetIdRegexMatch.Success ? assetIdRegexMatch.Groups[1].Value : null;
var sessionId = Helpers.GenerateSessionId();
Console.WriteLine("Decryption Needed, attempting to decrypt"); Console.WriteLine("Decryption Needed, attempting to decrypt");
if (!_widevine.canDecrypt){ if (!_widevine.canDecrypt){
@ -1481,40 +1478,10 @@ public class CrunchyrollManager{
}; };
} }
var reqBodyData = new{
accounting_id = "crunchyroll",
asset_id = assetId,
session_id = sessionId,
user_id = Token?.account_id
};
var json = JsonConvert.SerializeObject(reqBodyData);
var reqBody = new StringContent(json, Encoding.UTF8, "application/json");
var decRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.DRM}", HttpMethod.Post, false, false, null);
decRequest.Content = reqBody;
var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest);
if (!decRequestResponse.IsOk){
Console.Error.WriteLine("Request to DRM Authentication failed: ");
MainWindow.Instance.ShowError("Request to DRM Authentication failed");
dlFailed = true;
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = "DRM Authentication failed"
};
}
DrmAuthData authData = Helpers.Deserialize<DrmAuthData>(decRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new DrmAuthData();
Dictionary<string, string> authDataDict = new Dictionary<string, string> Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "dt-custom-data", authData.CustomData ?? string.Empty },{ "x-dt-auth-token", authData.Token ?? string.Empty } }; { { "authorization", "Bearer " + Token?.access_token },{"x-cr-content-id", mediaGuid},{"x-cr-video-token", pbData.Meta?.Token ?? string.Empty} };
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, "https://lic.drmtoday.com/license-proxy-widevine/cenc/", authDataDict); var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
if (encryptionKeys.Count == 0){ if (encryptionKeys.Count == 0){
Console.Error.WriteLine("Failed to get encryption keys"); Console.Error.WriteLine("Failed to get encryption keys");
@ -1527,7 +1494,6 @@ public class CrunchyrollManager{
}; };
} }
if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){ if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower(); var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower(); var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
@ -1610,6 +1576,7 @@ public class CrunchyrollManager{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s", Path = $"{tsFile}.video.m4s",
Lang = lang, Lang = lang,
Language = lang,
IsPrimary = isPrimary IsPrimary = isPrimary
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
@ -1694,6 +1661,7 @@ public class CrunchyrollManager{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s", Path = $"{tsFile}.video.m4s",
Lang = lang, Lang = lang,
Language = lang,
IsPrimary = isPrimary IsPrimary = isPrimary
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
@ -1753,13 +1721,7 @@ public class CrunchyrollManager{
} }
// Finding language by code // Finding language by code
var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang) ?? new LanguageItem{ var lang = Languages.languages.FirstOrDefault(l => l == curStream?.AudioLang) ?? Languages.DEFAULT_lang;
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
if (lang.Code == "und"){ if (lang.Code == "und"){
Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}"); Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}");
} }
@ -1793,7 +1755,7 @@ public class CrunchyrollManager{
} }
} }
if (options.IncludeVideoDescription){ if (options.IncludeVideoDescription && !data.OnlySubs){
string fullPath = (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) + ".xml"; string fullPath = (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) + ".xml";
if (!File.Exists(fullPath)){ if (!File.Exists(fullPath)){
@ -1821,6 +1783,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{ files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description, Type = DownloadMediaType.Description,
Path = fullPath, Path = fullPath,
Lang = Languages.DEFAULT_lang
}); });
data.downloadedFiles.Add(fullPath); data.downloadedFiles.Add(fullPath);
} else{ } else{
@ -1828,6 +1791,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{ files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description, Type = DownloadMediaType.Description,
Path = fullPath, Path = fullPath,
Lang = Languages.DEFAULT_lang
}); });
data.downloadedFiles.Add(fullPath); data.downloadedFiles.Add(fullPath);
} }
@ -1858,7 +1822,7 @@ public class CrunchyrollManager{
}; };
} }
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data, private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, LanguageItem audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data,
DownloadedMedia videoDownloadMedia){ DownloadedMedia videoDownloadMedia){
if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){ if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[]; List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
@ -1894,7 +1858,7 @@ public class CrunchyrollManager{
var langItem = subsItem.locale; var langItem = subsItem.locale;
var sxData = new SxItem(); var sxData = new SxItem();
sxData.Language = langItem; sxData.Language = langItem;
var isSigns = langItem.Code == audDub && !subsItem.isCC; var isSigns = langItem.CrLocale == audDub.CrLocale && !subsItem.isCC;
var isCc = subsItem.isCC; var isCc = subsItem.isCC;
sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all"))); sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all")));
@ -2127,7 +2091,7 @@ public class CrunchyrollManager{
Data = new Dictionary<string, StreamDetails>() Data = new Dictionary<string, StreamDetails>()
}; };
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play"; var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){ if (!playbackRequestResponse.IsOk){
@ -2138,7 +2102,7 @@ public class CrunchyrollManager{
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId);
} else{ } else{
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play";
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){ if (!playbackRequestResponse.IsOk){
@ -2190,30 +2154,36 @@ public class CrunchyrollManager{
var derivedPlayCrunchyStreams = new CrunchyStreams(); var derivedPlayCrunchyStreams = new CrunchyStreams();
if (playStream.HardSubs != null){ if (playStream.HardSubs != null){
//hlang "none" is no hardsube same url as the default url
foreach (var hardsub in playStream.HardSubs){ foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value; var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url, Url = stream.Url,
HardsubLocale = stream.Hlang IsHardsubbed = true,
HardsubLocale = stream.Hlang,
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
}; };
} }
} }
derivedPlayCrunchyStreams[""] = new StreamDetails{ derivedPlayCrunchyStreams[""] = new StreamDetails{
Url = playStream.Url, Url = playStream.Url,
HardsubLocale = Locale.DefaulT IsHardsubbed = false,
HardsubLocale = Locale.DefaulT,
HardsubLang = Languages.DEFAULT_lang
}; };
temppbData.Data = derivedPlayCrunchyStreams; temppbData.Data = derivedPlayCrunchyStreams;
temppbData.Total = 1; temppbData.Total = 1;
temppbData.Meta = new PlaybackMeta{ temppbData.Meta = new PlaybackMeta{
AudioLocale = playStream.AudioLocale, AudioLocale = Languages.FindLang(playStream.AudioLocale != null ? playStream.AudioLocale.GetEnumMemberValue() : ""),
Versions = playStream.Versions, Versions = playStream.Versions,
Bifs = new List<string>{ playStream.Bifs ?? "" }, Bifs = new List<string>{ playStream.Bifs ?? "" },
MediaId = mediaId, MediaId = mediaId,
Captions = playStream.Captions, Captions = playStream.Captions,
Subtitles = new Subtitles() Subtitles = new Subtitles(),
Token = playStream.Token,
}; };
if (playStream.Subtitles != null){ if (playStream.Subtitles != null){

View file

@ -270,7 +270,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null; ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null;
SelectedDescriptionLang = descriptionLang ?? DescriptionLangList[0]; SelectedDescriptionLang = descriptionLang ?? DescriptionLangList[0];
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == Languages.Locale2language(options.Hslang).CrLocale) ?? null; ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null;
SelectedHSLang = hsLang ?? HardSubLangList[0]; SelectedHSLang = hsLang ?? HardSubLangList[0];
ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null; ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null;
@ -416,10 +416,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
string descLang = SelectedDescriptionLang.Content + ""; string descLang = SelectedDescriptionLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale; CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang;
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";

View file

@ -41,10 +41,10 @@ public class Widevine{
if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){ if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){
foreach (var file in Directory.EnumerateFiles(CfgManager.PathWIDEVINE_DIR)){ foreach (var file in Directory.EnumerateFiles(CfgManager.PathWIDEVINE_DIR)){
var fileInfo = new FileInfo(file); var fileInfo = new FileInfo(file);
if (fileInfo.Length >= 1024 * 8 || fileInfo.Attributes.HasFlag(FileAttributes.Directory)) if (fileInfo.Length >= 1024 * 8 || fileInfo.Attributes.HasFlag(FileAttributes.Directory))
continue; continue;
string fileContents = File.ReadAllText(file, Encoding.UTF8); string fileContents = File.ReadAllText(file, Encoding.UTF8);
if (IsPrivateKey(fileContents)){ if (IsPrivateKey(fileContents)){
@ -107,7 +107,9 @@ public class Widevine{
} }
var licenceReq = ses.GetLicenseRequest(); var licenceReq = ses.GetLicenseRequest();
playbackRequest2.Content = new ByteArrayContent(licenceReq); var content = new ByteArrayContent(licenceReq);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
playbackRequest2.Content = content;
var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2); var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);

View file

@ -41,7 +41,7 @@ public enum SeriesType{
public enum Locale{ public enum Locale{
[EnumMember(Value = "")] [EnumMember(Value = "")]
DefaulT, DefaulT,
[EnumMember(Value = "un")] [EnumMember(Value = "un")]
Unknown, Unknown,

View file

@ -43,7 +43,7 @@ public class HlsDownloader{
Offset = options.Offset ?? 0, Offset = options.Offset ?? 0,
BaseUrl = options.BaseUrl, BaseUrl = options.BaseUrl,
SkipInit = options.SkipInit ?? false, SkipInit = options.SkipInit ?? false,
Timeout = options.Timeout ?? 60 * 1000, Timeout = options.Timeout ?? 15 * 1000,
CheckPartLength = true, CheckPartLength = true,
IsResume = options.Offset.HasValue && options.Offset.Value > 0, IsResume = options.Offset.HasValue && options.Offset.Value > 0,
BytesDownloaded = 0, BytesDownloaded = 0,
@ -449,17 +449,17 @@ public class HlsDownloader{
// Log retry attempts // Log retry attempts
string partType = isKey ? "Key" : "Part"; string partType = isKey ? "Key" : "Part";
int partIndx = partIndex + 1 + segOffset; int partIndx = partIndex + 1 + segOffset;
Console.WriteLine($"{partType} {partIndx}: Attempt {attempt + 1} to retrieve data failed."); Console.Error.WriteLine($"{partType} {partIndx}: Attempt {attempt + 1} to retrieve data failed.");
Console.WriteLine($"\tError: {ex.Message}"); Console.Error.WriteLine($"\tError: {ex.Message}");
if (attempt == retryCount) if (attempt == retryCount)
throw; // rethrow after last retry throw; // rethrow after last retry
await Task.Delay(_data.WaitTime); await Task.Delay(_data.WaitTime);
}catch (Exception ex) { }catch (Exception ex) {
Console.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:"); Console.Error.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:");
Console.WriteLine($"\tType: {ex.GetType()}"); Console.Error.WriteLine($"\tType: {ex.GetType()}");
Console.WriteLine($"\tMessage: {ex.Message}"); Console.Error.WriteLine($"\tMessage: {ex.Message}");
throw; throw;
} }
} }

View file

@ -240,7 +240,7 @@ public class Helpers{
if (e.Data.StartsWith("Error:")){ if (e.Data.StartsWith("Error:")){
Console.Error.WriteLine(e.Data); Console.Error.WriteLine(e.Data);
} else{ } else{
Console.WriteLine(e.Data); Console.WriteLine(e.Data);
} }
} }
}; };
@ -604,29 +604,29 @@ public class Helpers{
public static Dictionary<string, List<DownloadedMedia>> GroupByLanguageWithSubtitles(List<DownloadedMedia> allMedia){ public static Dictionary<string, List<DownloadedMedia>> GroupByLanguageWithSubtitles(List<DownloadedMedia> allMedia){
//Group by language //Group by language
var languageGroups = allMedia var languageGroups = allMedia
.Where(media => .Where(media => media.Type != DownloadMediaType.Description &&
!string.IsNullOrEmpty(media.Lang.CrLocale) || (!string.IsNullOrEmpty(media.Lang?.CrLocale) ||
(media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null && (media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null } &&
!string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang.CrLocale)) !string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang?.CrLocale)))
) )
.GroupBy(media => { .GroupBy(media => {
if (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null){ if (media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null }){
return media.RelatedVideoDownloadMedia.Lang.CrLocale; return media.RelatedVideoDownloadMedia.Lang?.CrLocale ?? "und";
} }
return media.Lang.CrLocale; return media.Lang?.CrLocale ?? "und";
}) })
.ToDictionary(group => group.Key, group => group.ToList()); .ToDictionary(group => group.Key, group => group.ToList());
//Find and add Description media to each group //Find and add Description media to each group
var descriptionMedia = allMedia.Where(media => media.Type == DownloadMediaType.Description).ToList(); var descriptionMedia = allMedia.Where(media => media.Type == DownloadMediaType.Description).ToList();
foreach (var description in descriptionMedia){ if (descriptionMedia.Count > 0){
foreach (var group in languageGroups.Values){ foreach (var group in languageGroups.Values){
group.Add(description); group.Add(descriptionMedia[0]);
} }
} }
return languageGroups; return languageGroups;
} }

View file

@ -263,14 +263,22 @@ public static class ApiUrls{
public static string Cms => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/cms"; public static string Cms => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/cms";
public static string Content => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2"; public static string Content => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2";
public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v2";
//https://www.crunchyroll.com/playback/v2
//https://cr-play-service.prd.crunchyrollsvc.com/v2
public static string Subscription => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/subs/v3/subscriptions/"; public static string Subscription => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/subs/v3/subscriptions/";
public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string BetaCms = ApiBeta + "/cms/v2";
public static readonly string DRM = ApiBeta + "/drm/v1/auth"; public static readonly string DRM = ApiBeta + "/drm/v1/auth";
public static readonly string WidevineLicenceUrl = "https://www.crunchyroll.com/license/v1/license/widevine";
//https://lic.drmtoday.com/license-proxy-widevine/cenc/
//https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/
public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE="; public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE=";
public static readonly string MobileUserAgent = "Crunchyroll/3.79.0 Android/15 okhttp/4.12.0"; public static readonly string MobileUserAgent = "Crunchyroll/3.81.8 Android/15 okhttp/4.12.0";
public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0"; public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0";
} }

View file

@ -361,6 +361,10 @@ public class Merger{
case < 0.1: case < 0.1:
return startOffset; return startOffset;
case > 1: case > 1:
Console.Error.WriteLine($"Couldn't sync dub:");
Console.Error.WriteLine($"\tStart offset: {startOffset} seconds");
Console.Error.WriteLine($"\tEnd offset: {endOffset} seconds");
Console.Error.WriteLine($"\tVideo length difference: {lengthDiff} seconds");
return -100; return -100;
default: default:
return endOffset; return endOffset;

View file

@ -63,6 +63,9 @@ public class CrDownloadOptions{
[JsonProperty("history")] [JsonProperty("history")]
public bool History{ get; set; } public bool History{ get; set; }
[JsonProperty("history_count_missing")]
public bool HistoryCountMissing { get; set; }
[JsonProperty("history_include_cr_artists")] [JsonProperty("history_include_cr_artists")]
public bool HistoryIncludeCrArtists{ get; set; } public bool HistoryIncludeCrArtists{ get; set; }

View file

@ -402,6 +402,16 @@ public class CrunchyEpMetaData{
public List<EpisodeVersion>? Versions{ get; set; } public List<EpisodeVersion>? Versions{ get; set; }
public bool IsSubbed{ get; set; } public bool IsSubbed{ get; set; }
public bool IsDubbed{ get; set; } public bool IsDubbed{ get; set; }
public (string? seasonID, string? guid) GetOriginalIds(){
var version = Versions?.FirstOrDefault(a => a.Original);
if (version != null && !string.IsNullOrEmpty(version.Guid) && !string.IsNullOrEmpty(version.SeasonGuid)){
return (version.SeasonGuid, version.Guid);
}
return (null, null);
}
} }
public class CrunchyRollEpisodeData{ public class CrunchyRollEpisodeData{

View file

@ -16,7 +16,9 @@ public class StreamDetails{
public string? Url{ get; set; } public string? Url{ get; set; }
[JsonProperty("hardsub_lang")] [JsonProperty("hardsub_lang")]
public string? HardsubLang{ get; set; } public required LanguageItem HardsubLang{ get; set; }
public bool IsHardsubbed{ get; set; }
[JsonProperty("audio_lang")] [JsonProperty("audio_lang")]
public string? AudioLang{ get; set; } public string? AudioLang{ get; set; }
@ -24,6 +26,18 @@ public class StreamDetails{
public string? Type{ get; set; } public string? Type{ get; set; }
} }
public class StreamDetailsPop{
public Locale? HardsubLocale{ get; set; }
public string? Url{ get; set; }
public required LanguageItem HardsubLang{ get; set; }
public bool IsHardsubbed{ get; set; }
public required LanguageItem AudioLang{ get; set; }
public string? Type{ get; set; }
public string? Format{ get; set; }
}
public class PlaybackMeta{ public class PlaybackMeta{
[JsonProperty("media_id")] [JsonProperty("media_id")]
public string? MediaId{ get; set; } public string? MediaId{ get; set; }
@ -33,12 +47,14 @@ public class PlaybackMeta{
public List<PlaybackVersion>? Versions{ get; set; } public List<PlaybackVersion>? Versions{ get; set; }
[JsonProperty("audio_locale")] [JsonProperty("audio_locale")]
public Locale? AudioLocale{ get; set; } public LanguageItem AudioLocale{ get; set; }
[JsonProperty("closed_captions")] [JsonProperty("closed_captions")]
public Subtitles? ClosedCaptions{ get; set; } public Subtitles? ClosedCaptions{ get; set; }
public Dictionary<string, Caption>? Captions{ get; set; } public Dictionary<string, Caption>? Captions{ get; set; }
public string? Token{ get; set; }
} }
public class SubtitleInfo{ public class SubtitleInfo{
@ -71,11 +87,3 @@ public class PlaybackVersion{
public string? Variant{ get; set; } public string? Variant{ get; set; }
} }
public class StreamDetailsPop{
public Locale? HardsubLocale{ get; set; }
public string? Url{ get; set; }
public string? HardsubLang{ get; set; }
public string? AudioLang{ get; set; }
public string? Type{ get; set; }
public string? Format{ get; set; }
}

View file

@ -82,7 +82,7 @@ public class DownloadResponse{
public class DownloadedMedia : SxItem{ public class DownloadedMedia : SxItem{
public DownloadMediaType Type{ get; set; } public DownloadMediaType Type{ get; set; }
public LanguageItem Lang{ get; set; } public required LanguageItem Lang{ get; set; }
public bool IsPrimary{ get; set; } public bool IsPrimary{ get; set; }
public bool? Cc{ get; set; } public bool? Cc{ get; set; }

View file

@ -22,7 +22,7 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_type")] [JsonProperty("series_type")]
public SeriesType SeriesType{ get; set; } = SeriesType.Unknown; public SeriesType SeriesType{ get; set; } = SeriesType.Unknown;
[JsonProperty("series_is_inactive")] [JsonProperty("series_is_inactive")]
public bool IsInactive{ get; set; } public bool IsInactive{ get; set; }
@ -107,10 +107,10 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore] [JsonIgnore]
public string SeriesFolderPath{ get; set; } public string SeriesFolderPath{ get; set; }
[JsonIgnore] [JsonIgnore]
public bool SeriesFolderPathExists{ get; set; } public bool SeriesFolderPathExists{ get; set; }
#region Settings Override #region Settings Override
[JsonIgnore] [JsonIgnore]
@ -225,9 +225,9 @@ public class HistorySeries : INotifyPropertyChanged{
SelectedSubLang.CollectionChanged += Changes; SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes; SelectedDubLang.CollectionChanged += Changes;
UpdateSeriesFolderPath(); UpdateSeriesFolderPath();
Loading = false; Loading = false;
} }
@ -249,113 +249,83 @@ public class HistorySeries : INotifyPropertyChanged{
public void UpdateNewEpisodes(){ public void UpdateNewEpisodes(){
int count = 0; int count = 0;
bool foundWatched = false; bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; var options = CrunchyrollManager.Instance.CrunOptions;
var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled &&
!string.IsNullOrEmpty(SonarrSeriesId);
var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; bool historyAddSpecials = options.HistoryAddSpecials;
bool sonarrEnabled = SeriesType != SeriesType.Artist &&
options.SonarrProperties?.SonarrEnabled == true &&
!string.IsNullOrEmpty(SonarrSeriesId);
bool skipUnmonitored = options.HistorySkipUnmonitored;
bool countMissing = options.HistoryCountMissing;
bool useSonarrCounting = options.HistoryCountSonarr;
if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){ for (int i = Seasons.Count - 1; i >= 0; i--){
for (int i = Seasons.Count - 1; i >= 0; i--){ var season = Seasons[i];
var season = Seasons[i]; var episodes = season.EpisodesList;
if (season.SpecialSeason == true){ if (season.SpecialSeason == true){
if (historyAddSpecials){ if (historyAddSpecials){
var episodes = season.EpisodesList; for (int j = episodes.Count - 1; j >= 0; j--){
for (int j = episodes.Count - 1; j >= 0; j--){ var ep = episodes[j];
if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){ if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){
count++; continue;
}
} }
if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
count++;
}
}
}
continue;
}
for (int j = episodes.Count - 1; j >= 0; j--){
var ep = episodes[j];
if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){
continue;
}
if (ep.SpecialEpisode){
if (historyAddSpecials && ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
count++;
} }
continue; continue;
} }
var episodesList = season.EpisodesList; if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, foundWatched)){
for (int j = episodesList.Count - 1; j >= 0; j--){ count++;
var episode = episodesList[j]; } else{
foundWatched = true;
if (sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ //if not count specials break
continue; if (!historyAddSpecials && !countMissing){
} break;
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.SonarrHasFile){
count++;
}
continue;
}
if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){
count++;
} }
} }
} }
} else{
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (season.SpecialSeason == true){ if (foundWatched && !historyAddSpecials && !countMissing){
if (historyAddSpecials){ break;
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrEnabled && sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!episodes[j].WasDownloaded){
count++;
}
}
}
continue;
}
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
count++;
}
continue;
}
if (!episode.WasDownloaded && !foundWatched){
count++;
} else{
foundWatched = true;
if (!historyAddSpecials){
break;
}
}
}
if (foundWatched && !historyAddSpecials){
break;
}
} }
} }
NewEpisodes = count; NewEpisodes = count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
} }
private bool ShouldCountEpisode(HistoryEpisode episode, bool useSonarr, bool countMissing, bool foundWatched){
if (useSonarr)
return !string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile;
return !episode.WasDownloaded && (!foundWatched || countMissing);
}
public void SetFetchingData(){ public void SetFetchingData(){
FetchingData = true; FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
@ -363,104 +333,65 @@ public class HistorySeries : INotifyPropertyChanged{
public async Task AddNewMissingToDownloads(){ public async Task AddNewMissingToDownloads(){
bool foundWatched = false; bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; var options = CrunchyrollManager.Instance.CrunOptions;
var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled &&
!string.IsNullOrEmpty(SonarrSeriesId);
var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; bool historyAddSpecials = options.HistoryAddSpecials;
bool sonarrEnabled = SeriesType != SeriesType.Artist &&
options.SonarrProperties?.SonarrEnabled == true &&
!string.IsNullOrEmpty(SonarrSeriesId);
bool skipUnmonitored = options.HistorySkipUnmonitored;
bool countMissing = options.HistoryCountMissing;
bool useSonarrCounting = options.HistoryCountSonarr;
if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){ for (int i = Seasons.Count - 1; i >= 0; i--){
for (int i = Seasons.Count - 1; i >= 0; i--){ var season = Seasons[i];
var season = Seasons[i]; var episodes = season.EpisodesList;
if (season.SpecialSeason == true){ if (season.SpecialSeason == true){
if (historyAddSpecials){ if (historyAddSpecials){
var episodes = season.EpisodesList; for (int j = episodes.Count - 1; j >= 0; j--){
for (int j = episodes.Count - 1; j >= 0; j--){ var ep = episodes[j];
if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){ if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){
await Seasons[i].EpisodesList[j].DownloadEpisode(); continue;
}
} }
if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
await ep.DownloadEpisode();
}
}
}
continue;
}
for (int j = episodes.Count - 1; j >= 0; j--){
var ep = episodes[j];
if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){
continue;
}
if (ep.SpecialEpisode){
if (historyAddSpecials && ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
await ep.DownloadEpisode();
} }
continue; continue;
} }
var episodesList = season.EpisodesList; if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, foundWatched)){
for (int j = episodesList.Count - 1; j >= 0; j--){ await ep.DownloadEpisode();
var episode = episodesList[j]; } else{
foundWatched = true;
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ if (!historyAddSpecials && !countMissing){
continue; break;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.SonarrHasFile){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
continue;
}
if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){
await Seasons[i].EpisodesList[j].DownloadEpisode();
} }
} }
} }
} else{
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (season.SpecialSeason == true){ if (foundWatched && !historyAddSpecials && !countMissing){
if (historyAddSpecials){ break;
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!episodes[j].WasDownloaded){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
}
}
continue;
}
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
continue;
}
if (!episode.WasDownloaded && !foundWatched){
await Seasons[i].EpisodesList[j].DownloadEpisode();
} else{
foundWatched = true;
if (!historyAddSpecials){
break;
}
}
}
if (foundWatched && !historyAddSpecials){
break;
}
} }
} }
} }
@ -470,7 +401,7 @@ public class HistorySeries : INotifyPropertyChanged{
FetchingData = true; FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
var isOk = true; var isOk = true;
switch (SeriesType){ switch (SeriesType){
case SeriesType.Artist: case SeriesType.Artist:
try{ try{
@ -503,7 +434,7 @@ public class HistorySeries : INotifyPropertyChanged{
UpdateNewEpisodes(); UpdateNewEpisodes();
FetchingData = false; FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
return isOk; return isOk;
} }
@ -536,7 +467,7 @@ public class HistorySeries : INotifyPropertyChanged{
break; break;
} }
} }
public void UpdateSeriesFolderPath(){ public void UpdateSeriesFolderPath(){
var season = Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath)); var season = Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath));
@ -587,8 +518,7 @@ public class HistorySeries : INotifyPropertyChanged{
SeriesFolderPathExists = true; SeriesFolderPathExists = true;
} }
} }
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
} }
} }

View file

@ -9,35 +9,53 @@ namespace CRD.Utils.Structs;
public class Languages{ public class Languages{
public static readonly LanguageItem[] languages ={ public static readonly LanguageItem[] languages ={
new(){ CrLocale = "ja-JP", Locale = "ja", Code = "jpn", Name = "Japanese" }, new(){ CrLocale = "ja-JP", Locale = "ja", Code = "jpn", Name = "Japanese" },
new(){ CrLocale = "en-US", Locale = "en", Code = "eng", Name = "English" },
new(){ CrLocale = "de-DE", Locale = "de", Code = "deu", Name = "German" }, new(){ CrLocale = "de-DE", Locale = "de", Code = "deu", Name = "German" },
new(){ CrLocale = "en-US", Locale = "en", Code = "eng", Name = "English" },
new(){ CrLocale = "en-IN", Locale = "en-IN", Code = "eng", Name = "English (India)" }, new(){ CrLocale = "en-IN", Locale = "en-IN", Code = "eng", Name = "English (India)" },
new(){ CrLocale = "es-LA", Locale = "es-LA", Code = "spa", Name = "Spanish", Language = "Latin American Spanish" },
new(){ CrLocale = "es-419", Locale = "es-419", Code = "spa-419", Name = "Spanish", Language = "Latin American Spanish" }, new(){ CrLocale = "es-419", Locale = "es-419", Code = "spa-419", Name = "Spanish", Language = "Latin American Spanish" },
new(){ CrLocale = "es-ES", Locale = "es-ES", Code = "spa-ES", Name = "Castilian", Language = "European Spanish" }, new(){ CrLocale = "es-ES", Locale = "es-ES", Code = "spa-ES", Name = "Castilian", Language = "European Spanish" },
new(){ CrLocale = "pt-BR", Locale = "pt-BR", Code = "por", Name = "Portuguese", Language = "Brazilian Portuguese" }, new(){ CrLocale = "pt-BR", Locale = "pt-BR", Code = "por", Name = "Portuguese", Language = "Brazilian Portuguese" },
new(){ CrLocale = "pt-PT", Locale = "pt-PT", Code = "por", Name = "Portuguese (Portugal)", Language = "Portugues (Portugal)" }, new(){ CrLocale = "pt-PT", Locale = "pt-PT", Code = "por", Name = "Portuguese (Portugal)", Language = "Portugues (Portugal)" },
new(){ CrLocale = "fr-FR", Locale = "fr", Code = "fra", Name = "French" }, new(){ CrLocale = "fr-FR", Locale = "fr", Code = "fra", Name = "French" },
new(){ CrLocale = "ar-ME", Locale = "ar", Code = "ara-ME", Name = "Arabic" },
new(){ CrLocale = "ar-SA", Locale = "ar", Code = "ara", Name = "Arabic (Saudi Arabia)" },
new(){ CrLocale = "it-IT", Locale = "it", Code = "ita", Name = "Italian" }, new(){ CrLocale = "it-IT", Locale = "it", Code = "ita", Name = "Italian" },
new(){ CrLocale = "ru-RU", Locale = "ru", Code = "rus", Name = "Russian" },
new(){ CrLocale = "tr-TR", Locale = "tr", Code = "tur", Name = "Turkish" },
new(){ CrLocale = "hi-IN", Locale = "hi", Code = "hin", Name = "Hindi" },
// new(){ locale = "zh", code = "cmn", name = "Chinese (Mandarin, PRC)" },
new(){ CrLocale = "zh-CN", Locale = "zh-CN", Code = "zho", Name = "Chinese (Mainland China)" },
new(){ CrLocale = "zh-TW", Locale = "zh-TW", Code = "chi", Name = "Chinese (Taiwan)" },
new(){ CrLocale = "zh-HK", Locale = "zh-HK", Code = "zho-HK", Name = "Chinese (Hong Kong)" },
new(){ CrLocale = "ko-KR", Locale = "ko", Code = "kor", Name = "Korean" },
new(){ CrLocale = "ca-ES", Locale = "ca-ES", Code = "cat", Name = "Catalan" },
new(){ CrLocale = "pl-PL", Locale = "pl-PL", Code = "pol", Name = "Polish" }, new(){ CrLocale = "pl-PL", Locale = "pl-PL", Code = "pol", Name = "Polish" },
new(){ CrLocale = "th-TH", Locale = "th-TH", Code = "tha", Name = "Thai", Language = "ไทย" },
new(){ CrLocale = "ta-IN", Locale = "ta-IN", Code = "tam", Name = "Tamil (India)", Language = "தமிழ்" },
new(){ CrLocale = "ms-MY", Locale = "ms-MY", Code = "may", Name = "Malay (Malaysia)", Language = "Bahasa Melayu" },
new(){ CrLocale = "vi-VN", Locale = "vi-VN", Code = "vie", Name = "Vietnamese", Language = "Tiếng Việt" },
new(){ CrLocale = "id-ID", Locale = "id-ID", Code = "ind", Name = "Indonesian", Language = "Bahasa Indonesia" }, new(){ CrLocale = "id-ID", Locale = "id-ID", Code = "ind", Name = "Indonesian", Language = "Bahasa Indonesia" },
new(){ CrLocale = "ms-MY", Locale = "ms-MY", Code = "may", Name = "Malay (Malaysia)", Language = "Bahasa Melayu" },
new(){ CrLocale = "ca-ES", Locale = "ca-ES", Code = "cat", Name = "Catalan" },
new(){ CrLocale = "vi-VN", Locale = "vi-VN", Code = "vie", Name = "Vietnamese", Language = "Tiếng Việt" },
new(){ CrLocale = "tr-TR", Locale = "tr", Code = "tur", Name = "Turkish" },
new(){ CrLocale = "ru-RU", Locale = "ru", Code = "rus", Name = "Russian" },
new(){ CrLocale = "ar-SA", Locale = "ar-SA", Code = "ara", Name = "Arabic" },
new(){ CrLocale = "hi-IN", Locale = "hi", Code = "hin", Name = "Hindi" },
new(){ CrLocale = "ta-IN", Locale = "ta-IN", Code = "tam", Name = "Tamil (India)", Language = "தமிழ்" },
new(){ CrLocale = "te-IN", Locale = "te-IN", Code = "tel", Name = "Telugu (India)", Language = "తెలుగు" }, new(){ CrLocale = "te-IN", Locale = "te-IN", Code = "tel", Name = "Telugu (India)", Language = "తెలుగు" },
new(){ CrLocale = "zh-CN", Locale = "zh-CN", Code = "zho", Name = "Chinese (Mainland China)" },
new(){ CrLocale = "zh-HK", Locale = "zh-HK", Code = "zho-HK", Name = "Chinese (Hong Kong)" },
new(){ CrLocale = "zh-TW", Locale = "zh-TW", Code = "chi", Name = "Chinese (Taiwan)" },
new(){ CrLocale = "ko-KR", Locale = "ko", Code = "kor", Name = "Korean" },
new(){ CrLocale = "th-TH", Locale = "th-TH", Code = "tha", Name = "Thai", Language = "ไทย" },
}; };
public static readonly LanguageItem DEFAULT_lang = new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
public static List<string> SortListByLangList(List<string> langList){ public static List<string> SortListByLangList(List<string> langList){
var orderMap = languages.Select((value, index) => new{ Value = value.CrLocale, Index = index }) var orderMap = languages.Select((value, index) => new{ Value = value.CrLocale, Index = index })
@ -65,7 +83,7 @@ public class Languages{
public static LanguageItem FixAndFindCrLc(string cr_locale){ public static LanguageItem FixAndFindCrLc(string cr_locale){
if (string.IsNullOrEmpty(cr_locale)){ if (string.IsNullOrEmpty(cr_locale)){
return new LanguageItem(); return DEFAULT_lang;
} }
string str = FixLanguageTag(cr_locale); string str = FixLanguageTag(cr_locale);
@ -77,7 +95,7 @@ public class Languages{
string fileName = $"{fnOutput}"; string fileName = $"{fnOutput}";
if (addIndexAndLangCode){ if (addIndexAndLangCode){
fileName += $".{langItem.CrLocale}"; //.{subsIndex} fileName += $".{langItem.Locale}"; //.{subsIndex}
} }
//removed .{langItem.language} from file name at end //removed .{langItem.language} from file name at end
@ -123,13 +141,7 @@ public class Languages{
if (lang?.CrLocale != null){ if (lang?.CrLocale != null){
return lang; return lang;
} else{ } else{
return new LanguageItem{ return DEFAULT_lang;
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
} }
} }
@ -139,13 +151,7 @@ public class Languages{
if (filteredLocale != null){ if (filteredLocale != null){
return (LanguageItem)filteredLocale; return (LanguageItem)filteredLocale;
} else{ } else{
return new LanguageItem{ return DEFAULT_lang;
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
} }
} }

View file

@ -501,28 +501,30 @@ public partial class HistoryPageViewModel : ViewModelBase{
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){ public async Task DownloadSeasonAll(HistorySeason season){
var downloadTasks = season.EpisodesList foreach (var episode in season.EpisodesList){
.Select(episode => episode.DownloadEpisode()); await episode.DownloadEpisode();
}
await Task.WhenAll(downloadTasks);
} }
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonMissing(HistorySeason season){ public async Task DownloadSeasonMissing(HistorySeason season){
var downloadTasks = season.EpisodesList var missingEpisodes = season.EpisodesList
.Where(episode => !episode.WasDownloaded) .Where(episode => !episode.WasDownloaded).ToList();
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks); if (missingEpisodes.Count == 0){
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
} else{
foreach (var episode in missingEpisodes){
await episode.DownloadEpisode();
}
}
} }
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){ public async Task DownloadSeasonMissingSonarr(HistorySeason season){
var downloadTasks = season.EpisodesList foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
.Where(episode => !episode.SonarrHasFile) await episode.DownloadEpisode();
.Select(episode => episode.DownloadEpisode()); }
await Task.WhenAll(downloadTasks);
} }
[RelayCommand] [RelayCommand]

View file

@ -136,32 +136,30 @@ public partial class SeriesPageViewModel : ViewModelBase{
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){ public async Task DownloadSeasonAll(HistorySeason season){
var downloadTasks = season.EpisodesList foreach (var episode in season.EpisodesList){
.Select(episode => episode.DownloadEpisode()); await episode.DownloadEpisode();
}
await Task.WhenAll(downloadTasks);
} }
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonMissing(HistorySeason season){ public async Task DownloadSeasonMissing(HistorySeason season){
var downloadTasks = season.EpisodesList var missingEpisodes = season.EpisodesList
.Where(episode => !episode.WasDownloaded) .Where(episode => !episode.WasDownloaded).ToList();
.Select(episode => episode.DownloadEpisode()).ToList();
if (downloadTasks.Count == 0){ if (missingEpisodes.Count == 0){
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3)); MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
} else{ } else{
await Task.WhenAll(downloadTasks); foreach (var episode in missingEpisodes){
await episode.DownloadEpisode();
}
} }
} }
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){ public async Task DownloadSeasonMissingSonarr(HistorySeason season){
var downloadTasks = season.EpisodesList foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
.Where(episode => !episode.SonarrHasFile) await episode.DownloadEpisode();
.Select(episode => episode.DownloadEpisode()); }
await Task.WhenAll(downloadTasks);
} }
[RelayCommand] [RelayCommand]

View file

@ -146,6 +146,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private int _selectedIndex; private int _selectedIndex;
[ObservableProperty]
private bool _quickAddMode;
[ObservableProperty] [ObservableProperty]
private bool _isLoading; private bool _isLoading;
@ -251,6 +254,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
[RelayCommand] [RelayCommand]
public async Task AddToHistory(AnilistSeries series){ public async Task AddToHistory(AnilistSeries series){
if (ProgramManager.Instance.FetchingData){
MessageBus.Current.SendMessage(new ToastMessage($"History still loading", ToastType.Warning, 3));
return;
}
if (!string.IsNullOrEmpty(series.CrunchyrollID)){ if (!string.IsNullOrEmpty(series.CrunchyrollID)){
if (CrunchyrollManager.Instance.CrunOptions.History){ if (CrunchyrollManager.Instance.CrunOptions.History){
series.IsInHistory = true; series.IsInHistory = true;
@ -431,7 +439,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
} }
public void SelectionChangedOfSeries(AnilistSeries? value){ public void SelectionChangedOfSeries(AnilistSeries? value){
if (value != null) value.IsExpanded = !value.IsExpanded; if (value != null && !QuickAddMode) value.IsExpanded = !value.IsExpanded;
SelectedSeries = null; SelectedSeries = null;
SelectedIndex = -1; SelectedIndex = -1;
} }

View file

@ -35,6 +35,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _history; private bool _history;
[ObservableProperty]
private bool _historyCountMissing;
[ObservableProperty] [ObservableProperty]
private bool _historyIncludeCrArtists; private bool _historyIncludeCrArtists;
@ -259,6 +262,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
ProxyUsername = options.ProxyUsername ?? ""; ProxyUsername = options.ProxyUsername ?? "";
ProxyPassword = options.ProxyPassword ?? ""; ProxyPassword = options.ProxyPassword ?? "";
ProxyPort = options.ProxyPort; ProxyPort = options.ProxyPort;
HistoryCountMissing = options.HistoryCountMissing;
HistoryIncludeCrArtists = options.HistoryIncludeCrArtists; HistoryIncludeCrArtists = options.HistoryIncludeCrArtists;
HistoryAddSpecials = options.HistoryAddSpecials; HistoryAddSpecials = options.HistoryAddSpecials;
HistorySkipUnmonitored = options.HistorySkipUnmonitored; HistorySkipUnmonitored = options.HistorySkipUnmonitored;
@ -287,42 +291,45 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
return; return;
} }
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedPlaySound = DownloadFinishedPlaySound; var settings = CrunchyrollManager.Instance.CrunOptions;
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1);
CrunchyrollManager.Instance.CrunOptions.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10); settings.DownloadFinishedPlaySound = DownloadFinishedPlaySound;
CrunchyrollManager.Instance.CrunOptions.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30);
CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder; settings.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials; settings.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1);
CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists = HistoryIncludeCrArtists;
CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored = HistorySkipUnmonitored; settings.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10);
CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr; settings.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30);
CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
CrunchyrollManager.Instance.CrunOptions.ProxyEnabled = ProxyEnabled; settings.DownloadToTempFolder = DownloadToTempFolder;
CrunchyrollManager.Instance.CrunOptions.ProxySocks = ProxySocks; settings.HistoryCountMissing = HistoryCountMissing;
CrunchyrollManager.Instance.CrunOptions.ProxyHost = ProxyHost; settings.HistoryAddSpecials = HistoryAddSpecials;
CrunchyrollManager.Instance.CrunOptions.ProxyPort = Math.Clamp((int)(ProxyPort ?? 0), 0, 65535); settings.HistoryIncludeCrArtists = HistoryIncludeCrArtists;
CrunchyrollManager.Instance.CrunOptions.ProxyUsername = ProxyUsername; settings.HistorySkipUnmonitored = HistorySkipUnmonitored;
CrunchyrollManager.Instance.CrunOptions.ProxyPassword = ProxyPassword; settings.HistoryCountSonarr = HistoryCountSonarr;
settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
settings.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
settings.ProxyEnabled = ProxyEnabled;
settings.ProxySocks = ProxySocks;
settings.ProxyHost = ProxyHost;
settings.ProxyPort = Math.Clamp((int)(ProxyPort ?? 0), 0, 65535);
settings.ProxyUsername = ProxyUsername;
settings.ProxyPassword = ProxyPassword;
string historyLang = SelectedHistoryLang.Content + ""; string historyLang = SelectedHistoryLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : CrunchyrollManager.Instance.DefaultLocale; settings.HistoryLang = historyLang != "default" ? historyLang : CrunchyrollManager.Instance.DefaultLocale;
CrunchyrollManager.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + ""; settings.Theme = CurrentAppTheme?.Content + "";
if (_faTheme.CustomAccentColor != (Application.Current?.PlatformSettings?.GetColorValues().AccentColor1)){ if (_faTheme.CustomAccentColor != (Application.Current?.PlatformSettings?.GetColorValues().AccentColor1)){
CrunchyrollManager.Instance.CrunOptions.AccentColor = _faTheme.CustomAccentColor.ToString(); settings.AccentColor = _faTheme.CustomAccentColor.ToString();
} else{ } else{
CrunchyrollManager.Instance.CrunOptions.AccentColor = string.Empty; settings.AccentColor = string.Empty;
} }
CrunchyrollManager.Instance.CrunOptions.History = History; settings.History = History;
var props = new SonarrProperties(); var props = new SonarrProperties();
@ -339,9 +346,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
props.ApiKey = SonarrApiKey; props.ApiKey = SonarrApiKey;
CrunchyrollManager.Instance.CrunOptions.SonarrProperties = props; settings.SonarrProperties = props;
CrunchyrollManager.Instance.CrunOptions.LogMode = LogMode; settings.LogMode = LogMode;
CfgManager.WriteCrSettings(); CfgManager.WriteCrSettings();
} }

View file

@ -57,7 +57,17 @@
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<StackPanel Grid.Column="1" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
<ToggleButton Width="50" Height="50" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsChecked="{Binding QuickAddMode}"
IsEnabled="{Binding !IsLoading}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Add" FontSize="25" />
<TextBlock Text="Fast" TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonSorting" Width="50" Height="50" <ToggleButton x:Name="DropdownButtonSorting" Width="50" Height="50"
@ -147,7 +157,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right" IsExpanded="{Binding IsExpanded}"> <Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right" IsExpanded="{Binding IsExpanded}" IsVisible="{Binding !$parent[UserControl].((vm:UpcomingPageViewModel)DataContext).QuickAddMode}">
<Expander.Styles> <Expander.Styles>
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader"> <Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
@ -329,6 +339,49 @@
</StackPanel> </StackPanel>
<Grid Grid.Column="0" Grid.ColumnSpan="2" IsVisible="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).QuickAddMode}"
Background="#90000000"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Button
Background="Transparent"
IsVisible="{Binding !IsInHistory}"
BorderThickness="0"
FontStyle="Italic"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).AddToHistory}"
CommandParameter="{Binding}"
IsEnabled="{Binding HasCrID}">
<ToolTip.Tip>
<TextBlock Text="Add to history" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Library" FontSize="32" IsVisible="{Binding HasCrID}" />
<controls:SymbolIcon Symbol="Add" FontSize="32" IsVisible="{Binding HasCrID}" />
</StackPanel>
</Button>
<Button
Background="Transparent"
IsVisible="{Binding IsInHistory}"
BorderThickness="0"
FontStyle="Italic"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
IsEnabled="False">
<ToolTip.Tip>
<TextBlock Text="Already in history" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Library" FontSize="32" />
</StackPanel>
</Button>
</Grid>
</Grid> </Grid>
</DataTemplate> </DataTemplate>

View file

@ -32,6 +32,12 @@
</ComboBox> </ComboBox>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History count Missing" Description="The missing count (orange corner) shows all missing episodes, not just new ones">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountMissing}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Include CR Artists" Description="Add Crunchyroll artists (music) to the history"> <controls:SettingsExpanderItem Content="History Include CR Artists" Description="Add Crunchyroll artists (music) to the history">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>
@ -39,7 +45,7 @@
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Add Specials" Description="Add specials to the queue if they weren't downloaded before"> <controls:SettingsExpanderItem Content="History Add Specials" Description="Add specials to the queue/count if they weren't downloaded before">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox> <CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>