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

View file

@ -343,40 +343,6 @@ public class CrSeries{
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){
await crunInstance.CrAuth.RefreshToken(true);
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);

View file

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
@ -296,6 +297,7 @@ public class CrunchyrollManager{
await CrAuth.AuthAnonymous();
}
if (CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
@ -329,6 +331,12 @@ public class CrunchyrollManager{
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){
bool syncError = false;
bool muxError = false;
var notSyncedDubs = "";
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
@ -382,6 +391,7 @@ public class CrunchyrollManager{
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){
@ -391,7 +401,7 @@ public class CrunchyrollManager{
SubLangList = options.DlSubs,
FfmpegOptions = options.FfmpegOptions,
SkipSubMux = options.SkipSubsMux,
Output = fileNameAndPath,
Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}",
Mp4 = options.Mp4,
MuxFonts = options.MuxFonts,
VideoTitle = res.VideoTitle,
@ -411,7 +421,7 @@ public class CrunchyrollManager{
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced,
},
fileNameAndPath, data);
fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", data);
if (result is{ merger: not null, isMuxed: true }){
mergers.Add(result.merger);
@ -479,6 +489,7 @@ public class CrunchyrollManager{
fileNameAndPath, data);
syncError = result.syncError;
notSyncedDubs = result.notSyncedDubs;
muxError = !result.isMuxed;
if (result is{ merger: not null, isMuxed: true }){
@ -512,7 +523,7 @@ public class CrunchyrollManager{
Percent = 100,
Time = 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){
@ -551,7 +562,8 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh();
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 }){
@ -648,7 +660,7 @@ public class CrunchyrollManager{
#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;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -657,7 +669,7 @@ public class CrunchyrollManager{
muxToMp3 = true;
} else{
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;
List<string> notSyncedDubs =[];
if (options is{ SyncTiming: true, DlVideoOnce: true }){
crunchyEpMeta.DownloadProgress = new DownloadProgress(){
@ -755,6 +769,7 @@ public class CrunchyrollManager{
if (delay <= -100){
syncError = true;
notSyncedDubs.Add(syncVideo.Lang.CrLocale ?? syncVideo.Language.CrLocale);
continue;
}
@ -794,7 +809,7 @@ public class CrunchyrollManager{
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){
@ -974,7 +989,7 @@ public class CrunchyrollManager{
List<string> compiledChapters = new List<string>();
if (options.Chapters){
if (options.Chapters && !data.OnlySubs){
await ParseChapters(mediaGuid, compiledChapters);
if (compiledChapters.Count == 0 && primaryVersion.MediaGuid != null && mediaGuid != primaryVersion.MediaGuid){
@ -1052,18 +1067,16 @@ public class CrunchyrollManager{
if (pbStreams?.Keys != null){
var pb = pbStreams.Select(v => {
v.Value.HardsubLang = v.Value.HardsubLocale != null
? Languages.FixAndFindCrLc(v.Value.HardsubLocale.GetEnumMemberValue()).Locale
: null;
if (v.Value.HardsubLocale != null && v.Value.HardsubLang != null && !hsLangs.Contains(v.Value.HardsubLocale.GetEnumMemberValue())){
hsLangs.Add(v.Value.HardsubLang);
if (v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
hsLangs.Add(v.Value.HardsubLang.CrLocale);
}
return new StreamDetailsPop{
Url = v.Value.Url,
IsHardsubbed = v.Value.IsHardsubbed,
HardsubLocale = v.Value.HardsubLocale,
HardsubLang = v.Value.HardsubLang,
AudioLang = v.Value.AudioLang,
AudioLang = pbData.Meta?.AudioLocale ?? Languages.DEFAULT_lang,
Type = v.Value.Type,
Format = "drm_adaptive_dash",
};
@ -1083,16 +1096,16 @@ public class CrunchyrollManager{
};
}
var audDub = "";
var audDub = Languages.DEFAULT_lang;
if (pbData.Meta != null){
audDub = Languages.FindLang(Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue())).Code;
audDub = pbData.Meta.AudioLocale;
}
hsLangs = Languages.SortTags(hsLangs);
streams = streams.Select(s => {
s.AudioLang = audDub;
s.HardsubLang = string.IsNullOrEmpty(s.HardsubLang) ? "-" : s.HardsubLang;
s.HardsubLang = s.HardsubLang;
s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}";
return s;
}).ToList();
@ -1102,21 +1115,15 @@ public class CrunchyrollManager{
if (options.Hslang != "none"){
if (hsLangs.IndexOf(options.Hslang) > -1){
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{
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){
Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
}
if (dlVideoOnce && options.DlVideoOnce){
streams = streams.Where((s) => {
if (s.HardsubLang != "-"){
return false;
}
return true;
}).ToList();
streams = streams.Where((s) => !s.IsHardsubbed).ToList();
} else{
if (hsLangs.Count > 0){
var dialog = new ContentDialog(){
@ -1141,7 +1148,7 @@ public class CrunchyrollManager{
if (hsLangs.IndexOf(selectedValue) > -1){
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;
}
} else{
@ -1167,13 +1174,7 @@ public class CrunchyrollManager{
}
}
} else{
streams = streams.Where((s) => {
if (s.HardsubLang != "-"){
return false;
}
return true;
}).ToList();
streams = streams.Where((s) => !s.IsHardsubbed).ToList();
if (streams.Count < 1){
Console.Error.WriteLine("Raw streams not available!");
@ -1205,7 +1206,7 @@ public class CrunchyrollManager{
}
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 }){
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null);
@ -1231,7 +1232,7 @@ public class CrunchyrollManager{
//Parse MPD Playlists
var crLocal = "";
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);
@ -1348,10 +1349,10 @@ public class CrunchyrollManager{
variables.Add(new Variable("width", chosenVideoSegments.quality.width, false));
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){
Console.Error.WriteLine($"Unable to find language for code {curStream.AudioLang}");
MainWindow.Instance.ShowError($"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.CrLocale}");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
@ -1465,10 +1466,6 @@ public class CrunchyrollManager{
};
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");
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>
{ { "dt-custom-data", authData.CustomData ?? string.Empty },{ "x-dt-auth-token", authData.Token ?? string.Empty } };
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, "https://lic.drmtoday.com/license-proxy-widevine/cenc/", authDataDict);
{ { "authorization", "Bearer " + Token?.access_token },{"x-cr-content-id", mediaGuid},{"x-cr-video-token", pbData.Meta?.Token ?? string.Empty} };
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
if (encryptionKeys.Count == 0){
Console.Error.WriteLine("Failed to get encryption keys");
@ -1527,7 +1494,6 @@ public class CrunchyrollManager{
};
}
if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
@ -1610,6 +1576,7 @@ public class CrunchyrollManager{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s",
Lang = lang,
Language = lang,
IsPrimary = isPrimary
};
files.Add(videoDownloadMedia);
@ -1694,6 +1661,7 @@ public class CrunchyrollManager{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s",
Lang = lang,
Language = lang,
IsPrimary = isPrimary
};
files.Add(videoDownloadMedia);
@ -1753,13 +1721,7 @@ public class CrunchyrollManager{
}
// Finding language by code
var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang) ?? new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
var lang = Languages.languages.FirstOrDefault(l => l == curStream?.AudioLang) ?? Languages.DEFAULT_lang;
if (lang.Code == "und"){
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";
if (!File.Exists(fullPath)){
@ -1821,6 +1783,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description,
Path = fullPath,
Lang = Languages.DEFAULT_lang
});
data.downloadedFiles.Add(fullPath);
} else{
@ -1828,6 +1791,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description,
Path = fullPath,
Lang = Languages.DEFAULT_lang
});
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){
if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
@ -1894,7 +1858,7 @@ public class CrunchyrollManager{
var langItem = subsItem.locale;
var sxData = new SxItem();
sxData.Language = langItem;
var isSigns = langItem.Code == audDub && !subsItem.isCC;
var isSigns = langItem.CrLocale == audDub.CrLocale && !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")));
@ -2127,7 +2091,7 @@ public class CrunchyrollManager{
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);
if (!playbackRequestResponse.IsOk){
@ -2138,7 +2102,7 @@ public class CrunchyrollManager{
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId);
} else{
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);
if (!playbackRequestResponse.IsOk){
@ -2190,30 +2154,36 @@ public class CrunchyrollManager{
var derivedPlayCrunchyStreams = new CrunchyStreams();
if (playStream.HardSubs != null){
//hlang "none" is no hardsube same url as the default url
foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url,
HardsubLocale = stream.Hlang
IsHardsubbed = true,
HardsubLocale = stream.Hlang,
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
};
}
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
Url = playStream.Url,
HardsubLocale = Locale.DefaulT
IsHardsubbed = false,
HardsubLocale = Locale.DefaulT,
HardsubLang = Languages.DEFAULT_lang
};
temppbData.Data = derivedPlayCrunchyStreams;
temppbData.Total = 1;
temppbData.Meta = new PlaybackMeta{
AudioLocale = playStream.AudioLocale,
AudioLocale = Languages.FindLang(playStream.AudioLocale != null ? playStream.AudioLocale.GetEnumMemberValue() : ""),
Versions = playStream.Versions,
Bifs = new List<string>{ playStream.Bifs ?? "" },
MediaId = mediaId,
Captions = playStream.Captions,
Subtitles = new Subtitles()
Subtitles = new Subtitles(),
Token = playStream.Token,
};
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;
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];
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 + "";
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang;
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";

View file

@ -41,10 +41,10 @@ public class Widevine{
if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){
foreach (var file in Directory.EnumerateFiles(CfgManager.PathWIDEVINE_DIR)){
var fileInfo = new FileInfo(file);
if (fileInfo.Length >= 1024 * 8 || fileInfo.Attributes.HasFlag(FileAttributes.Directory))
continue;
string fileContents = File.ReadAllText(file, Encoding.UTF8);
if (IsPrivateKey(fileContents)){
@ -107,7 +107,9 @@ public class Widevine{
}
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);

View file

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

View file

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

View file

@ -240,7 +240,7 @@ public class Helpers{
if (e.Data.StartsWith("Error:")){
Console.Error.WriteLine(e.Data);
} 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){
//Group by language
var languageGroups = allMedia
.Where(media =>
!string.IsNullOrEmpty(media.Lang.CrLocale) ||
(media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null &&
!string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang.CrLocale))
.Where(media => media.Type != DownloadMediaType.Description &&
(!string.IsNullOrEmpty(media.Lang?.CrLocale) ||
(media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null } &&
!string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang?.CrLocale)))
)
.GroupBy(media => {
if (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null){
return media.RelatedVideoDownloadMedia.Lang.CrLocale;
if (media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null }){
return media.RelatedVideoDownloadMedia.Lang?.CrLocale ?? "und";
}
return media.Lang.CrLocale;
return media.Lang?.CrLocale ?? "und";
})
.ToDictionary(group => group.Key, group => group.ToList());
//Find and add Description media to each group
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){
group.Add(description);
group.Add(descriptionMedia[0]);
}
}
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 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 readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
public static readonly string BetaCms = ApiBeta + "/cms/v2";
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 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";
}

View file

@ -361,6 +361,10 @@ public class Merger{
case < 0.1:
return startOffset;
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;
default:
return endOffset;

View file

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

View file

@ -402,6 +402,16 @@ public class CrunchyEpMetaData{
public List<EpisodeVersion>? Versions{ get; set; }
public bool IsSubbed{ 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{

View file

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

View file

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

View file

@ -9,35 +9,53 @@ namespace CRD.Utils.Structs;
public class Languages{
public static readonly LanguageItem[] languages ={
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 = "en-US", Locale = "en", Code = "eng", Name = "English" },
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-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-PT", Locale = "pt-PT", Code = "por", Name = "Portuguese (Portugal)", Language = "Portugues (Portugal)" },
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 = "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 = "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 = "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 = "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){
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){
if (string.IsNullOrEmpty(cr_locale)){
return new LanguageItem();
return DEFAULT_lang;
}
string str = FixLanguageTag(cr_locale);
@ -77,7 +95,7 @@ public class Languages{
string fileName = $"{fnOutput}";
if (addIndexAndLangCode){
fileName += $".{langItem.CrLocale}"; //.{subsIndex}
fileName += $".{langItem.Locale}"; //.{subsIndex}
}
//removed .{langItem.language} from file name at end
@ -123,13 +141,7 @@ public class Languages{
if (lang?.CrLocale != null){
return lang;
} else{
return new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
return DEFAULT_lang;
}
}
@ -139,13 +151,7 @@ public class Languages{
if (filteredLocale != null){
return (LanguageItem)filteredLocale;
} else{
return new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
return DEFAULT_lang;
}
}

View file

@ -501,28 +501,30 @@ public partial class HistoryPageViewModel : ViewModelBase{
[RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){
var downloadTasks = season.EpisodesList
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
foreach (var episode in season.EpisodesList){
await episode.DownloadEpisode();
}
}
[RelayCommand]
public async Task DownloadSeasonMissing(HistorySeason season){
var downloadTasks = season.EpisodesList
.Where(episode => !episode.WasDownloaded)
.Select(episode => episode.DownloadEpisode());
var missingEpisodes = season.EpisodesList
.Where(episode => !episode.WasDownloaded).ToList();
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]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
var downloadTasks = season.EpisodesList
.Where(episode => !episode.SonarrHasFile)
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
await episode.DownloadEpisode();
}
}
[RelayCommand]

View file

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

View file

@ -146,6 +146,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{
[ObservableProperty]
private int _selectedIndex;
[ObservableProperty]
private bool _quickAddMode;
[ObservableProperty]
private bool _isLoading;
@ -251,6 +254,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
[RelayCommand]
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 (CrunchyrollManager.Instance.CrunOptions.History){
series.IsInHistory = true;
@ -431,7 +439,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
}
public void SelectionChangedOfSeries(AnilistSeries? value){
if (value != null) value.IsExpanded = !value.IsExpanded;
if (value != null && !QuickAddMode) value.IsExpanded = !value.IsExpanded;
SelectedSeries = null;
SelectedIndex = -1;
}

View file

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

View file

@ -57,7 +57,17 @@
</ItemsControl.ItemTemplate>
</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>
<ToggleButton x:Name="DropdownButtonSorting" Width="50" Height="50"
@ -147,7 +157,7 @@
</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>
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent" />
@ -329,6 +339,49 @@
</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>
</DataTemplate>

View file

@ -32,6 +32,12 @@
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</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.Footer>
@ -39,7 +45,7 @@
</controls:SettingsExpanderItem.Footer>
</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>
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>